From ba203faad42a8da4bb9ba8fd8217eec428243000 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Mon, 20 May 2024 17:58:30 +0400 Subject: [PATCH] android: check known relays before file reception, support user approval of unknown relays (#4196) --- .../chat/simplex/common/model/ChatModel.kt | 4 ++ .../chat/simplex/common/model/SimpleXAPI.kt | 48 +++++++++++++++---- .../common/views/chat/item/CIFileView.kt | 5 +- .../common/views/chat/item/CIImageView.kt | 10 ++-- .../common/views/chat/item/CIVIdeoView.kt | 5 +- .../common/views/chat/item/CIVoiceView.kt | 8 +++- .../common/views/chat/item/FramedItemView.kt | 2 +- .../views/usersettings/PrivacySettings.kt | 23 +++++++-- .../commonMain/resources/MR/base/strings.xml | 6 +++ 9 files changed, 84 insertions(+), 27 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 3780066092..56487874ba 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -2644,6 +2644,7 @@ data class CIFile( is CIFileStatus.RcvInvitation -> false is CIFileStatus.RcvAccepted -> false is CIFileStatus.RcvTransfer -> false + is CIFileStatus.RcvAborted -> false is CIFileStatus.RcvCancelled -> false is CIFileStatus.RcvComplete -> true is CIFileStatus.RcvError -> false @@ -2665,6 +2666,7 @@ data class CIFile( is CIFileStatus.RcvInvitation -> null is CIFileStatus.RcvAccepted -> rcvCancelAction is CIFileStatus.RcvTransfer -> rcvCancelAction + is CIFileStatus.RcvAborted -> null is CIFileStatus.RcvCancelled -> null is CIFileStatus.RcvComplete -> null is CIFileStatus.RcvError -> null @@ -2845,6 +2847,7 @@ sealed class CIFileStatus { @Serializable @SerialName("rcvInvitation") object RcvInvitation: CIFileStatus() @Serializable @SerialName("rcvAccepted") object RcvAccepted: CIFileStatus() @Serializable @SerialName("rcvTransfer") class RcvTransfer(val rcvProgress: Long, val rcvTotal: Long): CIFileStatus() + @Serializable @SerialName("rcvAborted") object RcvAborted: CIFileStatus() @Serializable @SerialName("rcvComplete") object RcvComplete: CIFileStatus() @Serializable @SerialName("rcvCancelled") object RcvCancelled: CIFileStatus() @Serializable @SerialName("rcvError") object RcvError: CIFileStatus() @@ -2859,6 +2862,7 @@ sealed class CIFileStatus { is RcvInvitation -> false is RcvAccepted -> false is RcvTransfer -> false + is RcvAborted -> false is RcvComplete -> false is RcvCancelled -> false is RcvError -> false diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index fa9c2580ee..ca237cd52f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -12,6 +12,7 @@ import dev.icerock.moko.resources.compose.painterResource import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.call.* +import chat.simplex.common.views.chat.group.toggleShowMemberMessages import chat.simplex.common.views.migration.MigrationFileLinkData import chat.simplex.common.views.onboarding.OnboardingStage import chat.simplex.common.views.usersettings.* @@ -106,6 +107,7 @@ class AppPreferences { val privacySaveLastDraft = mkBoolPreference(SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT, true) val privacyDeliveryReceiptsSet = mkBoolPreference(SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET, false) val privacyEncryptLocalFiles = mkBoolPreference(SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES, true) + val privacyAskToApproveRelays = mkBoolPreference(SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS, true) val experimentalCalls = mkBoolPreference(SHARED_PREFS_EXPERIMENTAL_CALLS, false) val showUnreadAndFavorites = mkBoolPreference(SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES, false) val chatArchiveName = mkStrPreference(SHARED_PREFS_CHAT_ARCHIVE_NAME, null) @@ -292,6 +294,7 @@ class AppPreferences { private const val SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT = "PrivacySaveLastDraft" private const val SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET = "PrivacyDeliveryReceiptsSet" private const val SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES = "PrivacyEncryptLocalFiles" + private const val SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS = "PrivacyAskToApproveRelays" const val SHARED_PREFS_PRIVACY_FULL_BACKUP = "FullBackup" private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls" private const val SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES = "ShowUnreadAndFavorites" @@ -1337,9 +1340,9 @@ object ChatController { } } - suspend fun apiReceiveFile(rh: Long?, fileId: Long, encrypted: Boolean, inline: Boolean? = null, auto: Boolean = false): AChatItem? { + 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, encrypted, inline)) + val r = sendCmd(rh, CC.ReceiveFile(fileId, userApprovedRelays = userApprovedRelays, encrypt = encrypted, inline = inline)) return when (r) { is CR.RcvFileAccepted -> r.chatItem is CR.RcvFileAcceptedSndCancelled -> { @@ -1358,7 +1361,23 @@ object ChatController { val maybeChatError = chatError(r) if (maybeChatError is ChatErrorType.FileCancelled || maybeChatError is ChatErrorType.FileAlreadyReceiving) { Log.d(TAG, "apiReceiveFile ignoring FileCancelled or FileAlreadyReceiving error") - } else { + } 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) } } @@ -2216,9 +2235,14 @@ object ChatController { } } - suspend fun receiveFile(rhId: Long?, user: UserLike, fileId: Long, auto: Boolean = false) { - val encrypted = appPrefs.privacyEncryptLocalFiles.get() - val chatItem = apiReceiveFile(rhId, fileId, encrypted = encrypted, auto = auto) + 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) } @@ -2501,7 +2525,7 @@ sealed class CC { class ApiRejectContact(val contactReqId: Long): CC() class ApiChatRead(val type: ChatType, val id: Long, val range: ItemRange): CC() class ApiChatUnread(val type: ChatType, val id: Long, val unreadChat: Boolean): CC() - class ReceiveFile(val fileId: Long, val encrypt: Boolean, val inline: Boolean?): CC() + class ReceiveFile(val fileId: Long, val userApprovedRelays: Boolean, val encrypt: Boolean, val inline: Boolean?): CC() class CancelFile(val fileId: Long): CC() // Remote control class SetLocalDeviceName(val displayName: String): CC() @@ -2652,6 +2676,7 @@ sealed class CC { is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}" is ReceiveFile -> "/freceive $fileId" + + (" approved_relays=${onOff(userApprovedRelays)}") + (if (encrypt == null) "" else " encrypt=${onOff(encrypt)}") + (if (inline == null) "" else " inline=${onOff(inline)}") is CancelFile -> "/fcancel $fileId" @@ -4892,13 +4917,14 @@ sealed class ChatErrorType { is FileCancel -> "fileCancel" is FileAlreadyExists -> "fileAlreadyExists" is FileRead -> "fileRead" - is FileWrite -> "fileWrite" + is FileWrite -> "fileWrite $message" is FileSend -> "fileSend" is FileRcvChunk -> "fileRcvChunk" is FileInternal -> "fileInternal" is FileImageType -> "fileImageType" is FileImageSize -> "fileImageSize" is FileNotReceived -> "fileNotReceived" + is FileNotApproved -> "fileNotApproved" // is XFTPRcvFile -> "xftpRcvFile" // is XFTPSndFile -> "xftpSndFile" is FallbackToSMPProhibited -> "fallbackToSMPProhibited" @@ -4978,6 +5004,7 @@ sealed class ChatErrorType { @Serializable @SerialName("fileImageType") class FileImageType(val filePath: String): ChatErrorType() @Serializable @SerialName("fileImageSize") class FileImageSize(val filePath: String): ChatErrorType() @Serializable @SerialName("fileNotReceived") class FileNotReceived(val fileId: Long): ChatErrorType() + @Serializable @SerialName("fileNotApproved") class FileNotApproved(val fileId: Long, val unknownServers: List): ChatErrorType() // @Serializable @SerialName("xFTPRcvFile") object XFTPRcvFile: ChatErrorType() // @Serializable @SerialName("xFTPSndFile") object XFTPSndFile: ChatErrorType() @Serializable @SerialName("fallbackToSMPProhibited") class FallbackToSMPProhibited(val fileId: Long): ChatErrorType() @@ -5476,6 +5503,7 @@ enum class NotificationsMode() { data class AppSettings( var networkConfig: NetCfg? = null, var privacyEncryptLocalFiles: Boolean? = null, + var privacyAskToApproveRelays: Boolean? = null, var privacyAcceptImages: Boolean? = null, var privacyLinkPreviews: Boolean? = null, var privacyShowChatPreviews: Boolean? = null, @@ -5499,6 +5527,7 @@ data class AppSettings( val def = defaults if (networkConfig != def.networkConfig) { empty.networkConfig = networkConfig } if (privacyEncryptLocalFiles != def.privacyEncryptLocalFiles) { empty.privacyEncryptLocalFiles = privacyEncryptLocalFiles } + if (privacyAskToApproveRelays != def.privacyAskToApproveRelays) { empty.privacyAskToApproveRelays = privacyAskToApproveRelays } if (privacyAcceptImages != def.privacyAcceptImages) { empty.privacyAcceptImages = privacyAcceptImages } if (privacyLinkPreviews != def.privacyLinkPreviews) { empty.privacyLinkPreviews = privacyLinkPreviews } if (privacyShowChatPreviews != def.privacyShowChatPreviews) { empty.privacyShowChatPreviews = privacyShowChatPreviews } @@ -5530,6 +5559,7 @@ data class AppSettings( setNetCfg(net) } privacyEncryptLocalFiles?.let { def.privacyEncryptLocalFiles.set(it) } + privacyAskToApproveRelays?.let { def.privacyAskToApproveRelays.set(it) } privacyAcceptImages?.let { def.privacyAcceptImages.set(it) } privacyLinkPreviews?.let { def.privacyLinkPreviews.set(it) } privacyShowChatPreviews?.let { def.privacyShowChatPreviews.set(it) } @@ -5554,6 +5584,7 @@ data class AppSettings( get() = AppSettings( networkConfig = NetCfg.defaults, privacyEncryptLocalFiles = true, + privacyAskToApproveRelays = true, privacyAcceptImages = true, privacyLinkPreviews = true, privacyShowChatPreviews = true, @@ -5579,6 +5610,7 @@ data class AppSettings( return defaults.copy( networkConfig = getNetCfg(), privacyEncryptLocalFiles = def.privacyEncryptLocalFiles.get(), + privacyAskToApproveRelays = def.privacyAskToApproveRelays.get(), privacyAcceptImages = def.privacyAcceptImages.get(), privacyLinkPreviews = def.privacyLinkPreviews.get(), privacyShowChatPreviews = def.privacyShowChatPreviews.get(), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt index f079a152ab..a48bb2bb12 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt @@ -1,6 +1,5 @@ package chat.simplex.common.views.chat.item -import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CornerSize @@ -64,7 +63,7 @@ fun CIFileView( fun fileAction() { if (file != null) { when { - file.fileStatus is CIFileStatus.RcvInvitation -> { + file.fileStatus is CIFileStatus.RcvInvitation || file.fileStatus is CIFileStatus.RcvAborted -> { if (fileSizeValid(file)) { receiveFile(file.fileId) } else { @@ -176,6 +175,8 @@ fun CIFileView( } else { progressIndicator() } + is CIFileStatus.RcvAborted -> + fileIcon(innerIcon = painterResource(MR.images.ic_sync_problem), color = MaterialTheme.colors.primary) is CIFileStatus.RcvComplete -> fileIcon() is CIFileStatus.RcvCancelled -> fileIcon(innerIcon = painterResource(MR.images.ic_close)) is CIFileStatus.RcvError -> fileIcon(innerIcon = painterResource(MR.images.ic_close)) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt index 65fb38575d..7cffe4564b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt @@ -21,17 +21,12 @@ import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.DEFAULT_MAX_IMAGE_WIDTH import chat.simplex.res.MR import dev.icerock.moko.resources.StringResource -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.runBlocking -import java.io.File -import java.net.URI @Composable fun CIImageView( image: String, file: CIFile?, - metaColor: Color, imageProvider: () -> ImageGalleryProvider, showMenu: MutableState, receiveFile: (Long) -> Unit @@ -51,7 +46,7 @@ fun CIImageView( icon, stringResource(stringId), Modifier.fillMaxSize(), - tint = metaColor + tint = Color.White ) } @@ -78,6 +73,7 @@ fun CIImageView( is CIFileStatus.RcvInvitation -> fileIcon(painterResource(MR.images.ic_arrow_downward), MR.strings.icon_descr_asked_to_receive) is CIFileStatus.RcvAccepted -> fileIcon(painterResource(MR.images.ic_more_horiz), MR.strings.icon_descr_waiting_for_image) is CIFileStatus.RcvTransfer -> progressIndicator() + is CIFileStatus.RcvAborted -> fileIcon(painterResource(MR.images.ic_sync_problem), MR.strings.icon_descr_file) is CIFileStatus.RcvCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) is CIFileStatus.RcvError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) is CIFileStatus.Invalid -> fileIcon(painterResource(MR.images.ic_question_mark), MR.strings.icon_descr_file) @@ -206,7 +202,7 @@ fun CIImageView( imageView(base64ToBitmap(image), onClick = { if (file != null) { when (file.fileStatus) { - CIFileStatus.RcvInvitation -> + CIFileStatus.RcvInvitation, CIFileStatus.RcvAborted -> if (fileSizeValid()) { receiveFile(file.fileId) } else { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVIdeoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVIdeoView.kt index a79e509d02..749816f918 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVIdeoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVIdeoView.kt @@ -73,7 +73,7 @@ fun CIVideoView( VideoPreviewImageView(preview, onClick = { if (file != null) { when (file.fileStatus) { - CIFileStatus.RcvInvitation -> + CIFileStatus.RcvInvitation, CIFileStatus.RcvAborted -> receiveFileIfValidSize(file, receiveFile) CIFileStatus.RcvAccepted -> when (file.fileProtocol) { @@ -102,7 +102,7 @@ fun CIVideoView( if (file != null) { DurationProgress(file, remember { mutableStateOf(false) }, remember { mutableStateOf(duration * 1000L) }, remember { mutableStateOf(0L) }/*, soundEnabled*/) } - if (file?.fileStatus is CIFileStatus.RcvInvitation) { + if (file?.fileStatus is CIFileStatus.RcvInvitation || file?.fileStatus is CIFileStatus.RcvAborted) { PlayButton(error = false, { showMenu.value = true }) { receiveFileIfValidSize(file, receiveFile) } } } @@ -396,6 +396,7 @@ private fun loadingIndicator(file: CIFile?) { } else { progressIndicator() } + is CIFileStatus.RcvAborted -> fileIcon(painterResource(MR.images.ic_sync_problem), MR.strings.icon_descr_file) is CIFileStatus.RcvCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) is CIFileStatus.RcvError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) is CIFileStatus.Invalid -> fileIcon(painterResource(MR.images.ic_question_mark), MR.strings.icon_descr_file) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt index cac89b2587..f973a6ea66 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt @@ -22,6 +22,7 @@ import chat.simplex.common.views.helpers.* import chat.simplex.common.model.* import chat.simplex.common.platform.* import chat.simplex.res.MR +import dev.icerock.moko.resources.ImageResource import kotlinx.coroutines.flow.* // TODO refactor https://github.com/simplex-chat/simplex-chat/pull/1451#discussion_r1033429901 @@ -220,7 +221,8 @@ private fun PlayPauseButton( error: Boolean, play: () -> Unit, pause: () -> Unit, - longClick: () -> Unit + longClick: () -> Unit, + icon: ImageResource = MR.images.ic_play_arrow_filled, ) { val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage @@ -241,7 +243,7 @@ private fun PlayPauseButton( contentAlignment = Alignment.Center ) { Icon( - if (audioPlaying) painterResource(MR.images.ic_pause_filled) else painterResource(MR.images.ic_play_arrow_filled), + if (audioPlaying) painterResource(MR.images.ic_pause_filled) else painterResource(icon), contentDescription = null, Modifier.size(36.dp), tint = if (error) WarningOrange else if (!enabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary @@ -294,6 +296,8 @@ private fun VoiceMsgIndicator( ) { ProgressIndicator() } + } else if (file?.fileStatus is CIFileStatus.RcvAborted) { + PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, { receiveFile(file.fileId) }, {}, longClick = longClick, icon = MR.images.ic_sync_problem) } else { PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, false, false, {}, {}, longClick) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt index aec314d2c1..4dc0fd9cf5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt @@ -241,7 +241,7 @@ fun FramedItemView( } else { when (val mc = ci.content.msgContent) { is MsgContent.MCImage -> { - CIImageView(image = mc.image, file = ci.file, metaColor = metaColor, imageProvider ?: return@PriorityLayout, showMenu, receiveFile) + CIImageView(image = mc.image, file = ci.file, imageProvider ?: return@PriorityLayout, showMenu, receiveFile) if (mc.text == "" && !ci.meta.isLive) { metaColor = Color.White } else { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt index b376285259..dc0760193d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt @@ -1,6 +1,7 @@ package chat.simplex.common.views.usersettings import SectionBottomSpacer +import SectionCustomFooter import SectionDividerSpaced import SectionItemView import SectionTextFooter @@ -63,10 +64,6 @@ fun PrivacySettingsView( SectionDividerSpaced() SectionView(stringResource(MR.strings.settings_section_title_chats)) { - SettingsPreferenceItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.encrypt_local_files), chatModel.controller.appPrefs.privacyEncryptLocalFiles, onChange = { enable -> - withBGApi { chatModel.controller.apiSetEncryptLocalFiles(enable) } - }) - SettingsPreferenceItem(painterResource(MR.images.ic_image), stringResource(MR.strings.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages) SettingsPreferenceItem(painterResource(MR.images.ic_travel_explore), stringResource(MR.strings.send_link_previews), chatModel.controller.appPrefs.privacyLinkPreviews) SettingsPreferenceItem( painterResource(MR.images.ic_chat_bubble), @@ -91,6 +88,22 @@ fun PrivacySettingsView( chatModel.simplexLinkMode.value = it }) } + SectionDividerSpaced() + + SectionView(stringResource(MR.strings.settings_section_title_files)) { + SettingsPreferenceItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.encrypt_local_files), chatModel.controller.appPrefs.privacyEncryptLocalFiles, onChange = { enable -> + withBGApi { chatModel.controller.apiSetEncryptLocalFiles(enable) } + }) + SettingsPreferenceItem(painterResource(MR.images.ic_image), stringResource(MR.strings.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages) + SettingsPreferenceItem(painterResource(MR.images.ic_security), stringResource(MR.strings.protect_ip_address), chatModel.controller.appPrefs.privacyAskToApproveRelays) + } + SectionCustomFooter { + if (chatModel.controller.appPrefs.privacyAskToApproveRelays.state.value) { + Text(stringResource(MR.strings.app_will_ask_to_confirm_unknown_file_servers)) + } else { + Text(stringResource(MR.strings.without_tor_or_vpn_ip_address_will_be_visible_to_file_servers)) + } + } val currentUser = chatModel.currentUser.value if (currentUser != null) { @@ -141,7 +154,7 @@ fun PrivacySettingsView( } if (!chatModel.desktopNoUserNoRemote) { - SectionDividerSpaced() + SectionDividerSpaced(maxTopPadding = true) DeliveryReceiptsSection( currentUser = currentUser, setOrAskSendReceiptsContacts = { enable -> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 93e6a902f4..9308f5886e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -119,6 +119,8 @@ Error joining group Cannot receive file Sender cancelled file transfer. + Unknown servers! + Without Tor or VPN, your IP address will be visible to these XFTP relays:\n%1$s. Error receiving file Error creating address Contact already exists @@ -992,6 +994,9 @@ Protect app screen Encrypt local files Auto-accept images + Protect IP address + The app will ask to confirm downloads from unknown file servers (except .onion or when SOCKS proxy is enabled). + Without Tor or VPN, your IP address will be visible to file servers. Send link previews Show last messages Message draft @@ -1056,6 +1061,7 @@ APP DEVICE CHATS + FILES SEND DELIVERY RECEIPTS TO Restart Shutdown