From 1c7dc9a8f7e8cfb3ef428876e3790b6c9416c1aa Mon Sep 17 00:00:00 2001 From: denis0001-dev Date: Wed, 29 Apr 2026 14:00:05 +0300 Subject: [PATCH] Implement default calls port Signed-off-by: denis0001-dev --- .../composeResources/values-ru/strings.xml | 4 +- .../composeResources/values/strings.xml | 4 +- .../kotlin/ru/fromchat/core/Settings.kt | 29 +++++-- .../kotlin/ru/fromchat/core/config/Config.kt | 4 +- .../fromchat/ui/setup/ServerConfigScreen.kt | 85 ++++++++++--------- 5 files changed, 72 insertions(+), 54 deletions(-) diff --git a/app/shared/src/commonMain/composeResources/values-ru/strings.xml b/app/shared/src/commonMain/composeResources/values-ru/strings.xml index 7f44705..c92ed54 100644 --- a/app/shared/src/commonMain/composeResources/values-ru/strings.xml +++ b/app/shared/src/commonMain/composeResources/values-ru/strings.xml @@ -129,9 +129,9 @@ IP или имя сервера 192.168.1.10 Порт - 8301 + Порт звонков - 8302 + Защищённое соединение HTTPS. Отключайте только для HTTP без шифрования. HTTPS diff --git a/app/shared/src/commonMain/composeResources/values/strings.xml b/app/shared/src/commonMain/composeResources/values/strings.xml index abb65d5..db8f821 100644 --- a/app/shared/src/commonMain/composeResources/values/strings.xml +++ b/app/shared/src/commonMain/composeResources/values/strings.xml @@ -156,9 +156,9 @@ Server IP or hostname 192.168.1.10 Port - 8301 + Calls port - 8302 + Secure connection Uses HTTPS. Turn off only for plain HTTP. HTTPS diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/core/Settings.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/core/Settings.kt index 22c26d0..9318e0c 100644 --- a/app/shared/src/commonMain/kotlin/ru/fromchat/core/Settings.kt +++ b/app/shared/src/commonMain/kotlin/ru/fromchat/core/Settings.kt @@ -11,14 +11,19 @@ import kotlinx.serialization.json.Json import ru.fromchat.api.DeviceSessionInfo import ru.fromchat.ui.Theme +/** Default WebRTC / calls port when none is stored or the field is left empty in UI. */ +const val DEFAULT_CALLS_PORT = 8303 + /** - * Server configuration: [serverIp], HTTP(S) [apiPort], optional [callsPort] (WebRTC / HAProxy; null = calls disabled). First install defaults to fromchat.ru:443 with calls on 8302. + * Server configuration: [serverIp], HTTP(S) [apiPort], [callsPort] for WebRTC / HAProxy (defaults to [DEFAULT_CALLS_PORT]). + * [callsEnabled] is updated when saving server settings (probe on the calls port); defaults to true when unset. */ data class ServerConfigData( val serverIp: String, val apiPort: Int, - val callsPort: Int?, + val callsPort: Int, val httpsEnabled: Boolean, + val callsEnabled: Boolean = true, ) object Settings { @@ -28,8 +33,9 @@ object Settings { private const val SERVER_URL_KEY = "server_url" private const val SERVER_IP_KEY = "server_ip" private const val API_PORT_KEY = "api_port" - /** Stored as Int; values ≤ 0 mean unset (calls disabled). */ + /** Stored as Int; invalid values fall back to [DEFAULT_CALLS_PORT] when read. */ private const val CALLS_PORT_KEY = "calls_port" + private const val CALLS_ENABLED_KEY = "calls_enabled" private const val HTTPS_ENABLED_KEY = "https_enabled" private const val DEVICE_SESSIONS_CACHE_KEY = "device_sessions_cache_v1" private const val LAST_SERVER_INSTANCE_ID_KEY = "last_server_instance_id" @@ -105,7 +111,7 @@ object Settings { if (!settings.contains(SERVER_IP_KEY)) { settings.putString(SERVER_IP_KEY, "fromchat.ru") settings.putInt(API_PORT_KEY, 443) - settings.putInt(CALLS_PORT_KEY, 8302) + settings.putInt(CALLS_PORT_KEY, DEFAULT_CALLS_PORT) if (!settings.contains(HTTPS_ENABLED_KEY)) { settings.putBoolean(HTTPS_ENABLED_KEY, true) } @@ -116,21 +122,28 @@ object Settings { } val apiPort = settings.getInt(API_PORT_KEY, 443).coerceIn(1, 65535) val rawCalls = settings.getInt(CALLS_PORT_KEY, -1) - val calls = if (rawCalls > 0 && rawCalls <= 65535) rawCalls else null + val callsPort = if (rawCalls in 1..65535) rawCalls else DEFAULT_CALLS_PORT val https = settings.getBoolean(HTTPS_ENABLED_KEY, true) + val callsEnabled = + if (settings.contains(CALLS_ENABLED_KEY)) { + settings.getBoolean(CALLS_ENABLED_KEY, true) + } else { + true + } ServerConfigData( serverIp = ip, apiPort = apiPort, - callsPort = calls, + callsPort = callsPort, httpsEnabled = https, + callsEnabled = callsEnabled, ) } set(value) = runIO { settings.putString(SERVER_IP_KEY, value.serverIp.trim()) settings.putInt(API_PORT_KEY, value.apiPort.coerceIn(1, 65535)) - val callsStored = value.callsPort?.coerceIn(1, 65535) ?: -1 - settings.putInt(CALLS_PORT_KEY, callsStored) + settings.putInt(CALLS_PORT_KEY, value.callsPort.coerceIn(1, 65535)) settings.putBoolean(HTTPS_ENABLED_KEY, value.httpsEnabled) + settings.putBoolean(CALLS_ENABLED_KEY, value.callsEnabled) } /** Cached device sessions (JSON). Shown immediately while refreshing from the network. */ diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/core/config/Config.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/core/config/Config.kt index d0b721d..24391a7 100644 --- a/app/shared/src/commonMain/kotlin/ru/fromchat/core/config/Config.kt +++ b/app/shared/src/commonMain/kotlin/ru/fromchat/core/config/Config.kt @@ -35,9 +35,9 @@ object Config { _serverConfig.value = config } - /** Voice/video calls (LiveKit) are enabled only when a calls (WebRTC) port is configured. */ + /** Voice/video calls when the last server-config apply probe to the calls port succeeded. */ val callsEnabled: Boolean - get() = config.callsPort != null + get() = config.callsEnabled /** * LiveKit signaling WebSocket URL: same host and API port as HTTPS reverse proxy (`/api/livekit/rtc`). diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/setup/ServerConfigScreen.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/setup/ServerConfigScreen.kt index f514e72..2082e0d 100644 --- a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/setup/ServerConfigScreen.kt +++ b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/setup/ServerConfigScreen.kt @@ -107,6 +107,7 @@ import dev.chrisbanes.haze.rememberHazeState import kotlin.time.TimeSource import kotlinx.coroutines.delay import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withTimeout import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.compose.resources.getString @@ -114,13 +115,12 @@ import org.jetbrains.compose.resources.stringResource import ru.fromchat.Res import ru.fromchat.api.ApiClient import ru.fromchat.api.WebSocketManager -import ru.fromchat.api_port_hint import ru.fromchat.api_port_label import ru.fromchat.back -import ru.fromchat.calls_port_hint import ru.fromchat.calls_port_label import ru.fromchat.cancel import ru.fromchat.confirm +import ru.fromchat.core.DEFAULT_CALLS_PORT import ru.fromchat.core.ServerConfigData import ru.fromchat.core.Settings import ru.fromchat.core.config.Config @@ -137,7 +137,6 @@ import ru.fromchat.server_config_snackbar_api_fail import ru.fromchat.server_config_snackbar_defaults import ru.fromchat.server_config_snackbar_ok_api_calls_bad import ru.fromchat.server_config_snackbar_ok_calls -import ru.fromchat.server_config_snackbar_ok_calls_skip import ru.fromchat.server_config_subtitle import ru.fromchat.server_config_title import ru.fromchat.server_ip_hint @@ -206,6 +205,13 @@ private fun apiBaseUrlFor(config: ServerConfigData): String { return "$scheme://${config.serverIp}:${config.apiPort}/api" } +private suspend fun probeCallsReachable(config: ServerConfigData): Boolean { + val urlScheme = if (config.httpsEnabled) "https" else "http" + val host = config.serverIp.trim() + val root = "$urlScheme://${hostForAuthority(host)}:${config.callsPort}/" + return ApiClient.probeHttpGet(root) +} + private fun resolvedApiPort(apiPortText: String): Int { val t = apiPortText.trim() if (t.isEmpty()) return 443 @@ -213,9 +219,12 @@ private fun resolvedApiPort(apiPortText: String): Int { return n.takeIf { it in 1..65535 } ?: 443 } -private fun resolvedCallsPort(callsPortText: String): Int? = - if (callsPortText.isBlank()) null - else callsPortText.trim().toIntOrNull()?.takeIf { it in 1..65535 } +/** Blank calls field uses [DEFAULT_CALLS_PORT] instead of disabling calls. */ +private fun effectiveCallsPort(callsPortText: String): Int { + val t = callsPortText.trim() + if (t.isBlank()) return DEFAULT_CALLS_PORT + return t.toIntOrNull()?.takeIf { it in 1..65535 } ?: DEFAULT_CALLS_PORT +} private suspend fun LazyListState.scrollFocusedItemIntoView( itemIndex: Int, @@ -421,7 +430,7 @@ fun ServerConfigScreen() { val c = Settings.serverConfig serverIp = c.serverIp apiPortText = c.apiPort.toString() - callsPortText = c.callsPort?.toString().orEmpty() + callsPortText = c.callsPort.toString() httpsEnabled = c.httpsEnabled } @@ -447,7 +456,7 @@ fun ServerConfigScreen() { val callsPortError = callsPortText.isNotEmpty() && !isValidPortNumber(callsPortText) val apiPortEffective = resolvedApiPort(apiPortText) - val callsPortParsed = resolvedCallsPort(callsPortText) + val callsPortParsed = effectiveCallsPort(callsPortText) val canApply = hostOk && !apiPortError && @@ -457,7 +466,7 @@ fun ServerConfigScreen() { val resetToDefaults = { serverIp = "fromchat.ru" apiPortText = "443" - callsPortText = "8302" + callsPortText = DEFAULT_CALLS_PORT.toString() httpsEnabled = true scope.launch { snackbarHostState.showSnackbar(strSnackbarDefaults) @@ -466,6 +475,8 @@ fun ServerConfigScreen() { val verifyServer: () -> Unit = { scope.launch { + // Do not block actual checks on snackbar dismissal. + launch { snackbarHostState.showSnackbar("Checking...") } val host = serverIp.trim() if (host.isEmpty() || !isValidIpOrHostname(host)) { snackbarHostState.showSnackbar(strHostError) @@ -480,7 +491,7 @@ fun ServerConfigScreen() { return@launch } val apiPort = resolvedApiPort(apiPortText) - val calls = resolvedCallsPort(callsPortText) + val calls = effectiveCallsPort(callsPortText) val tentative = ServerConfigData( serverIp = host, apiPort = apiPort, @@ -488,32 +499,24 @@ fun ServerConfigScreen() { httpsEnabled = httpsEnabled, ) val apiBase = apiBaseUrlFor(tentative) - val pingMark = TimeSource.Monotonic.markNow() - val id = runCatching { ApiClient.fetchServerInstanceId(apiBase) } - .getOrNull() - ?.trim() - .orEmpty() - val pingMs = pingMark.elapsedNow().inWholeMilliseconds - .toInt() - .coerceAtLeast(0) - if (id.isEmpty()) { - snackbarHostState.showSnackbar(strSnackbarApiFail) - return@launch - } - val urlScheme = if (httpsEnabled) "https" else "http" - val callsOk = if (calls != null) { - val root = "$urlScheme://${hostForAuthority(host)}:${calls}/" - ApiClient.probeHttpGet(root) - } else { - null - } - val msg = when { - calls == null -> - getString(Res.string.server_config_snackbar_ok_calls_skip, pingMs) - callsOk == true -> - getString(Res.string.server_config_snackbar_ok_calls, pingMs) - else -> strSnackbarOkApiCallsBad - } + + val msg = runCatching { + withTimeout(3000) { + val pingMark = TimeSource.Monotonic.markNow() + val id = ApiClient.fetchServerInstanceId(apiBase) + val pingMs = pingMark.elapsedNow().inWholeMilliseconds + .toInt() + .coerceAtLeast(0) + if (id.isEmpty()) return@withTimeout strSnackbarApiFail + + val urlScheme = if (httpsEnabled) "https" else "http" + val root = "$urlScheme://${hostForAuthority(host)}:${calls}/" + val callsOk = ApiClient.probeHttpGet(root) + if (callsOk) getString(Res.string.server_config_snackbar_ok_calls, pingMs) + else strSnackbarOkApiCallsBad + } + }.getOrElse { strSnackbarApiFail } + snackbarHostState.showSnackbar(msg) } } @@ -621,7 +624,8 @@ fun ServerConfigScreen() { val bearer = ApiClient.token?.trim().orEmpty() if (bearer.isEmpty()) { - Config.updateServerConfig(tentative) + val callsOk = probeCallsReachable(tentative) + Config.updateServerConfig(tentative.copy(callsEnabled = callsOk)) Settings.lastKnownServerInstanceId = newId WebSocketManager.disconnect() withContext(Dispatchers.Main) { @@ -646,7 +650,8 @@ fun ServerConfigScreen() { return@launch } - Config.updateServerConfig(tentative) + val callsOk = probeCallsReachable(tentative) + Config.updateServerConfig(tentative.copy(callsEnabled = callsOk)) Settings.lastKnownServerInstanceId = newId WebSocketManager.disconnect() WebSocketManager.connect(forceRestart = true) @@ -839,7 +844,7 @@ fun ServerConfigScreen() { overflow = TextOverflow.Ellipsis, ) }, - placeholder = { Text(stringResource(Res.string.api_port_hint)) }, + placeholder = { Text("8301") }, modifier = Modifier .fillMaxWidth() .onFocusChanged { @@ -872,7 +877,7 @@ fun ServerConfigScreen() { overflow = TextOverflow.Ellipsis, ) }, - placeholder = { Text(stringResource(Res.string.calls_port_hint)) }, + placeholder = { Text(DEFAULT_CALLS_PORT.toString()) }, modifier = Modifier .fillMaxWidth() .onFocusChanged {