From 96ce59f33071b5afb7feeacd1d8ed4049a13d73a Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Wed, 15 May 2024 23:09:51 +0400 Subject: [PATCH] multiplatform: SMP proxy configuration buttons (#4188) * multiplatform: SMP proxy configuration buttons * fix * icons * icon * icon --- .../UserSettings/NetworkAndServers.swift | 6 +- .../views/usersettings/NetworkAndServers.kt | 153 +++++++++++++++++- .../commonMain/resources/MR/base/strings.xml | 21 +++ .../MR/images/ic_arrows_left_right.svg | 4 + 4 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrows_left_right.svg diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift index 01cb6ad2d3..6d849479e5 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift @@ -261,9 +261,9 @@ struct NetworkAndServers: View { private func proxyFallbackInfo(_ proxyFallback: SMPProxyFallback) -> LocalizedStringKey { switch proxyFallback { - case .allow: return "Send messages directly when your or destination server does not support 2-hop onion routing." - case .allowProtected: return "Send messages directly when IP address is protected and your or destination server does not support 2-hop onion routing." - case .prohibit: return "Do NOT send messages directly, even if your or destination server does not support 2-hop onion routing." + case .allow: return "Send messages directly when your or destination server does not support private routing." + case .allowProtected: return "Send messages directly when IP address is protected and your or destination server does not support private routing." + case .prohibit: return "Do NOT send messages directly, even if your or destination server does not support private routing." } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt index 4d33040e29..d07fad8623 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt @@ -43,6 +43,8 @@ fun NetworkAndServersView() { val developerTools = chatModel.controller.appPrefs.developerTools.get() val onionHosts = remember { mutableStateOf(netCfg.onionHosts) } val sessionMode = remember { mutableStateOf(netCfg.sessionMode) } + val smpProxyMode = remember { mutableStateOf(netCfg.smpProxyMode) } + val smpProxyFallback = remember { mutableStateOf(netCfg.smpProxyFallback) } val proxyPort = remember { derivedStateOf { chatModel.controller.appPrefs.networkProxyHostPort.state.value?.split(":")?.lastOrNull()?.toIntOrNull() ?: 9050 } } NetworkAndServersLayout( @@ -51,6 +53,8 @@ fun NetworkAndServersView() { networkUseSocksProxy = networkUseSocksProxy, onionHosts = onionHosts, sessionMode = sessionMode, + smpProxyMode = smpProxyMode, + smpProxyFallback = smpProxyFallback, proxyPort = proxyPort, toggleSocksProxy = { enable -> if (enable) { @@ -137,6 +141,59 @@ fun NetworkAndServersView() { } } } + }, + updateSMPProxyMode = { + if (smpProxyMode.value == it) return@NetworkAndServersLayout + val prevValue = smpProxyMode.value + smpProxyMode.value = it + val startsWith = when (it) { + SMPProxyMode.Always -> generalGetString(MR.strings.network_smp_proxy_mode_always_description) + SMPProxyMode.Unknown -> generalGetString(MR.strings.network_smp_proxy_mode_unknown_description) + SMPProxyMode.Unprotected -> generalGetString(MR.strings.network_smp_proxy_mode_unprotected_description) + SMPProxyMode.Never -> generalGetString(MR.strings.network_smp_proxy_mode_never_description) + } + showUpdateNetworkSettingsDialog( + title = generalGetString(MR.strings.update_network_smp_proxy_mode_question), + startsWith, + onDismiss = { smpProxyMode.value = prevValue } + ) { + withBGApi { + val newCfg = chatModel.controller.getNetCfg().copy(smpProxyMode = it) + val res = chatModel.controller.apiSetNetworkConfig(newCfg) + if (res) { + chatModel.controller.setNetCfg(newCfg) + smpProxyMode.value = it + } else { + smpProxyMode.value = prevValue + } + } + } + }, + updateSMPProxyFallback = { + if (smpProxyFallback.value == it) return@NetworkAndServersLayout + val prevValue = smpProxyFallback.value + smpProxyFallback.value = it + val startsWith = when (it) { + SMPProxyFallback.Allow -> generalGetString(MR.strings.network_smp_proxy_fallback_allow_description) + SMPProxyFallback.AllowProtected -> generalGetString(MR.strings.network_smp_proxy_fallback_allow_protected_description) + SMPProxyFallback.Prohibit -> generalGetString(MR.strings.network_smp_proxy_fallback_prohibit_description) + } + showUpdateNetworkSettingsDialog( + title = generalGetString(MR.strings.update_network_smp_proxy_fallback_question), + startsWith, + onDismiss = { smpProxyFallback.value = prevValue } + ) { + withBGApi { + val newCfg = chatModel.controller.getNetCfg().copy(smpProxyFallback = it) + val res = chatModel.controller.apiSetNetworkConfig(newCfg) + if (res) { + chatModel.controller.setNetCfg(newCfg) + smpProxyFallback.value = it + } else { + smpProxyFallback.value = prevValue + } + } + } } ) } @@ -147,16 +204,22 @@ fun NetworkAndServersView() { networkUseSocksProxy: MutableState, onionHosts: MutableState, sessionMode: MutableState, + smpProxyMode: MutableState, + smpProxyFallback: MutableState, proxyPort: State, toggleSocksProxy: (Boolean) -> Unit, useOnion: (OnionHosts) -> Unit, updateSessionMode: (TransportSessionMode) -> Unit, + updateSMPProxyMode: (SMPProxyMode) -> Unit, + updateSMPProxyFallback: (SMPProxyFallback) -> Unit, ) { val m = chatModel ColumnWithScrollBar( Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp) ) { + val showModal = { it: @Composable ModalData.() -> Unit -> ModalManager.start.showModal(content = it) } + AppBarTitle(stringResource(MR.strings.network_and_servers)) if (!chatModel.desktopNoUserNoRemote) { SectionView(generalGetString(MR.strings.settings_section_title_messages)) { @@ -165,7 +228,6 @@ fun NetworkAndServersView() { SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.xftp_servers), { ModalManager.start.showCustomModal { close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.XFTP, close) } }) if (currentRemoteHost == null) { - val showModal = { it: @Composable ModalData.() -> Unit -> ModalManager.start.showModal(content = it) } UseSocksProxySwitch(networkUseSocksProxy, proxyPort, toggleSocksProxy, showModal, chatModel.controller.appPrefs.networkProxyHostPort, false) UseOnionHosts(onionHosts, networkUseSocksProxy, showModal, useOnion) if (developerTools) { @@ -188,6 +250,18 @@ fun NetworkAndServersView() { Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 24.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp)) } + if (currentRemoteHost == null) { + SectionView(generalGetString(MR.strings.settings_section_title_private_message_routing)) { + SMPProxyModePicker(smpProxyMode, showModal, updateSMPProxyMode) + SMPProxyFallbackPicker(smpProxyFallback, showModal, updateSMPProxyFallback, enabled = remember { mutableStateOf(smpProxyMode.value != SMPProxyMode.Never) }) + SettingsPreferenceItem(painterResource(MR.images.ic_arrow_forward), stringResource(MR.strings.private_routing_show_message_status), chatModel.controller.appPrefs.showSentViaProxy) + } + SectionCustomFooter { + Text(stringResource(MR.strings.private_routing_explanation)) + } + Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 32.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp)) + } + SectionView(generalGetString(MR.strings.settings_section_title_calls)) { SettingsActionItem(painterResource(MR.images.ic_electrical_services), stringResource(MR.strings.webrtc_ice_servers), { ModalManager.start.showModal { RTCServersView(m) } }) } @@ -452,6 +526,79 @@ private fun SessionModePicker( ) } +@Composable +private fun SMPProxyModePicker( + smpProxyMode: MutableState, + showModal: (@Composable ModalData.() -> Unit) -> Unit, + updateSMPProxyMode: (SMPProxyMode) -> Unit, +) { + val density = LocalDensity.current + val values = remember { + SMPProxyMode.values().map { + when (it) { + SMPProxyMode.Always -> ValueTitleDesc(SMPProxyMode.Always, generalGetString(MR.strings.network_smp_proxy_mode_always), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_mode_always_description), density)) + SMPProxyMode.Unknown -> ValueTitleDesc(SMPProxyMode.Unknown, generalGetString(MR.strings.network_smp_proxy_mode_unknown), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_mode_unknown_description), density)) + SMPProxyMode.Unprotected -> ValueTitleDesc(SMPProxyMode.Unprotected, generalGetString(MR.strings.network_smp_proxy_mode_unprotected), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_mode_unprotected_description), density)) + SMPProxyMode.Never -> ValueTitleDesc(SMPProxyMode.Never, generalGetString(MR.strings.network_smp_proxy_mode_never), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_mode_never_description), density)) + } + } + } + + SectionItemWithValue( + generalGetString(MR.strings.network_smp_proxy_mode_private_routing), + smpProxyMode, + values, + icon = painterResource(MR.images.ic_settings_ethernet), + onSelected = { + showModal { + ColumnWithScrollBar( + Modifier.fillMaxWidth(), + ) { + AppBarTitle(stringResource(MR.strings.network_smp_proxy_mode_private_routing)) + SectionViewSelectable(null, smpProxyMode, values, updateSMPProxyMode) + } + } + } + ) +} + +@Composable +private fun SMPProxyFallbackPicker( + smpProxyFallback: MutableState, + showModal: (@Composable ModalData.() -> Unit) -> Unit, + updateSMPProxyFallback: (SMPProxyFallback) -> Unit, + enabled: State, +) { + val density = LocalDensity.current + val values = remember { + SMPProxyFallback.values().map { + when (it) { + SMPProxyFallback.Allow -> ValueTitleDesc(SMPProxyFallback.Allow, generalGetString(MR.strings.network_smp_proxy_fallback_allow), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_fallback_allow_description), density)) + SMPProxyFallback.AllowProtected -> ValueTitleDesc(SMPProxyFallback.AllowProtected, generalGetString(MR.strings.network_smp_proxy_fallback_allow_protected), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_fallback_allow_protected_description), density)) + SMPProxyFallback.Prohibit -> ValueTitleDesc(SMPProxyFallback.Prohibit, generalGetString(MR.strings.network_smp_proxy_fallback_prohibit), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_fallback_prohibit_description), density)) + } + } + } + + SectionItemWithValue( + generalGetString(MR.strings.network_smp_proxy_fallback_allow_downgrade), + smpProxyFallback, + values, + icon = painterResource(MR.images.ic_arrows_left_right), + enabled = enabled, + onSelected = { + showModal { + ColumnWithScrollBar( + Modifier.fillMaxWidth(), + ) { + AppBarTitle(stringResource(MR.strings.network_smp_proxy_fallback_allow_downgrade)) + SectionViewSelectable(null, smpProxyFallback, values, updateSMPProxyFallback) + } + } + } + ) +} + @Composable private fun NetworkSectionFooter(revert: () -> Unit, save: () -> Unit, revertDisabled: Boolean, saveDisabled: Boolean) { Row( @@ -506,8 +653,12 @@ fun PreviewNetworkAndServersLayout() { toggleSocksProxy = {}, onionHosts = remember { mutableStateOf(OnionHosts.PREFER) }, sessionMode = remember { mutableStateOf(TransportSessionMode.User) }, + smpProxyMode = remember { mutableStateOf(SMPProxyMode.Never) }, + smpProxyFallback = remember { mutableStateOf(SMPProxyFallback.Allow) }, useOnion = {}, updateSessionMode = {}, + updateSMPProxyMode = {}, + updateSMPProxyFallback = {}, ) } } 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 83ca79bd3a..93e6a902f4 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -722,6 +722,26 @@ Update transport isolation mode? Use .onion hosts to No if SOCKS proxy does not support them.]]> Please note: message and file relays are connected via SOCKS proxy. Calls and sending link previews use direct connection.]]> + Private routing + Always + Unknown relays + Unprotected + Never + Always use private routing. + Use private routing with unknown servers. + Use private routing with unknown servers when IP address is not protected. + Do NOT use private routing. + Message routing mode + Allow downgrade + Yes + When IP hidden + No + Send messages directly when your or destination server does not support private routing. + Send messages directly when IP address is protected and your or destination server does not support private routing. + Do NOT send messages directly, even if your or destination server does not support private routing. + Message routing fallback + Show message status + To protect your IP address, private routing uses your SMP servers to deliver messages. Appearance Customize theme THEME COLORS @@ -1047,6 +1067,7 @@ THEMES Profile images MESSAGES AND FILES + PRIVATE MESSAGE ROUTING CALLS Network connection Incognito mode diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrows_left_right.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrows_left_right.svg new file mode 100644 index 0000000000..de3a39b826 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrows_left_right.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file