Merge branch 'master' into master-android

This commit is contained in:
Evgeny Poberezkin
2024-09-20 09:45:53 +01:00
34 changed files with 963 additions and 372 deletions

View File

@@ -82,7 +82,7 @@ object ChatModel {
val desktopOnboardingRandomPassword = mutableStateOf(false)
// set when app is opened via contact or invitation URI (rhId, uri)
val appOpenUrl = mutableStateOf<Pair<Long?, URI>?>(null)
val appOpenUrl = mutableStateOf<Pair<Long?, String>?>(null)
// Needed to check for bottom nav bar and to apply or not navigation bar color on Android
val newChatSheetVisible = mutableStateOf(false)
@@ -1404,6 +1404,14 @@ class Group (
var members: List<GroupMember>
)
@Serializable
sealed class ForwardConfirmation {
@Serializable @SerialName("filesNotAccepted") data class FilesNotAccepted(val fileIds: List<Long>) : ForwardConfirmation()
@Serializable @SerialName("filesInProgress") data class FilesInProgress(val filesCount: Int) : ForwardConfirmation()
@Serializable @SerialName("filesMissing") data class FilesMissing(val filesCount: Int) : ForwardConfirmation()
@Serializable @SerialName("filesFailed") data class FilesFailed(val filesCount: Int) : ForwardConfirmation()
}
@Serializable
data class GroupInfo (
val groupId: Long,

View File

@@ -1,18 +1,19 @@
package chat.simplex.common.model
import SectionItemView
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import chat.simplex.common.views.helpers.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.ChatController.getNetCfg
import chat.simplex.common.model.ChatController.setNetCfg
import chat.simplex.common.model.ChatModel.changingActiveUserMutex
@@ -905,7 +906,15 @@ object ChatController {
return processSendMessageCmd(rh, cmd)?.map { it.chatItem }
}
suspend fun apiPlanForwardChatItems(rh: Long?, fromChatType: ChatType, fromChatId: Long, chatItemIds: List<Long>): CR.ForwardPlan? {
return when (val r = sendCmd(rh, CC.ApiPlanForwardChatItems(fromChatType, fromChatId, chatItemIds))) {
is CR.ForwardPlan -> r
else -> {
apiErrorAlert("apiPlanForwardChatItems", generalGetString(MR.strings.error_forwarding_messages), r)
null
}
}
}
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))
@@ -1541,50 +1550,132 @@ object ChatController {
}
}
suspend fun apiReceiveFile(rh: Long?, fileId: Long, userApprovedRelays: Boolean, encrypted: Boolean, inline: Boolean? = null, auto: Boolean = false): AChatItem? {
// -1 here is to override default behavior of providing current remote host id because file can be asked by local device while remote is connected
val r = sendCmd(rh, CC.ReceiveFile(fileId, userApprovedRelays = userApprovedRelays, encrypt = encrypted, inline = inline))
return when (r) {
is CR.RcvFileAccepted -> r.chatItem
is CR.RcvFileAcceptedSndCancelled -> {
Log.d(TAG, "apiReceiveFile error: sender cancelled file transfer")
if (!auto) {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.cannot_receive_file),
generalGetString(MR.strings.sender_cancelled_file_transfer)
)
}
null
}
suspend fun receiveFiles(rhId: Long?, user: UserLike, fileIds: List<Long>, userApprovedRelays: Boolean = false, auto: Boolean = false) {
val fileIdsToApprove = mutableListOf<Long>()
val srvsToApprove = mutableSetOf<String>()
val otherFileErrs = mutableListOf<CR>()
else -> {
if (!(networkErrorAlert(r))) {
val maybeChatError = chatError(r)
if (maybeChatError is ChatErrorType.FileCancelled || maybeChatError is ChatErrorType.FileAlreadyReceiving) {
Log.d(TAG, "apiReceiveFile ignoring FileCancelled or FileAlreadyReceiving error")
} else if (maybeChatError is ChatErrorType.FileNotApproved) {
Log.d(TAG, "apiReceiveFile FileNotApproved error")
if (!auto) {
val srvs = maybeChatError.unknownServers.map{ serverHostname(it) }
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.file_not_approved_title),
text = generalGetString(MR.strings.file_not_approved_descr).format(srvs.sorted().joinToString(separator = ", ")),
confirmText = generalGetString(MR.strings.download_file),
onConfirm = {
val user = chatModel.currentUser.value
if (user != null) {
withBGApi { chatModel.controller.receiveFile(rh, user, fileId, userApprovedRelays = true) }
}
},
)
}
} else if (!auto) {
apiErrorAlert("apiReceiveFile", generalGetString(MR.strings.error_receiving_file), r)
}
for (fileId in fileIds) {
val r = sendCmd(
rhId, CC.ReceiveFile(
fileId,
userApprovedRelays = userApprovedRelays || !appPrefs.privacyAskToApproveRelays.get(),
encrypt = appPrefs.privacyEncryptLocalFiles.get(),
inline = null
)
)
if (r is CR.RcvFileAccepted) {
chatItemSimpleUpdate(rhId, user, r.chatItem)
} else {
val maybeChatError = chatError(r)
if (maybeChatError is ChatErrorType.FileNotApproved) {
fileIdsToApprove.add(maybeChatError.fileId)
srvsToApprove.addAll(maybeChatError.unknownServers.map { serverHostname(it) })
} else {
otherFileErrs.add(r)
}
null
}
}
if (!auto) {
// If there are not approved files, alert is shown the same way both in case of singular and plural files reception
if (fileIdsToApprove.isNotEmpty()) {
showFilesToApproveAlert(
srvsToApprove = srvsToApprove,
otherFileErrs = otherFileErrs,
approveFiles = {
withBGApi {
receiveFiles(
rhId = rhId,
user = user,
fileIds = fileIdsToApprove,
userApprovedRelays = true
)
}
}
)
} else if (otherFileErrs.size == 1) { // If there is a single other error, we differentiate on it
when (val errCR = otherFileErrs.first()) {
is CR.RcvFileAcceptedSndCancelled -> {
Log.d(TAG, "receiveFiles error: sender cancelled file transfer")
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.cannot_receive_file),
generalGetString(MR.strings.sender_cancelled_file_transfer)
)
}
else -> {
val maybeChatError = chatError(errCR)
if (maybeChatError is ChatErrorType.FileCancelled || maybeChatError is ChatErrorType.FileAlreadyReceiving) {
Log.d(TAG, "receiveFiles ignoring FileCancelled or FileAlreadyReceiving error")
} else {
apiErrorAlert("receiveFiles", generalGetString(MR.strings.error_receiving_file), errCR)
}
}
}
} else if (otherFileErrs.size > 1) { // If there are multiple other errors, we show general alert
val errsStr = otherFileErrs.map { json.encodeToString(it) }.joinToString(separator = "\n")
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.error_receiving_file),
text = String.format(generalGetString(MR.strings.n_file_errors), otherFileErrs.size, errsStr),
shareText = true
)
}
}
}
private fun showFilesToApproveAlert(
srvsToApprove: Set<String>,
otherFileErrs: List<CR>,
approveFiles: (() -> Unit)
) {
val srvsToApproveStr = srvsToApprove.sorted().joinToString(separator = ", ")
val alertText =
generalGetString(MR.strings.file_not_approved_descr).format(srvsToApproveStr) +
(if (otherFileErrs.isNotEmpty()) "\n" + generalGetString(MR.strings.n_other_file_errors).format(otherFileErrs.size) else "")
AlertManager.shared.showAlertDialogButtonsColumn(generalGetString(MR.strings.file_not_approved_title), alertText, belowTextContent = {
if (otherFileErrs.isNotEmpty()) {
val clipboard = LocalClipboardManager.current
SimpleButtonFrame(click = {
clipboard.setText(AnnotatedString(otherFileErrs.map { json.encodeToString(it) }.joinToString(separator = "\n")))
}) {
Icon(
painterResource(MR.images.ic_content_copy),
contentDescription = null,
tint = MaterialTheme.colors.primary,
modifier = Modifier.padding(end = 8.dp)
)
Text(generalGetString(MR.strings.copy_error), color = MaterialTheme.colors.primary)
}
}
}) {
Row(
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING),
horizontalArrangement = Arrangement.SpaceBetween
) {
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
// Wait before focusing to prevent auto-confirming if a user used Enter key on hardware keyboard
delay(200)
focusRequester.requestFocus()
}
TextButton(onClick = AlertManager.shared::hideAlert) { Text(generalGetString(MR.strings.cancel_verb)) }
TextButton(onClick = {
approveFiles.invoke()
AlertManager.shared.hideAlert()
}, Modifier.focusRequester(focusRequester)) { Text(generalGetString(MR.strings.download_file)) }
}
}
}
suspend fun receiveFile(rhId: Long?, user: UserLike, fileId: Long, userApprovedRelays: Boolean = false, auto: Boolean = false) {
receiveFiles(
rhId = rhId,
user = user,
fileIds = listOf(fileId),
userApprovedRelays = userApprovedRelays,
auto = auto
)
}
suspend fun cancelFile(rh: Long?, user: User, fileId: Long) {
@@ -2689,19 +2780,6 @@ object ChatController {
}
}
suspend fun receiveFile(rhId: Long?, user: UserLike, fileId: Long, userApprovedRelays: Boolean = false, auto: Boolean = false) {
val chatItem = apiReceiveFile(
rhId,
fileId,
userApprovedRelays = userApprovedRelays || !appPrefs.privacyAskToApproveRelays.get(),
encrypted = appPrefs.privacyEncryptLocalFiles.get(),
auto = auto
)
if (chatItem != null) {
chatItemSimpleUpdate(rhId, user, chatItem)
}
}
suspend fun leaveGroup(rh: Long?, groupId: Long) {
val groupInfo = apiLeaveGroup(rh, groupId)
if (groupInfo != null) {
@@ -2914,6 +2992,7 @@ sealed class CC {
class ApiDeleteChatItem(val type: ChatType, val id: Long, val itemIds: List<Long>, val mode: CIDeleteMode): CC()
class ApiDeleteMemberChatItem(val groupId: Long, val itemIds: List<Long>): CC()
class ApiChatItemReaction(val type: ChatType, val id: Long, val itemId: Long, val add: Boolean, val reaction: MsgReaction): CC()
class ApiPlanForwardChatItems(val fromChatType: ChatType, val fromChatId: Long, val chatItemIds: List<Long>): CC()
class ApiForwardChatItems(val toChatType: ChatType, val toChatId: Long, val fromChatType: ChatType, val fromChatId: Long, val itemIds: List<Long>, val ttl: Int?): 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()
@@ -3072,6 +3151,9 @@ sealed class CC {
val ttlStr = if (ttl != null) "$ttl" else "default"
"/_forward ${chatRef(toChatType, toChatId)} ${chatRef(fromChatType, fromChatId)} ${itemIds.joinToString(",")} ttl=${ttlStr}"
}
is ApiPlanForwardChatItems -> {
"/_forward plan ${chatRef(fromChatType, fromChatId)} ${chatItemIds.joinToString(",")}"
}
is ApiNewGroup -> "/_group $userId incognito=${onOff(incognito)} ${json.encodeToString(groupProfile)}"
is ApiAddMember -> "/_add #$groupId $contactId ${memberRole.memberRole}"
is ApiJoinGroup -> "/_join #$groupId"
@@ -3216,6 +3298,7 @@ sealed class CC {
is ApiDeleteMemberChatItem -> "apiDeleteMemberChatItem"
is ApiChatItemReaction -> "apiChatItemReaction"
is ApiForwardChatItems -> "apiForwardChatItems"
is ApiPlanForwardChatItems -> "apiPlanForwardChatItems"
is ApiNewGroup -> "apiNewGroup"
is ApiAddMember -> "apiAddMember"
is ApiJoinGroup -> "apiJoinGroup"
@@ -4878,6 +4961,7 @@ sealed class CR {
@Serializable @SerialName("chatItemNotChanged") class ChatItemNotChanged(val user: UserRef, val chatItem: AChatItem): CR()
@Serializable @SerialName("chatItemReaction") class ChatItemReaction(val user: UserRef, val added: Boolean, val reaction: ACIReaction): CR()
@Serializable @SerialName("chatItemsDeleted") class ChatItemsDeleted(val user: UserRef, val chatItemDeletions: List<ChatItemDeletion>, val byUser: Boolean): CR()
@Serializable @SerialName("forwardPlan") class ForwardPlan(val user: UserRef, val itemsCount: Int, val chatItemIds: List<Long>, val forwardConfirmation: ForwardConfirmation? = null): CR()
// group events
@Serializable @SerialName("groupCreated") class GroupCreated(val user: UserRef, val groupInfo: GroupInfo): CR()
@Serializable @SerialName("sentGroupInvitation") class SentGroupInvitation(val user: UserRef, val groupInfo: GroupInfo, val contact: Contact, val member: GroupMember): CR()
@@ -5055,6 +5139,7 @@ sealed class CR {
is ChatItemNotChanged -> "chatItemNotChanged"
is ChatItemReaction -> "chatItemReaction"
is ChatItemsDeleted -> "chatItemsDeleted"
is ForwardPlan -> "forwardPlan"
is GroupCreated -> "groupCreated"
is SentGroupInvitation -> "sentGroupInvitation"
is UserAcceptedGroupSent -> "userAcceptedGroupSent"
@@ -5224,6 +5309,7 @@ sealed class CR {
is ChatItemNotChanged -> withUser(user, json.encodeToString(chatItem))
is ChatItemReaction -> withUser(user, "added: $added\n${json.encodeToString(reaction)}")
is ChatItemsDeleted -> withUser(user, "${chatItemDeletions.map { (deletedChatItem, toChatItem) -> "deletedChatItem: ${json.encodeToString(deletedChatItem)}\ntoChatItem: ${json.encodeToString(toChatItem)}" }} \nbyUser: $byUser")
is ForwardPlan -> withUser(user, "itemsCount: $itemsCount\nchatItemIds: ${json.encodeToString(chatItemIds)}\nforwardConfirmation: ${json.encodeToString(forwardConfirmation)}")
is GroupCreated -> withUser(user, json.encodeToString(groupInfo))
is SentGroupInvitation -> withUser(user, "groupInfo: $groupInfo\ncontact: $contact\nmember: $member")
is UserAcceptedGroupSent -> json.encodeToString(groupInfo)

View File

@@ -21,6 +21,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.*
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.*
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
@@ -172,7 +173,30 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
})
}
}
}
},
forwardItems = {
val itemIds = selectedChatItems.value
if (itemIds != null) {
withBGApi {
val chatItemIds = itemIds.toList()
val forwardPlan = controller.apiPlanForwardChatItems(
rh = chatRh,
fromChatType = chatInfo.chatType,
fromChatId = chatInfo.apiId,
chatItemIds = chatItemIds
)
if (forwardPlan != null) {
if (forwardPlan.chatItemIds.count() < chatItemIds.count() || forwardPlan.forwardConfirmation != null) {
handleForwardConfirmation(chatRh, forwardPlan, chatInfo)
} else {
forwardContent(forwardPlan.chatItemIds, chatInfo)
}
}
}
}
},
)
}
},
@@ -347,9 +371,9 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
openDirectChat(chatRh, contactId, chatModel)
}
},
forwardItem = { cItem, cInfo ->
forwardItem = { cInfo, cItem ->
chatModel.chatId.value = null
chatModel.sharedContent.value = SharedContent.Forward(cInfo, cItem)
chatModel.sharedContent.value = SharedContent.Forward(listOf(cItem), cInfo)
},
updateContactStats = { contact ->
withBGApi {
@@ -1416,6 +1440,65 @@ private fun TopEndFloatingButton(
}
}
@Composable
private fun DownloadFilesButton(
forwardConfirmation: ForwardConfirmation.FilesNotAccepted,
rhId: Long?,
modifier: Modifier = Modifier,
contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding
) {
val user = chatModel.currentUser.value
if (user != null) {
TextButton(
contentPadding = contentPadding,
modifier = modifier,
onClick = {
AlertManager.shared.hideAlert()
withBGApi {
controller.receiveFiles(
rhId = rhId,
fileIds = forwardConfirmation.fileIds,
user = user
)
}
}
) {
Text(stringResource(MR.strings.forward_files_not_accepted_receive_files), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
}
}
@Composable
private fun ForwardButton(
forwardPlan: CR.ForwardPlan,
chatInfo: ChatInfo,
modifier: Modifier = Modifier,
contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding
) {
TextButton(
onClick = {
forwardContent(forwardPlan.chatItemIds, chatInfo)
AlertManager.shared.hideAlert()
},
modifier = modifier,
contentPadding = contentPadding
) {
Text(stringResource(MR.strings.forward_chat_item), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
}
@Composable
private fun ButtonRow(horizontalArrangement: Arrangement.Horizontal, content: @Composable() (RowScope.() -> Unit)) {
Row(
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING),
horizontalArrangement = horizontalArrangement
) {
content()
}
}
val chatViewScrollState = MutableStateFlow(false)
fun addGroupMembers(groupInfo: GroupInfo, rhId: Long?, view: Any? = null, close: (() -> Unit)? = null) {
@@ -1712,6 +1795,83 @@ private fun ViewConfiguration.bigTouchSlop(slop: Float = 50f) = object: ViewConf
override val touchSlop: Float get() = slop
}
private fun forwardContent(chatItemsIds: List<Long>, chatInfo: ChatInfo) {
chatModel.chatId.value = null
chatModel.sharedContent.value = SharedContent.Forward(
chatModel.chatItems.value.filter { chatItemsIds.contains(it.id) },
chatInfo
)
}
private fun forwardConfirmationAlertDescription(forwardConfirmation: ForwardConfirmation): String {
return when (forwardConfirmation) {
is ForwardConfirmation.FilesNotAccepted -> String.format(generalGetString(MR.strings.forward_files_not_accepted_desc), forwardConfirmation.fileIds.count())
is ForwardConfirmation.FilesInProgress -> String.format(generalGetString(MR.strings.forward_files_in_progress_desc), forwardConfirmation.filesCount)
is ForwardConfirmation.FilesFailed -> String.format(generalGetString(MR.strings.forward_files_failed_to_receive_desc), forwardConfirmation.filesCount)
is ForwardConfirmation.FilesMissing -> String.format(generalGetString(MR.strings.forward_files_missing_desc), forwardConfirmation.filesCount)
}
}
private fun handleForwardConfirmation(
rhId: Long?,
forwardPlan: CR.ForwardPlan,
chatInfo: ChatInfo
) {
var alertDescription = if (forwardPlan.forwardConfirmation != null) forwardConfirmationAlertDescription(forwardPlan.forwardConfirmation) else ""
if (forwardPlan.chatItemIds.isNotEmpty()) {
alertDescription += "\n${generalGetString(MR.strings.forward_alert_forward_messages_without_files)}"
}
AlertManager.shared.showAlertDialogButtonsColumn(
title = if (forwardPlan.chatItemIds.isNotEmpty())
String.format(generalGetString(MR.strings.forward_alert_title_messages_to_forward), forwardPlan.chatItemIds.count()) else
generalGetString(MR.strings.forward_alert_title_nothing_to_forward),
text = alertDescription,
buttons = {
if (forwardPlan.chatItemIds.isNotEmpty()) {
when (val confirmation = forwardPlan.forwardConfirmation) {
is ForwardConfirmation.FilesNotAccepted -> {
val fillMaxWidthModifier = Modifier.fillMaxWidth()
val contentPadding = PaddingValues(vertical = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL)
Column {
ForwardButton(forwardPlan, chatInfo, fillMaxWidthModifier, contentPadding)
DownloadFilesButton(confirmation, rhId, fillMaxWidthModifier, contentPadding)
TextButton(onClick = { AlertManager.shared.hideAlert() }, modifier = fillMaxWidthModifier, contentPadding = contentPadding) {
Text(stringResource(MR.strings.cancel_verb), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
}
}
else -> {
ButtonRow(Arrangement.SpaceBetween) {
TextButton(onClick = { AlertManager.shared.hideAlert() }) {
Text(stringResource(MR.strings.cancel_verb), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
ForwardButton(forwardPlan, chatInfo)
}
}
}
} else {
when (val confirmation = forwardPlan.forwardConfirmation) {
is ForwardConfirmation.FilesNotAccepted -> {
ButtonRow(Arrangement.SpaceBetween) {
TextButton(onClick = { AlertManager.shared.hideAlert() }) {
Text(stringResource(MR.strings.cancel_verb), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
DownloadFilesButton(confirmation, rhId)
}
}
else -> ButtonRow(Arrangement.Center) {
TextButton(onClick = { AlertManager.shared.hideAlert() }) {
Text(stringResource(MR.strings.ok), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
}
}
}
}
)
}
@Preview/*(
uiMode = Configuration.UI_MODE_NIGHT_YES,
showBackground = true,

View File

@@ -13,7 +13,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontStyle
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
@@ -49,7 +48,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 class ForwardingItems(val chatItems: List<ChatItem>, val fromChatInfo: ChatInfo): ComposeContextItem()
}
@Serializable
@@ -85,7 +84,7 @@ data class ComposeState(
}
val forwarding: Boolean
get() = when (contextItem) {
is ComposeContextItem.ForwardingItem -> true
is ComposeContextItem.ForwardingItems -> true
else -> false
}
val sendEnabled: () -> Boolean
@@ -407,33 +406,41 @@ fun ComposeView(
return null
}
suspend fun sendMessageAsync(text: String?, live: Boolean, ttl: Int?): ChatItem? {
suspend fun sendMessageAsync(text: String?, live: Boolean, ttl: Int?): List<ChatItem>? {
val cInfo = chat.chatInfo
val cs = composeState.value
var sent: ChatItem?
var sent: List<ChatItem>?
val msgText = text ?: cs.message
fun sending() {
composeState.value = composeState.value.copy(inProgress = true)
}
suspend fun forwardItem(rhId: Long?, forwardedItem: ChatItem, fromChatInfo: ChatInfo, ttl: Int?): ChatItem? {
suspend fun forwardItem(rhId: Long?, forwardedItem: List<ChatItem>, fromChatInfo: ChatInfo, ttl: Int?): List<ChatItem>? {
val chatItems = controller.apiForwardChatItems(
rh = rhId,
toChatType = chat.chatInfo.chatType,
toChatId = chat.chatInfo.apiId,
fromChatType = fromChatInfo.chatType,
fromChatId = fromChatInfo.apiId,
itemIds = listOf(forwardedItem.id),
itemIds = forwardedItem.map { it.id },
ttl = ttl
)
chatItems?.forEach { chatItem ->
withChats {
addChatItem(rhId, chat.chatInfo, chatItem)
}
}
// TODO batch send: forward multiple messages
return chatItems?.firstOrNull()
if (chatItems != null && chatItems.count() < forwardedItem.count()) {
AlertManager.shared.showAlertMsg(
title = String.format(generalGetString(MR.strings.forward_files_messages_deleted_after_selection_title), forwardedItem.count() - chatItems.count()),
text = generalGetString(MR.strings.forward_files_messages_deleted_after_selection_desc)
)
}
return chatItems
}
fun checkLinkPreview(): MsgContent {
@@ -506,16 +513,25 @@ 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, ttl = ttl)
} else if (cs.contextItem is ComposeContextItem.ForwardingItems) {
sent = forwardItem(chat.remoteHostId, cs.contextItem.chatItems, cs.contextItem.fromChatInfo, ttl = ttl)
if (cs.message.isNotEmpty()) {
sent = send(chat, checkLinkPreview(), quoted = sent?.id, live = false, ttl = ttl)
sent?.mapIndexed { index, message ->
if (index == sent!!.lastIndex) {
send(chat, checkLinkPreview(), quoted = message.id, live = false, ttl = ttl)
} else {
message
}
}
}
} else if (cs.contextItem is ComposeContextItem.EditingItem) {
}
else if (cs.contextItem is ComposeContextItem.EditingItem) {
val ei = cs.contextItem.chatItem
sent = updateMessage(ei, chat, live)
val updatedMessage = updateMessage(ei, chat, live)
sent = if (updatedMessage != null) listOf(updatedMessage) else null
} else if (liveMessage != null && liveMessage.sent) {
sent = updateMessage(liveMessage.chatItem, chat, live)
val updatedMessage = updateMessage(liveMessage.chatItem, chat, live)
sent = if (updatedMessage != null) listOf(updatedMessage) else null
} else {
val msgs: ArrayList<MsgContent> = ArrayList()
val files: ArrayList<CryptoFile> = ArrayList()
@@ -608,21 +624,23 @@ fun ComposeView(
localPath = file.filePath
)
}
sent = send(chat, content, if (index == 0) quotedItemId else null, file,
val sendResult = send(chat, content, if (index == 0) quotedItemId else null, file,
live = if (content !is MsgContent.MCVoice && index == msgs.lastIndex) live else false,
ttl = ttl
)
sent = if (sendResult != null) listOf(sendResult) else null
}
if (sent == null &&
(cs.preview is ComposePreview.MediaPreview ||
cs.preview is ComposePreview.FilePreview ||
cs.preview is ComposePreview.VoicePreview)
) {
sent = send(chat, MsgContent.MCText(msgText), quotedItemId, null, live, ttl)
val sendResult = send(chat, MsgContent.MCText(msgText), quotedItemId, null, live, ttl)
sent = if (sendResult != null) listOf(sendResult) else null
}
}
val wasForwarding = cs.forwarding
val forwardingFromChatId = (cs.contextItem as? ComposeContextItem.ForwardingItem)?.fromChatInfo?.id
val forwardingFromChatId = (cs.contextItem as? ComposeContextItem.ForwardingItems)?.fromChatInfo?.id
clearState(live)
val draft = chatModel.draft.value
if (wasForwarding && chatModel.draftChatId.value == chat.chatInfo.id && forwardingFromChatId != chat.chatInfo.id && draft != null) {
@@ -724,8 +742,8 @@ fun ComposeView(
val typedMsg = cs.message
if ((cs.sendEnabled() || cs.contextItem is ComposeContextItem.QuotedItem) && (cs.liveMessage == null || !cs.liveMessage.sent)) {
val ci = sendMessageAsync(typedMsg, live = true, ttl = null)
if (ci != null) {
composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci, typedMsg = typedMsg, sentMsg = typedMsg, sent = true))
if (!ci.isNullOrEmpty()) {
composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci.last(), typedMsg = typedMsg, sentMsg = typedMsg, sent = true))
}
} else if (cs.liveMessage == null) {
val cItem = chatModel.addLiveDummy(chat.chatInfo)
@@ -745,8 +763,8 @@ fun ComposeView(
val sentMsg = liveMessageToSend(liveMessage, typedMsg)
if (sentMsg != null) {
val ci = sendMessageAsync(sentMsg, live = true, ttl = null)
if (ci != null) {
composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci, typedMsg = typedMsg, sentMsg = sentMsg, sent = true))
if (!ci.isNullOrEmpty()) {
composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci.last(), typedMsg = typedMsg, sentMsg = sentMsg, sent = true))
}
} else if (liveMessage.typedMsg != typedMsg) {
composeState.value = composeState.value.copy(liveMessage = liveMessage.copy(typedMsg = typedMsg))
@@ -805,13 +823,13 @@ fun ComposeView(
fun contextItemView() {
when (val contextItem = composeState.value.contextItem) {
ComposeContextItem.NoContextItem -> {}
is ComposeContextItem.QuotedItem -> ContextItemView(contextItem.chatItem, painterResource(MR.images.ic_reply)) {
is ComposeContextItem.QuotedItem -> ContextItemView(listOf(contextItem.chatItem), painterResource(MR.images.ic_reply), chatType = chat.chatInfo.chatType) {
composeState.value = composeState.value.copy(contextItem = ComposeContextItem.NoContextItem)
}
is ComposeContextItem.EditingItem -> ContextItemView(contextItem.chatItem, painterResource(MR.images.ic_edit_filled)) {
is ComposeContextItem.EditingItem -> ContextItemView(listOf(contextItem.chatItem), painterResource(MR.images.ic_edit_filled), chatType = chat.chatInfo.chatType) {
clearState()
}
is ComposeContextItem.ForwardingItem -> ContextItemView(contextItem.chatItem, painterResource(MR.images.ic_forward), showSender = false) {
is ComposeContextItem.ForwardingItems -> ContextItemView(contextItem.chatItems, painterResource(MR.images.ic_forward), showSender = false, chatType = chat.chatInfo.chatType) {
composeState.value = composeState.value.copy(contextItem = ComposeContextItem.NoContextItem)
}
}
@@ -834,7 +852,7 @@ fun ComposeView(
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),
contextItem = ComposeContextItem.ForwardingItems(shared.chatItems, shared.fromChatInfo),
preview = if (composeState.value.preview is ComposePreview.CLinkPreview) composeState.value.preview else ComposePreview.NoPreview
)
null -> {}

View File

@@ -13,28 +13,31 @@ 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.text.font.FontStyle
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.common.platform.getLoadedFilePath
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource
import kotlinx.datetime.Clock
@Composable
fun ContextItemView(
contextItem: ChatItem,
contextItems: List<ChatItem>,
contextIcon: Painter,
showSender: Boolean = true,
cancelContextItem: () -> Unit
chatType: ChatType,
cancelContextItem: () -> Unit,
) {
val sent = contextItem.chatDir.sent
val sentColor = MaterialTheme.appColors.sentMessage
val receivedColor = MaterialTheme.appColors.receivedMessage
@Composable
fun MessageText(attachment: ImageResource?, lines: Int) {
fun MessageText(contextItem: ChatItem, attachment: ImageResource?, lines: Int) {
val inlineContent: Pair<AnnotatedString.Builder.() -> Unit, Map<String, InlineTextContent>>? = if (attachment != null) {
remember(contextItem.id) {
val inlineContentBuilder: AnnotatedString.Builder.() -> Unit = {
@@ -62,19 +65,24 @@ fun ContextItemView(
)
}
fun attachment(): ImageResource? =
when (contextItem.content.msgContent) {
is MsgContent.MCFile -> MR.images.ic_draft_filled
fun attachment(contextItem: ChatItem): ImageResource? {
val fileIsLoaded = getLoadedFilePath(contextItem.file) != null
return when (contextItem.content.msgContent) {
is MsgContent.MCFile -> if (fileIsLoaded) MR.images.ic_draft_filled else null
is MsgContent.MCImage -> MR.images.ic_image
is MsgContent.MCVoice -> MR.images.ic_play_arrow_filled
is MsgContent.MCVoice -> if (fileIsLoaded) MR.images.ic_play_arrow_filled else null
else -> null
}
}
@Composable
fun ContextMsgPreview(lines: Int) {
MessageText(remember(contextItem.id) { attachment() }, lines)
fun ContextMsgPreview(contextItem: ChatItem, lines: Int) {
MessageText(contextItem, remember(contextItem.id) { attachment(contextItem) }, lines)
}
val sent = contextItems[0].chatDir.sent
Row(
Modifier
.padding(top = 8.dp)
@@ -97,20 +105,27 @@ fun ContextItemView(
contentDescription = stringResource(MR.strings.icon_descr_context),
tint = MaterialTheme.colors.secondary,
)
val sender = contextItem.memberDisplayName
if (showSender && sender != null) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
sender,
style = TextStyle(fontSize = 13.5.sp, color = CurrentColors.value.colors.secondary)
)
ContextMsgPreview(lines = 2)
if (contextItems.count() == 1) {
val contextItem = contextItems[0]
val sender = contextItem.memberDisplayName
if (showSender && sender != null) {
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
sender,
style = TextStyle(fontSize = 13.5.sp, color = CurrentColors.value.colors.secondary)
)
ContextMsgPreview(contextItem, lines = 2)
}
} else {
ContextMsgPreview(contextItem, lines = 3)
}
} else {
ContextMsgPreview(lines = 3)
} else if (contextItems.isNotEmpty()) {
Text(String.format(generalGetString(if (chatType == ChatType.Local) MR.strings.compose_save_messages_n else MR.strings.compose_forward_messages_n), contextItems.count()), fontStyle = FontStyle.Italic)
}
}
IconButton(onClick = cancelContextItem) {
@@ -129,8 +144,9 @@ fun ContextItemView(
fun PreviewContextItemView() {
SimpleXTheme {
ContextItemView(
contextItem = ChatItem.getSampleData(1, CIDirection.DirectRcv(), Clock.System.now(), "hello"),
contextIcon = painterResource(MR.images.ic_edit_filled)
contextItems = listOf(ChatItem.getSampleData(1, CIDirection.DirectRcv(), Clock.System.now(), "hello")),
contextIcon = painterResource(MR.images.ic_edit_filled),
chatType = ChatType.Direct
) {}
}
}

View File

@@ -7,6 +7,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@@ -51,17 +52,29 @@ fun SelectedItemsBottomToolbar(
selectedChatItems: MutableState<Set<Long>?>,
deleteItems: (Boolean) -> Unit, // Boolean - delete for everyone is possible
moderateItems: () -> Unit,
// shareItems: () -> Unit,
forwardItems: () -> Unit,
) {
val deleteEnabled = remember { mutableStateOf(false) }
val deleteForEveryoneEnabled = remember { mutableStateOf(false) }
val canModerate = remember { mutableStateOf(false) }
val moderateEnabled = remember { mutableStateOf(false) }
val forwardEnabled = remember { mutableStateOf(false) }
val allButtonsDisabled = remember { mutableStateOf(false) }
Box {
// It's hard to measure exact height of ComposeView with different fontSizes. Better to depend on actual ComposeView, even empty
ComposeView(chatModel = chatModel, Chat.sampleData, remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, remember { mutableStateOf(null) }, {})
Row(Modifier.matchParentSize().background(MaterialTheme.colors.background), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
Row(
Modifier
.matchParentSize()
.background(MaterialTheme.colors.background)
.pointerInput(Unit) {
detectGesture {
true
}
},
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
IconButton({ deleteItems(deleteForEveryoneEnabled.value) }, enabled = deleteEnabled.value && !allButtonsDisabled.value) {
Icon(
painterResource(MR.images.ic_delete),
@@ -80,18 +93,18 @@ fun SelectedItemsBottomToolbar(
)
}
IconButton({ /*shareItems()*/ }, Modifier.alpha(0f), enabled = false/*!allButtonsDisabled.value*/) {
IconButton({ forwardItems() }, enabled = forwardEnabled.value && !allButtonsDisabled.value) {
Icon(
painterResource(MR.images.ic_share),
painterResource(MR.images.ic_forward),
null,
Modifier.size(22.dp),
tint = if (allButtonsDisabled.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
tint = if (!forwardEnabled.value || allButtonsDisabled.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
)
}
}
}
LaunchedEffect(chatInfo, chatItems, selectedChatItems.value) {
recheckItems(chatInfo, chatItems, selectedChatItems, deleteEnabled, deleteForEveryoneEnabled, canModerate, moderateEnabled, allButtonsDisabled)
recheckItems(chatInfo, chatItems, selectedChatItems, deleteEnabled, deleteForEveryoneEnabled, canModerate, moderateEnabled, forwardEnabled, allButtonsDisabled)
}
}
@@ -102,6 +115,7 @@ private fun recheckItems(chatInfo: ChatInfo,
deleteForEveryoneEnabled: MutableState<Boolean>,
canModerate: MutableState<Boolean>,
moderateEnabled: MutableState<Boolean>,
forwardEnabled: MutableState<Boolean>,
allButtonsDisabled: MutableState<Boolean>
) {
val count = selectedChatItems.value?.size ?: 0
@@ -112,6 +126,7 @@ private fun recheckItems(chatInfo: ChatInfo,
var rDeleteForEveryoneEnabled = true
var rModerateEnabled = true
var rOnlyOwnGroupItems = true
var rForwardEnabled = true
val rSelectedChatItems = mutableSetOf<Long>()
for (ci in chatItems) {
if (selected.contains(ci.id)) {
@@ -119,6 +134,7 @@ private fun recheckItems(chatInfo: ChatInfo,
rDeleteForEveryoneEnabled = rDeleteForEveryoneEnabled && ci.meta.deletable && !ci.localNote
rOnlyOwnGroupItems = rOnlyOwnGroupItems && ci.chatDir is CIDirection.GroupSnd
rModerateEnabled = rModerateEnabled && ci.content.msgContent != null && ci.memberToModerate(chatInfo) != null
rForwardEnabled = rForwardEnabled && ci.content.msgContent != null && ci.meta.itemDeleted == null && !ci.isLiveDummy
rSelectedChatItems.add(ci.id) // we are collecting new selected items here to account for any changes in chat items list
}
}
@@ -126,6 +142,7 @@ private fun recheckItems(chatInfo: ChatInfo,
deleteEnabled.value = rDeleteEnabled
deleteForEveryoneEnabled.value = rDeleteForEveryoneEnabled
moderateEnabled.value = rModerateEnabled
forwardEnabled.value = rForwardEnabled
selectedChatItems.value = rSelectedChatItems
}

View File

@@ -665,9 +665,8 @@ private fun updateMemberRoleDialog(
fun connectViaMemberAddressAlert(rhId: Long?, connReqUri: String) {
try {
val uri = URI(connReqUri)
withBGApi {
planAndConnect(rhId, uri, incognito = null, close = { ModalManager.closeAllModalsEverywhere() })
planAndConnect(rhId, connReqUri, incognito = null, close = { ModalManager.closeAllModalsEverywhere() })
}
} catch (e: RuntimeException) {
AlertManager.shared.showAlertMsg(

View File

@@ -478,7 +478,7 @@ private fun ToggleFilterEnabledButton() {
@Composable
expect fun ActiveCallInteractiveArea(call: Call)
fun connectIfOpenedViaUri(rhId: Long?, uri: URI, chatModel: ChatModel) {
fun connectIfOpenedViaUri(rhId: Long?, uri: String, chatModel: ChatModel) {
Log.d(TAG, "connectIfOpenedViaUri: opened via link")
if (chatModel.currentUser.value == null) {
chatModel.appOpenUrl.value = rhId to uri
@@ -566,7 +566,7 @@ private fun connect(link: String, searchChatFilteredBySimplexLink: MutableState<
withBGApi {
planAndConnect(
chatModel.remoteHostId(),
URI.create(link),
link,
incognito = null,
filterKnownContact = { searchChatFilteredBySimplexLink.value = it.id },
filterKnownGroup = { searchChatFilteredBySimplexLink.value = it.id },

View File

@@ -58,11 +58,13 @@ fun ShareListView(chatModel: ChatModel, stopped: Boolean) {
hasSimplexLink = hasSimplexLink(sharedContent.text)
}
is SharedContent.Forward -> {
val mc = sharedContent.chatItem.content.msgContent
if (mc != null) {
isMediaOrFileAttachment = mc.isMediaOrFileAttachment
isVoice = mc.isVoice
hasSimplexLink = hasSimplexLink(mc.text)
sharedContent.chatItems.forEach { ci ->
val mc = ci.content.msgContent
if (mc != null) {
isMediaOrFileAttachment = isMediaOrFileAttachment || mc.isMediaOrFileAttachment
isVoice = isVoice || mc.isVoice
hasSimplexLink = hasSimplexLink || hasSimplexLink(mc.text)
}
}
}
null -> {}
@@ -175,11 +177,11 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal
title = {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
when (chatModel.sharedContent.value) {
when (val v = chatModel.sharedContent.value) {
is SharedContent.Text -> stringResource(MR.strings.share_message)
is SharedContent.Media -> stringResource(MR.strings.share_image)
is SharedContent.File -> stringResource(MR.strings.share_file)
is SharedContent.Forward -> stringResource(MR.strings.forward_message)
is SharedContent.Forward -> if (v.chatItems.size > 1) stringResource(MR.strings.forward_multiple) else stringResource(MR.strings.forward_message)
null -> stringResource(MR.strings.share_message)
},
color = MaterialTheme.colors.onBackground,

View File

@@ -2,8 +2,7 @@
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 chat.simplex.common.model.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.*
@@ -15,7 +14,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()
data class Forward(val chatItems: List<ChatItem>, val fromChatInfo: ChatInfo): SharedContent()
}
enum class AnimatedViewState {

View File

@@ -480,12 +480,11 @@ inline fun <reified T> serializableSaver(): Saver<T, *> = Saver(
)
fun UriHandler.openVerifiedSimplexUri(uri: String) {
val URI = try { URI.create(uri) } catch (e: Exception) { null }
if (URI != null) {
connectIfOpenedViaUri(chatModel.remoteHostId(), URI, ChatModel)
}
connectIfOpenedViaUri(chatModel.remoteHostId(), uri, ChatModel)
}
fun uriCreateOrNull(uri: String) = try { URI.create(uri) } catch (e: Exception) { null }
fun UriHandler.openUriCatching(uri: String) {
try {
openUri(uri)

View File

@@ -20,7 +20,7 @@ enum class ConnectionLinkType {
suspend fun planAndConnect(
rhId: Long?,
uri: URI,
uri: String,
incognito: Boolean?,
close: (() -> Unit)?,
cleanup: (() -> Unit)? = null,
@@ -29,7 +29,7 @@ suspend fun planAndConnect(
) {
val connectionPlan = chatModel.controller.apiConnectPlan(rhId, uri.toString())
if (connectionPlan != null) {
val link = strHasSingleSimplexLink(uri.toString().trim())
val link = strHasSingleSimplexLink(uri.trim())
val linkText = if (link?.format is Format.SimplexLink)
"<br><br><u>${link.simplexLinkText(link.format.linkType, link.format.smpHosts)}</u>"
else
@@ -323,13 +323,13 @@ suspend fun planAndConnect(
suspend fun connectViaUri(
chatModel: ChatModel,
rhId: Long?,
uri: URI,
uri: String,
incognito: Boolean,
connectionPlan: ConnectionPlan?,
close: (() -> Unit)?,
cleanup: (() -> Unit)?,
) {
val pcc = chatModel.controller.apiConnect(rhId, incognito, uri.toString())
val pcc = chatModel.controller.apiConnect(rhId, incognito, uri)
val connLinkType = if (connectionPlan != null) planToConnectionLinkType(connectionPlan) else ConnectionLinkType.INVITATION
if (pcc != null) {
withChats {
@@ -361,7 +361,7 @@ fun planToConnectionLinkType(connectionPlan: ConnectionPlan): ConnectionLinkType
fun askCurrentOrIncognitoProfileAlert(
chatModel: ChatModel,
rhId: Long?,
uri: URI,
uri: String,
connectionPlan: ConnectionPlan?,
close: (() -> Unit)?,
title: String,
@@ -417,7 +417,7 @@ fun openKnownContact(chatModel: ChatModel, rhId: Long?, close: (() -> Unit)?, co
fun ownGroupLinkConfirmConnect(
chatModel: ChatModel,
rhId: Long?,
uri: URI,
uri: String,
linkText: String,
incognito: Boolean?,
connectionPlan: ConnectionPlan?,

View File

@@ -482,7 +482,7 @@ private fun connect(link: String, searchChatFilteredBySimplexLink: MutableState<
withBGApi {
planAndConnect(
chatModel.remoteHostId(),
URI.create(link),
link,
incognito = null,
filterKnownContact = { searchChatFilteredBySimplexLink.value = it.id },
close = close,

View File

@@ -68,7 +68,7 @@ fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRC
* Otherwise, it will be called here AFTER [AddContactLearnMore] is launched and will clear the value too soon.
* It will be dropped automatically when connection established or when user goes away from this screen.
**/
if (chatModel.showingInvitation.value != null && ModalManager.start.openModalCount() == 1) {
if (chatModel.showingInvitation.value != null && ModalManager.start.openModalCount() <= 1) {
val conn = contactConnection.value
if (chatModel.showingInvitation.value?.connChatUsed == false && conn != null) {
AlertManager.shared.showAlertDialog(
@@ -308,6 +308,7 @@ fun ActiveProfilePicker(
switchingProfile.value = true
withApi {
try {
appPreferences.incognito.set(false)
var updatedConn: PendingContactConnection? = null;
if (contactConnection != null) {
@@ -361,6 +362,7 @@ fun ActiveProfilePicker(
switchingProfile.value = true
withApi {
try {
appPreferences.incognito.set(true)
val conn = controller.apiSetConnectionIncognito(rhId, contactConnection.pccConnId, true)
if (conn != null) {
withChats {
@@ -653,7 +655,7 @@ private suspend fun verify(rhId: Long?, text: String?, close: () -> Unit): Boole
private suspend fun connect(rhId: Long?, link: String, close: () -> Unit, cleanup: (() -> Unit)? = null) {
planAndConnect(
rhId,
URI.create(link),
link,
close = close,
cleanup = cleanup,
incognito = null

View File

@@ -125,6 +125,7 @@
<string name="proxy_destination_error_broker_version">Destination server version of %1$s is incompatible with forwarding server %2$s.</string>
<string name="please_try_later">Please try later.</string>
<string name="error_sending_message">Error sending message</string>
<string name="error_forwarding_messages">Error forwarding messages</string>
<string name="error_creating_message">Error creating message</string>
<string name="error_loading_details">Error loading details</string>
<string name="error_adding_members">Error adding member(s)</string>
@@ -133,7 +134,9 @@
<string name="sender_cancelled_file_transfer">Sender cancelled file transfer.</string>
<string name="file_not_approved_title">Unknown servers!</string>
<string name="file_not_approved_descr">Without Tor or VPN, your IP address will be visible to these XFTP relays:\n%1$s.</string>
<string name="n_other_file_errors">%1$d other file error(s).</string>
<string name="error_receiving_file">Error receiving file</string>
<string name="n_file_errors">%1$d file error(s):\n%2$s</string>
<string name="error_creating_address">Error creating address</string>
<string name="contact_already_exists">Contact already exists</string>
<string name="you_are_already_connected_to_vName_via_this_link">You are already connected to %1$s.</string>
@@ -378,12 +381,23 @@
<string name="no_selected_chat">No selected chat</string>
<string name="selected_chat_items_nothing_selected">Nothing selected</string>
<string name="selected_chat_items_selected_n">Selected %d</string>
<string name="forward_alert_title_messages_to_forward">Forward %1$s message(s)?</string>
<string name="forward_alert_title_nothing_to_forward">Nothing to forward!</string>
<string name="forward_alert_forward_messages_without_files">Forward messages without files?</string>
<string name="forward_files_messages_deleted_after_selection_desc">Messages were deleted after you selected them.</string>
<string name="forward_files_not_accepted_desc">%1$d file(s) were not downloaded.</string>
<string name="forward_files_in_progress_desc">%1$d file(s) are still being downloaded.</string>
<string name="forward_files_failed_to_receive_desc">%1$d file(s) failed to download.</string>
<string name="forward_files_missing_desc">%1$d file(s) were deleted.</string>
<string name="forward_files_not_accepted_receive_files">Download</string>
<string name="forward_files_messages_deleted_after_selection_title">%1$s messages not forwarded</string>
<!-- ShareListView.kt -->
<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>
<string name="forward_multiple">Forward messages…</string>
<string name="cannot_share_message_alert_title">Cannot send message</string>
<string name="cannot_share_message_alert_text">Selected chat preferences prohibit this message.</string>
@@ -405,6 +419,8 @@
<string name="files_and_media_prohibited">Files and media prohibited!</string>
<string name="only_owners_can_enable_files_and_media">Only group owners can enable files and media.</string>
<string name="compose_send_direct_message_to_connect">Send direct message to connect</string>
<string name="compose_forward_messages_n">Forwarding %1$s messages</string>
<string name="compose_save_messages_n">Saving %1$s messages</string>
<string name="simplex_links_not_allowed">SimpleX links not allowed</string>
<string name="files_and_media_not_allowed">Files and media not allowed</string>
<string name="voice_messages_not_allowed">Voice messages not allowed</string>

View File

@@ -34,6 +34,9 @@ var useWorker = false;
var isDesktop = false;
var localizedState = "";
var localizedDescription = "";
// When one side of a call sends candidates tot fast (until local & remote descriptions are set), that candidates
// will be stored here and then set when the call will be ready to process them
let afterCallInitializedCandidates = [];
const processCommand = (function () {
const defaultIceServers = [
{ urls: ["stuns:stun.simplex.im:443"] },
@@ -234,6 +237,8 @@ const processCommand = (function () {
const pc = activeCall.connection;
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
addIceCandidates(pc, afterCallInitializedCandidates);
afterCallInitializedCandidates = [];
// for debugging, returning the command for callee to use
// resp = {
// type: "offer",
@@ -272,6 +277,8 @@ const processCommand = (function () {
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
addIceCandidates(pc, remoteIceCandidates);
addIceCandidates(pc, afterCallInitializedCandidates);
afterCallInitializedCandidates = [];
// same as command for caller to use
resp = {
type: "answer",
@@ -297,17 +304,20 @@ const processCommand = (function () {
// console.log("answer remoteIceCandidates", JSON.stringify(remoteIceCandidates))
await pc.setRemoteDescription(new RTCSessionDescription(answer));
addIceCandidates(pc, remoteIceCandidates);
addIceCandidates(pc, afterCallInitializedCandidates);
afterCallInitializedCandidates = [];
resp = { type: "ok" };
}
break;
case "ice":
const remoteIceCandidates = parse(command.iceCandidates);
if (pc) {
const remoteIceCandidates = parse(command.iceCandidates);
addIceCandidates(pc, remoteIceCandidates);
resp = { type: "ok" };
}
else {
resp = { type: "error", message: "ice: call not started" };
afterCallInitializedCandidates.push(...remoteIceCandidates);
resp = { type: "error", message: "ice: call not started yet, will add candidates later" };
}
break;
case "media":

View File

@@ -204,7 +204,7 @@ fun startServer(onResponse: (WVAPIMessage) -> Unit): NanoWSD {
return when {
session.headers["upgrade"] == "websocket" -> super.handle(session)
session.uri.contains("/simplex/call/") -> resourcesToResponse("/desktop/call.html")
else -> resourcesToResponse(URI.create(session.uri).path)
else -> resourcesToResponse(uriCreateOrNull(session.uri)?.path ?: return newFixedLengthResponse("Error parsing URL"))
}
}
}