From e1ff7c88d70fdd67ca0e47d3ad3a672ce9c8aa05 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Sat, 2 Dec 2023 04:41:08 +0800 Subject: [PATCH] desktop: allow changing listening ip and port of remote (#3498) * desktop: allow changing listening ip and port of remote * remove empty lines --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../chat/simplex/common/model/SimpleXAPI.kt | 20 ++- .../views/helpers/DefaultBasicTextField.kt | 1 - .../helpers/ExposedDropDownSettingRow.kt | 117 +++++++------ .../simplex/common/views/helpers/Utils.kt | 2 +- .../common/views/remote/ConnectMobileView.kt | 159 ++++++++++++++---- .../views/usersettings/NetworkAndServers.kt | 6 +- 6 files changed, 211 insertions(+), 94 deletions(-) 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 724e6bfcb..6c01aff5d 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 @@ -1426,9 +1426,9 @@ object ChatController { chatModel.remoteHosts.addAll(hosts) } - suspend fun startRemoteHost(rhId: Long?, multicast: Boolean = true): Triple? { - val r = sendCmd(null, CC.StartRemoteHost(rhId, multicast)) - if (r is CR.RemoteHostStarted) return Triple(r.remoteHost_, r.invitation, r.ctrlPort) + suspend fun startRemoteHost(rhId: Long?, multicast: Boolean = true, address: RemoteCtrlAddress?, port: Int?): CR.RemoteHostStarted? { + val r = sendCmd(null, CC.StartRemoteHost(rhId, multicast, address, port)) + if (r is CR.RemoteHostStarted) return r apiErrorAlert("startRemoteHost", generalGetString(MR.strings.error_alert_title), r) return null } @@ -2248,7 +2248,7 @@ sealed class CC { // Remote control class SetLocalDeviceName(val displayName: String): CC() class ListRemoteHosts(): CC() - class StartRemoteHost(val remoteHostId: Long?, val multicast: Boolean): CC() + class StartRemoteHost(val remoteHostId: Long?, val multicast: Boolean, val address: RemoteCtrlAddress?, val port: Int?): CC() class SwitchRemoteHost (val remoteHostId: Long?): CC() class StopRemoteHost(val remoteHostKey: Long?): CC() class DeleteRemoteHost(val remoteHostId: Long): CC() @@ -2384,7 +2384,7 @@ sealed class CC { is CancelFile -> "/fcancel $fileId" is SetLocalDeviceName -> "/set device name $displayName" is ListRemoteHosts -> "/list remote hosts" - is StartRemoteHost -> "/start remote host " + if (remoteHostId == null) "new" else "$remoteHostId multicast=${onOff(multicast)}" + is StartRemoteHost -> "/start remote host " + (if (remoteHostId == null) "new" else "$remoteHostId multicast=${onOff(multicast)}") + (if (address != null) " addr=${address.address} iface=${address.`interface`}" else "") + (if (port != null) " port=$port" else "") is SwitchRemoteHost -> "/switch remote host " + if (remoteHostId == null) "local" else "$remoteHostId" is StopRemoteHost -> "/stop remote host " + if (remoteHostKey == null) "new" else "$remoteHostKey" is DeleteRemoteHost -> "/delete remote host $remoteHostId" @@ -3606,6 +3606,8 @@ data class RemoteHostInfo( val remoteHostId: Long, val hostDeviceName: String, val storePath: String, + val bindAddress_: RemoteCtrlAddress?, + val bindPort_: Int?, val sessionState: RemoteHostSessionState? ) { val activeHost: Boolean @@ -3614,6 +3616,12 @@ data class RemoteHostInfo( fun activeHost(): Boolean = chatModel.currentRemoteHost.value?.remoteHostId == remoteHostId } +@Serializable +data class RemoteCtrlAddress( + val address: String, + val `interface`: String +) + @Serializable sealed class RemoteHostSessionState { @Serializable @SerialName("starting") object Starting: RemoteHostSessionState() @@ -3848,7 +3856,7 @@ sealed class CR { // remote events (desktop) @Serializable @SerialName("remoteHostList") class RemoteHostList(val remoteHosts: List): CR() @Serializable @SerialName("currentRemoteHost") class CurrentRemoteHost(val remoteHost_: RemoteHostInfo?): CR() - @Serializable @SerialName("remoteHostStarted") class RemoteHostStarted(val remoteHost_: RemoteHostInfo?, val invitation: String, val ctrlPort: String): CR() + @Serializable @SerialName("remoteHostStarted") class RemoteHostStarted(val remoteHost_: RemoteHostInfo?, val invitation: String, val localAddrs: List, val ctrlPort: String): CR() @Serializable @SerialName("remoteHostSessionCode") class RemoteHostSessionCode(val remoteHost_: RemoteHostInfo?, val sessionCode: String): CR() @Serializable @SerialName("newRemoteHost") class NewRemoteHost(val remoteHost: RemoteHostInfo): CR() @Serializable @SerialName("remoteHostConnected") class RemoteHostConnected(val remoteHost: RemoteHostInfo): CR() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultBasicTextField.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultBasicTextField.kt index 08dfaa0df..65098cea2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultBasicTextField.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultBasicTextField.kt @@ -152,7 +152,6 @@ fun DefaultConfigurableTextField( BasicTextField( value = state.value, modifier = modifier - .fillMaxWidth() .background(colors.backgroundColor(enabled).value, shape) .indicatorLine(enabled, false, interactionSource, colors) .defaultMinSize( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ExposedDropDownSettingRow.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ExposedDropDownSettingRow.kt index 72a8aaf10..904b2fc34 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ExposedDropDownSettingRow.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ExposedDropDownSettingRow.kt @@ -10,72 +10,87 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +import androidx.compose.ui.unit.* import chat.simplex.res.MR import chat.simplex.common.ui.theme.* import chat.simplex.common.views.usersettings.SettingsActionItemWithContent +@Composable +fun ExposedDropDownSetting( + values: List>, + selection: State, + textColor: Color = MaterialTheme.colors.secondary, + label: String? = null, + enabled: State = mutableStateOf(true), + minWidth: Dp = 200.dp, + maxWidth: Dp = with(LocalDensity.current) { 180.sp.toDp() }, + onSelected: (T) -> Unit +) { + val expanded = remember { mutableStateOf(false) } + ExposedDropdownMenuBox( + expanded = expanded.value, + onExpandedChange = { + expanded.value = !expanded.value && enabled.value + } + ) { + Row( + Modifier.padding(start = 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End + ) { + Text( + values.first { it.first == selection.value }.second + (if (label != null) " $label" else ""), + Modifier.widthIn(max = maxWidth), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = textColor + ) + Spacer(Modifier.size(12.dp)) + Icon( + if (!expanded.value) painterResource(MR.images.ic_expand_more) else painterResource(MR.images.ic_expand_less), + generalGetString(MR.strings.icon_descr_more_button), + tint = MaterialTheme.colors.secondary + ) + } + DefaultExposedDropdownMenu( + modifier = Modifier.widthIn(min = minWidth), + expanded = expanded, + ) { + values.forEach { selectionOption -> + DropdownMenuItem( + onClick = { + onSelected(selectionOption.first) + expanded.value = false + }, + contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f) + ) { + Text( + selectionOption.second + (if (label != null) " $label" else ""), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = if (isInDarkTheme()) MenuTextColorDark else Color.Black, + ) + } + } + } + } +} + @Composable fun ExposedDropDownSettingRow( title: String, values: List>, selection: State, + textColor: Color = MaterialTheme.colors.secondary, label: String? = null, icon: Painter? = null, iconTint: Color = MaterialTheme.colors.secondary, enabled: State = mutableStateOf(true), + minWidth: Dp = 200.dp, + maxWidth: Dp = with(LocalDensity.current) { 180.sp.toDp() }, onSelected: (T) -> Unit ) { SettingsActionItemWithContent(icon, title, iconColor = iconTint, disabled = !enabled.value) { - val expanded = remember { mutableStateOf(false) } - ExposedDropdownMenuBox( - expanded = expanded.value, - onExpandedChange = { - expanded.value = !expanded.value && enabled.value - } - ) { - Row( - Modifier.padding(start = 10.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.End - ) { - val maxWidth = with(LocalDensity.current) { 180.sp.toDp() } - Text( - values.first { it.first == selection.value }.second + (if (label != null) " $label" else ""), - Modifier.widthIn(max = maxWidth), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colors.secondary - ) - Spacer(Modifier.size(12.dp)) - Icon( - if (!expanded.value) painterResource(MR.images.ic_expand_more) else painterResource(MR.images.ic_expand_less), - generalGetString(MR.strings.icon_descr_more_button), - tint = MaterialTheme.colors.secondary - ) - } - DefaultExposedDropdownMenu( - modifier = Modifier.widthIn(min = 200.dp), - expanded = expanded, - ) { - values.forEach { selectionOption -> - DropdownMenuItem( - onClick = { - onSelected(selectionOption.first) - expanded.value = false - }, - contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f) - ) { - Text( - selectionOption.second + (if (label != null) " $label" else ""), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = if (isInDarkTheme()) MenuTextColorDark else Color.Black, - ) - } - } - } - } + ExposedDropDownSetting(values, selection ,textColor, label, enabled, minWidth, maxWidth, onSelected) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt index 6e12681dd..0a0ef17c4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt @@ -55,7 +55,7 @@ fun annotatedStringResource(id: StringResource): AnnotatedString { @Composable fun annotatedStringResource(id: StringResource, vararg args: Any?): AnnotatedString { val density = LocalDensity.current - return remember(id) { + return remember(id, args) { escapedHtmlToAnnotatedString(id.localized().format(args = args), density) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt index a7e76b4ab..c1c5d978c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt @@ -9,6 +9,7 @@ import SectionView import TextIconSpaced import androidx.compose.foundation.* import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.* import androidx.compose.runtime.* @@ -17,6 +18,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.* import androidx.compose.ui.text.input.* @@ -32,11 +34,11 @@ import chat.simplex.common.views.chat.item.ItemAction import chat.simplex.common.views.chatlist.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.newchat.QRCode -import chat.simplex.common.views.usersettings.PreferenceToggle -import chat.simplex.common.views.usersettings.SettingsActionItemWithContent +import chat.simplex.common.views.usersettings.* import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.flow.distinctUntilChanged @Composable fun ConnectMobileView() { @@ -156,7 +158,7 @@ fun DeviceNameField( DefaultConfigurableTextField( state = state, placeholder = generalGetString(MR.strings.enter_this_device_name), - modifier = Modifier.padding(start = DEFAULT_PADDING), + modifier = Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING), isValid = { true }, ) KeyChangeEffect(state.value) { @@ -172,7 +174,10 @@ private fun ConnectMobileViewLayout( sessionCode: String?, port: String?, staleQrCode: Boolean = false, - refreshQrCode: () -> Unit = {} + editEnabled: Boolean = false, + editClicked: () -> Unit = {}, + refreshQrCode: () -> Unit = {}, + UnderQrLayout: @Composable () -> Unit = {}, ) { Column( Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), @@ -196,7 +201,18 @@ private fun ConnectMobileViewLayout( } } SectionTextFooter(annotatedStringResource(MR.strings.open_on_mobile_and_scan_qr_code), textAlign = TextAlign.Center) - SectionTextFooter(annotatedStringResource(MR.strings.waiting_for_mobile_to_connect_on_port, port), textAlign = TextAlign.Center) + Row(verticalAlignment = Alignment.CenterVertically) { + SectionTextFooter(annotatedStringResource(MR.strings.waiting_for_mobile_to_connect_on_port, port), textAlign = TextAlign.Center) + if (editEnabled) { + Spacer(Modifier.width(4.dp)) + IconButton(editClicked, Modifier.size(16.dp)) { + Icon(painterResource(MR.images.ic_edit), stringResource(MR.strings.edit_verb), Modifier.size(16.dp), tint = MaterialTheme.colors.primary) + } + Spacer(Modifier.width(DEFAULT_PADDING)) + } + } + + UnderQrLayout() if (remember { controller.appPrefs.developerTools.state }.value) { val clipboard = LocalClipboardManager.current @@ -259,14 +275,22 @@ private fun showAddingMobileDevice(connecting: MutableState) { @Composable fun AddingMobileDevice(showTitle: Boolean, staleQrCode: MutableState, connecting: MutableState, close: () -> Unit) { - val invitation = rememberSaveable { mutableStateOf(null) } - val port = rememberSaveable { mutableStateOf(null) } + val cachedR = remember { mutableStateOf(null) } + val customAddress = rememberSaveable { mutableStateOf(null) } + val customPort = rememberSaveable { mutableStateOf(null) } + var editing by rememberSaveable { mutableStateOf(false) } val startRemoteHost = suspend { - val r = chatModel.controller.startRemoteHost(null, controller.appPrefs.offerRemoteMulticast.get()) + val r = chatModel.controller.startRemoteHost( + rhId = null, + multicast = controller.appPrefs.offerRemoteMulticast.get(), + address = if (customAddress.value?.address != cachedR.address?.address) customAddress.value else cachedR.rh?.bindAddress_, + port = if (customPort.value != cachedR.port) customPort.value else cachedR.rh?.bindPort_ + ) if (r != null) { + cachedR.value = r connecting.value = true - invitation.value = r.second - port.value = r.third + customAddress.value = cachedR.address + customPort.value = cachedR.port chatModel.remoteHostPairing.value = null to RemoteHostSessionState.Starting } } @@ -283,19 +307,23 @@ fun AddingMobileDevice(showTitle: Boolean, staleQrCode: MutableState, c val remoteDeviceName = pairing.value?.first?.hostDeviceName ConnectMobileViewLayout( title = if (!showTitle) null else if (cachedSessionCode == null) stringResource(MR.strings.link_a_mobile) else stringResource(MR.strings.verify_connection), - invitation = invitation.value, + invitation = cachedR.invitation, deviceName = remoteDeviceName, sessionCode = cachedSessionCode, - port = port.value, - staleQrCode = staleQrCode.value, + port = cachedR.value?.ctrlPort, + staleQrCode = staleQrCode.value || (cachedR.address != customAddress.value && customAddress.value != null) || (cachedR.port != customPort.value && customPort.value != null), + editEnabled = !editing && cachedR.addresses.isNotEmpty(), + editClicked = { editing = true }, refreshQrCode = { withBGApi { if (chatController.stopRemoteHost(null)) { startRemoteHost() staleQrCode.value = false + editing = false } } }, + UnderQrLayout = { UnderQrLayout(editing, cachedR, customAddress, customPort) } ) val oldRemoteHostId by remember { mutableStateOf(chatModel.currentRemoteHost.value?.remoteHostId) } LaunchedEffect(remember { chatModel.currentRemoteHost }.value) { @@ -325,9 +353,26 @@ fun AddingMobileDevice(showTitle: Boolean, staleQrCode: MutableState, c private fun showConnectMobileDevice(rh: RemoteHostInfo, connecting: MutableState) { ModalManager.start.showModalCloseable { close -> + val cachedR = remember { mutableStateOf(null) } + val customAddress = rememberSaveable { mutableStateOf(null) } + val customPort = rememberSaveable { mutableStateOf(null) } + var editing by rememberSaveable { mutableStateOf(false) } + val startRemoteHost = suspend { + val r = chatModel.controller.startRemoteHost( + rhId = rh.remoteHostId, + multicast = controller.appPrefs.offerRemoteMulticast.get(), + address = if (customAddress.value?.address != cachedR.address?.address) customAddress.value else cachedR.rh?.bindAddress_ ?: rh.bindAddress_, + port = if (customPort.value != cachedR.port) customPort.value else cachedR.rh?.bindPort_ ?: rh.bindPort_ + ) + if (r != null) { + cachedR.value = r + connecting.value = true + customAddress.value = cachedR.address + customPort.value = cachedR.port + chatModel.remoteHostPairing.value = null to RemoteHostSessionState.Starting + } + } val pairing = remember { chatModel.remoteHostPairing } - val invitation = rememberSaveable { mutableStateOf(null) } - val port = rememberSaveable { mutableStateOf(null) } val sessionCode = when (val state = pairing.value?.second) { is RemoteHostSessionState.PendingConfirmation -> state.sessionCode else -> null @@ -339,25 +384,25 @@ private fun showConnectMobileDevice(rh: RemoteHostInfo, connecting: MutableState } ConnectMobileViewLayout( title = if (cachedSessionCode == null) stringResource(MR.strings.scan_from_mobile) else stringResource(MR.strings.verify_connection), - invitation = invitation.value, + invitation = cachedR.invitation, deviceName = pairing.value?.first?.hostDeviceName ?: rh.hostDeviceName, sessionCode = cachedSessionCode, - port = port.value + port = cachedR.value?.ctrlPort, + staleQrCode = (cachedR.address != customAddress.value && customAddress.value != null) || (cachedR.port != customPort.value && customPort.value != null), + editEnabled = !editing && cachedR.addresses.isNotEmpty(), + editClicked = { editing = true }, + refreshQrCode = { + withBGApi { + if (chatController.stopRemoteHost(rh.remoteHostId)) { + startRemoteHost() + editing = false + } + } + }, + UnderQrLayout = { UnderQrLayout(editing, cachedR, customAddress, customPort) } ) - var remoteHostId by rememberSaveable { mutableStateOf(null) } - LaunchedEffect(Unit) { - val r = chatModel.controller.startRemoteHost(rh.remoteHostId, controller.appPrefs.offerRemoteMulticast.get()) - if (r != null) { - val (rh_, inv) = r - connecting.value = true - remoteHostId = rh_?.remoteHostId - invitation.value = inv - port.value = r.third - chatModel.remoteHostPairing.value = null to RemoteHostSessionState.Starting - } - } LaunchedEffect(remember { chatModel.currentRemoteHost }.value) { - if (remoteHostId != null && chatModel.currentRemoteHost.value?.remoteHostId == remoteHostId) { + if (cachedR.remoteHostId != null && chatModel.currentRemoteHost.value?.remoteHostId == cachedR.remoteHostId) { close() } } @@ -367,10 +412,13 @@ private fun showConnectMobileDevice(rh: RemoteHostInfo, connecting: MutableState } } DisposableEffect(Unit) { + withBGApi { + startRemoteHost() + } onDispose { - if (remoteHostId != null && chatModel.currentRemoteHost.value?.remoteHostId != remoteHostId) { + if (cachedR.remoteHostId != null && chatModel.currentRemoteHost.value?.remoteHostId != cachedR.remoteHostId) { withBGApi { - chatController.stopRemoteHost(remoteHostId) + chatController.stopRemoteHost(cachedR.remoteHostId) } } chatModel.remoteHostPairing.value = null @@ -403,3 +451,50 @@ private fun showConnectedMobileDevice(rh: RemoteHostInfo, disconnectHost: () -> } } } + +@Composable +private fun UnderQrLayout(editing: Boolean, cachedR: State, customAddress: MutableState, customPort: MutableState) { + if (editing) { + Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) { + ExposedDropDownSetting( + cachedR.addresses.map { it to it.address + " (${it.`interface`})" }, + customAddress, + textColor = MaterialTheme.colors.onBackground, + minWidth = 250.dp, + maxWidth = with(LocalDensity.current) { 250.sp.toDp() }, + onSelected = { + customAddress.value = it + } + ) + val portUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf(TextFieldValue((customPort.value ?: cachedR.port!!).toString())) + } + Spacer(Modifier.width(DEFAULT_PADDING)) + DefaultConfigurableTextField( + portUnsaved, + stringResource(MR.strings.port_verb), + modifier = Modifier.widthIn(max = 100.dp), + isValid = { validPort(it) && it.toInt() > 1023 }, + keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done) }), + keyboardType = KeyboardType.Number, + ) + LaunchedEffect(Unit) { + snapshotFlow { portUnsaved.value.text } + .distinctUntilChanged() + .collect { + if (validPort(it) && it.toInt() > 1023) { + customPort.value = it.toInt() + } + } + } + } + } +} + +private val State.rh: RemoteHostInfo? get() = value?.remoteHost_ +private val State.remoteHostId: Long? get() = value?.remoteHost_?.remoteHostId +private val State.invitation: String? get() = value?.invitation +private val State.address: RemoteCtrlAddress? get() = value?.localAddrs?.firstOrNull() +private val State.addresses: List get() = + (if (controller.appPrefs.developerTools.get()) value?.localAddrs else value?.localAddrs?.filterNot { it.address == "127.0.0.1" }) ?: emptyList() +private val State.port: Int? get() = value?.ctrlPort?.toIntOrNull() 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 a96de71b9..fcd602ee2 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 @@ -305,7 +305,7 @@ fun SockProxySettings(m: ChatModel) { DefaultConfigurableTextField( hostUnsaved, stringResource(MR.strings.host_verb), - modifier = Modifier, + modifier = Modifier.fillMaxWidth(), isValid = ::validHost, keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }), keyboardType = KeyboardType.Text, @@ -315,7 +315,7 @@ fun SockProxySettings(m: ChatModel) { DefaultConfigurableTextField( portUnsaved, stringResource(MR.strings.port_verb), - modifier = Modifier, + modifier = Modifier.fillMaxWidth(), isValid = ::validPort, keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done); save() }), keyboardType = KeyboardType.Number, @@ -428,7 +428,7 @@ private fun validHost(s: String): Boolean { } // https://ihateregex.io/expr/port/ -private fun validPort(s: String): Boolean { +fun validPort(s: String): Boolean { val validPort = Regex("^(6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4})$") return s.isNotBlank() && s.matches(validPort) }