Merge commit '9329bf61447ad3ce9640d82b7b08f05f1db1729f' into master-android

This commit is contained in:
Evgeny Poberezkin
2025-05-07 09:56:53 +01:00
237 changed files with 10493 additions and 5515 deletions
@@ -38,6 +38,7 @@ import java.net.URI
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.*
import java.util.concurrent.atomic.AtomicLong
import kotlin.collections.ArrayList
import kotlin.random.Random
import kotlin.time.*
@@ -1396,19 +1397,21 @@ sealed class ChatInfo: SomeChat, NamedChat {
}
@Serializable @SerialName("invalidJSON")
class InvalidJSON(val json: String): ChatInfo() {
class InvalidJSON(
val json: String,
override val apiId: Long = -idGenerator.getAndIncrement(),
override val createdAt: Instant = Clock.System.now(),
override val updatedAt: Instant = Clock.System.now()
): ChatInfo() {
override val chatType get() = ChatType.Direct
override val localDisplayName get() = invalidChatName
override val id get() = ""
override val apiId get() = 0L
override val id get() = "?$apiId"
override val ready get() = false
override val chatDeleted get() = false
override val sendMsgEnabled get() = false
override val incognito get() = false
override fun featureEnabled(feature: ChatFeature) = false
override val timedMessagesTTL: Int? get() = null
override val createdAt get() = Clock.System.now()
override val updatedAt get() = Clock.System.now()
override val displayName get() = invalidChatName
override val fullName get() = invalidChatName
override val image get() = null
@@ -1416,6 +1419,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
companion object {
private val invalidChatName = generalGetString(MR.strings.invalid_chat)
private val idGenerator = AtomicLong(0)
}
}
@@ -48,7 +48,7 @@ private fun sendCommand(chatModel: ChatModel, composeState: MutableState<Compose
val prefPerformLA = chatModel.controller.appPrefs.performLA.get()
val s = composeState.value.message
if (s.text.startsWith("/sql") && (!prefPerformLA || !developerTools)) {
val resp = CR.ChatCmdError(null, ChatError.ChatErrorChat(ChatErrorType.CommandError("Failed reading: empty")))
val resp = API.Error(null, ChatError.ChatErrorChat(ChatErrorType.CommandError("Failed reading: empty")))
chatModel.addTerminalItem(TerminalItem.cmd(null, CC.Console(s.text)))
chatModel.addTerminalItem(TerminalItem.resp(null, resp))
composeState.value = ComposeState(useLinkPreviews = false)
@@ -10,13 +10,13 @@ import kotlinx.coroutines.*
expect fun ActiveCallView()
fun activeCallWaitDeliveryReceipt(scope: CoroutineScope) = scope.launch(Dispatchers.Default) {
for (apiResp in controller.messagesChannel) {
for (msg in controller.messagesChannel) {
val call = chatModel.activeCall.value
if (call == null || call.callState > CallState.InvitationSent) break
val msg = apiResp.resp
if (apiResp.remoteHostId == call.remoteHostId &&
msg is CR.ChatItemsStatusesUpdated &&
msg.chatItems.any {
if (msg.rhId == call.remoteHostId &&
msg is API.Result &&
msg.res is CR.ChatItemsStatusesUpdated &&
msg.res.chatItems.any {
it.chatInfo.id == call.contact.id && it.chatItem.content is CIContent.SndCall && it.chatItem.meta.itemStatus is CIStatus.SndRcvd
}
) {
@@ -554,11 +554,11 @@ fun ChatView(
ChatItemInfoView(chatRh, cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get(), chatInfo)
LaunchedEffect(cItem.id) {
withContext(Dispatchers.Default) {
for (apiResp in controller.messagesChannel) {
val msg = apiResp.resp
if (apiResp.remoteHostId == chatRh &&
msg is CR.ChatItemsStatusesUpdated &&
msg.chatItems.any { it.chatItem.id == cItem.id }
for (msg in controller.messagesChannel) {
if (msg.rhId == chatRh &&
msg is API.Result &&
msg.res is CR.ChatItemsStatusesUpdated &&
msg.res.chatItems.any { it.chatItem.id == cItem.id }
) {
ciInfo = loadChatItemInfo() ?: return@withContext
initialCiInfo = ciInfo
@@ -435,7 +435,7 @@ suspend fun encryptDatabase(
}
val error = m.controller.apiStorageEncryption(currentKey.value, newKey.value)
appPrefs.encryptionStartedAt.set(null)
val sqliteError = ((error?.chatError as? ChatError.ChatErrorDatabase)?.databaseError as? DatabaseError.ErrorExport)?.sqliteError
val sqliteError = ((error as? ChatError.ChatErrorDatabase)?.databaseError as? DatabaseError.ErrorExport)?.sqliteError
when {
sqliteError is SQLiteError.ErrorNotADatabase -> {
operationEnded(m, progressIndicator) {
@@ -449,7 +449,7 @@ suspend fun encryptDatabase(
error != null -> {
operationEnded(m, progressIndicator) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_encrypting_database),
"failed to set storage encryption: ${error.responseType} ${error.details}"
"failed to set storage encryption: error ${error.string}"
)
}
false
@@ -468,12 +468,12 @@ private suspend fun MutableState<MigrationFromState>.verifyDatabasePassphrase(db
val error = controller.testStorageEncryption(dbKey)
if (error == null) {
state = MigrationFromState.UploadConfirmation
} else if (((error.chatError as? ChatError.ChatErrorDatabase)?.databaseError as? DatabaseError.ErrorOpen)?.sqliteError is SQLiteError.ErrorNotADatabase) {
} else if (((error as? ChatError.ChatErrorDatabase)?.databaseError as? DatabaseError.ErrorOpen)?.sqliteError is SQLiteError.ErrorNotADatabase) {
showErrorOnMigrationIfNeeded(DBMigrationResult.ErrorNotADatabase(""))
} else {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.error),
text = generalGetString(MR.strings.migrate_from_device_error_verifying_passphrase) + " " + error.details
text = generalGetString(MR.strings.migrate_from_device_error_verifying_passphrase) + " " + error.string
)
}
}
@@ -556,11 +556,12 @@ private fun MutableState<MigrationFromState>.startUploading(
) {
withBGApi {
chatReceiver.value = MigrationFromChatReceiver(ctrl, tempDatabaseFile) { msg ->
when (msg) {
val r = msg.result
when (r) {
is CR.SndFileProgressXFTP -> {
val s = state
if (s is MigrationFromState.UploadProgress && s.uploadedBytes != s.totalBytes) {
state = MigrationFromState.UploadProgress(msg.sentSize, msg.totalSize, msg.fileTransferMeta.fileId, archivePath, ctrl, user)
state = MigrationFromState.UploadProgress(r.sentSize, r.totalSize, r.fileTransferMeta.fileId, archivePath, ctrl, user)
}
}
is CR.SndFileRedirectStartXFTP -> {
@@ -578,7 +579,7 @@ private fun MutableState<MigrationFromState>.startUploading(
requiredHostMode = cfg.requiredHostMode
)
)
state = MigrationFromState.LinkShown(msg.fileTransferMeta.fileId, data.addToLink(msg.rcvURIs[0]), ctrl)
state = MigrationFromState.LinkShown(r.fileTransferMeta.fileId, data.addToLink(r.rcvURIs[0]), ctrl)
}
is CR.SndFileError -> {
AlertManager.shared.showAlertMsg(
@@ -692,7 +693,7 @@ private class MigrationFromChatReceiver(
val ctrl: ChatCtrl,
val databaseUrl: File,
var receiveMessages: Boolean = true,
val processReceivedMsg: suspend (CR) -> Unit
val processReceivedMsg: suspend (API) -> Unit
) {
fun start() {
Log.d(TAG, "MigrationChatReceiver startReceiver")
@@ -701,19 +702,18 @@ private class MigrationFromChatReceiver(
try {
val msg = ChatController.recvMsg(ctrl)
if (msg != null && receiveMessages) {
val r = msg.resp
val rhId = msg.remoteHostId
Log.d(TAG, "processReceivedMsg: ${r.responseType}")
chatModel.addTerminalItem(TerminalItem.resp(rhId, r))
val rhId = msg.rhId
Log.d(TAG, "processReceivedMsg: ${msg.responseType}")
chatModel.addTerminalItem(TerminalItem.resp(rhId, msg))
val finishedWithoutTimeout = withTimeoutOrNull(60_000L) {
processReceivedMsg(r)
processReceivedMsg(msg)
}
if (finishedWithoutTimeout == null) {
Log.e(TAG, "Timeout reached while processing received message: " + msg.resp.responseType)
Log.e(TAG, "Timeout reached while processing received message: " + msg.responseType)
if (appPreferences.developerTools.get() && appPreferences.showSlowApiCalls.get()) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.possible_slow_function_title),
text = generalGetString(MR.strings.possible_slow_function_desc).format(60, msg.resp.responseType + "\n" + Exception().stackTraceToString()),
text = generalGetString(MR.strings.possible_slow_function_desc).format(60, msg.responseType + "\n" + Exception().stackTraceToString()),
shareText = true
)
}
@@ -580,12 +580,13 @@ private fun MutableState<MigrationToState?>.startDownloading(
) {
withBGApi {
chatReceiver.value = MigrationToChatReceiver(ctrl, tempDatabaseFile) { msg ->
when (msg) {
is CR.RcvFileProgressXFTP -> {
state = MigrationToState.DownloadProgress(msg.receivedSize, msg.totalSize, msg.rcvFileTransfer.fileId, link, archivePath, netCfg, networkProxy, ctrl)
val r = msg.result
when {
r is CR.RcvFileProgressXFTP -> {
state = MigrationToState.DownloadProgress(r.receivedSize, r.totalSize, r.rcvFileTransfer.fileId, link, archivePath, netCfg, networkProxy, ctrl)
MigrationToDeviceState.save(MigrationToDeviceState.DownloadProgress(link, File(archivePath).name, netCfg, networkProxy))
}
is CR.RcvStandaloneFileComplete -> {
r is CR.RcvStandaloneFileComplete -> {
delay(500)
// User closed the whole screen before new state was saved
if (state == null) {
@@ -595,22 +596,22 @@ private fun MutableState<MigrationToState?>.startDownloading(
MigrationToDeviceState.save(MigrationToDeviceState.ArchiveImport(File(archivePath).name, netCfg, networkProxy))
}
}
is CR.RcvFileError -> {
r is CR.RcvFileError -> {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.migrate_to_device_download_failed),
generalGetString(MR.strings.migrate_to_device_file_delete_or_link_invalid)
)
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg, networkProxy)
}
is CR.ChatRespError -> {
if (msg.chatError is ChatError.ChatErrorChat && msg.chatError.errorType is ChatErrorType.NoRcvFileUser) {
msg is API.Error -> {
if (msg.err is ChatError.ChatErrorChat && msg.err.errorType is ChatErrorType.NoRcvFileUser) {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.migrate_to_device_download_failed),
generalGetString(MR.strings.migrate_to_device_file_delete_or_link_invalid)
)
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg, networkProxy)
} else {
Log.d(TAG, "unsupported error: ${msg.responseType}, ${json.encodeToString(msg.chatError)}")
Log.d(TAG, "unsupported error: ${msg.responseType}, ${json.encodeToString(msg.err)}")
}
}
else -> Log.d(TAG, "unsupported event: ${msg.responseType}")
@@ -739,7 +740,7 @@ private class MigrationToChatReceiver(
val ctrl: ChatCtrl,
val databaseUrl: File,
var receiveMessages: Boolean = true,
val processReceivedMsg: suspend (CR) -> Unit
val processReceivedMsg: suspend (API) -> Unit
) {
fun start() {
Log.d(TAG, "MigrationChatReceiver startReceiver")
@@ -748,19 +749,18 @@ private class MigrationToChatReceiver(
try {
val msg = ChatController.recvMsg(ctrl)
if (msg != null && receiveMessages) {
val r = msg.resp
val rhId = msg.remoteHostId
Log.d(TAG, "processReceivedMsg: ${r.responseType}")
chatModel.addTerminalItem(TerminalItem.resp(rhId, r))
val rhId = msg.rhId
Log.d(TAG, "processReceivedMsg: ${msg.responseType}")
chatModel.addTerminalItem(TerminalItem.resp(rhId, msg))
val finishedWithoutTimeout = withTimeoutOrNull(60_000L) {
processReceivedMsg(r)
processReceivedMsg(msg)
}
if (finishedWithoutTimeout == null) {
Log.e(TAG, "Timeout reached while processing received message: " + msg.resp.responseType)
Log.e(TAG, "Timeout reached while processing received message: " + msg.responseType)
if (appPreferences.developerTools.get() && appPreferences.showSlowApiCalls.get()) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.possible_slow_function_title),
text = generalGetString(MR.strings.possible_slow_function_desc).format(60, msg.resp.responseType + "\n" + Exception().stackTraceToString()),
text = generalGetString(MR.strings.possible_slow_function_desc).format(60, msg.responseType + "\n" + Exception().stackTraceToString()),
shareText = true
)
}
@@ -492,7 +492,7 @@ private suspend fun connectDesktopAddress(sessionAddress: MutableState<String>,
}
}
private suspend fun connectDesktop(sessionAddress: MutableState<String>, connect: suspend () -> Pair<SomeRemoteCtrl?, CR.ChatCmdError?>): Boolean {
private suspend fun connectDesktop(sessionAddress: MutableState<String>, connect: suspend () -> Pair<SomeRemoteCtrl?, ChatError?>): Boolean {
val res = connect()
if (res.first != null) {
val (rc_, ctrlAppInfo, v) = res.first!!
@@ -505,13 +505,13 @@ private suspend fun connectDesktop(sessionAddress: MutableState<String>, connect
} else {
val e = res.second ?: return false
when {
e.chatError is ChatError.ChatErrorRemoteCtrl && e.chatError.remoteCtrlError is RemoteCtrlError.BadInvitation -> showBadInvitationErrorAlert()
e.chatError is ChatError.ChatErrorChat && e.chatError.errorType is ChatErrorType.CommandError -> showBadInvitationErrorAlert()
e.chatError is ChatError.ChatErrorRemoteCtrl && e.chatError.remoteCtrlError is RemoteCtrlError.BadVersion -> showBadVersionAlert(v = e.chatError.remoteCtrlError.appVersion)
e.chatError is ChatError.ChatErrorAgent && e.chatError.agentError is AgentErrorType.RCP && e.chatError.agentError.rcpErr is RCErrorType.VERSION -> showBadVersionAlert(v = null)
e.chatError is ChatError.ChatErrorAgent && e.chatError.agentError is AgentErrorType.RCP && e.chatError.agentError.rcpErr is RCErrorType.CTRL_AUTH -> showDesktopDisconnectedErrorAlert()
e is ChatError.ChatErrorRemoteCtrl && e.remoteCtrlError is RemoteCtrlError.BadInvitation -> showBadInvitationErrorAlert()
e is ChatError.ChatErrorChat && e.errorType is ChatErrorType.CommandError -> showBadInvitationErrorAlert()
e is ChatError.ChatErrorRemoteCtrl && e.remoteCtrlError is RemoteCtrlError.BadVersion -> showBadVersionAlert(v = e.remoteCtrlError.appVersion)
e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.RCP && e.agentError.rcpErr is RCErrorType.VERSION -> showBadVersionAlert(v = null)
e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.RCP && e.agentError.rcpErr is RCErrorType.CTRL_AUTH -> showDesktopDisconnectedErrorAlert()
else -> {
val errMsg = "${e.responseType}: ${e.details}"
val errMsg = "error: ${e.string}"
Log.e(TAG, "bad response: $errMsg")
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), errMsg)
}
@@ -291,7 +291,7 @@ private fun DeliveryReceiptsSection(
SectionView(stringResource(MR.strings.settings_section_title_delivery_receipts)) {
SettingsActionItemWithContent(painterResource(MR.images.ic_person), stringResource(MR.strings.receipts_section_contacts)) {
DefaultSwitch(
checked = currentUser.sendRcptsContacts ?: false,
checked = currentUser.sendRcptsContacts,
onCheckedChange = { enable ->
setOrAskSendReceiptsContacts(enable)
}
@@ -299,7 +299,7 @@ private fun DeliveryReceiptsSection(
}
SettingsActionItemWithContent(painterResource(MR.images.ic_group), stringResource(MR.strings.receipts_section_groups)) {
DefaultSwitch(
checked = currentUser.sendRcptsSmallGroups ?: false,
checked = currentUser.sendRcptsSmallGroups,
onCheckedChange = { enable ->
setOrAskSendReceiptsGroups(enable)
}
@@ -19,6 +19,7 @@ import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.ui.theme.*
@@ -45,7 +46,7 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U
val sessionMode = remember { mutableStateOf(currentCfgVal.sessionMode) }
val smpProxyMode = remember { mutableStateOf(currentCfgVal.smpProxyMode) }
val smpProxyFallback = remember { mutableStateOf(currentCfgVal.smpProxyFallback) }
val smpWebPort = remember { mutableStateOf(currentCfgVal.smpWebPort) }
val smpWebPortServers = remember { mutableStateOf(currentCfgVal.smpWebPortServers) }
val networkTCPConnectTimeout = remember { mutableStateOf(currentCfgVal.tcpConnectTimeout) }
val networkTCPTimeout = remember { mutableStateOf(currentCfgVal.tcpTimeout) }
@@ -84,7 +85,7 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U
sessionMode = sessionMode.value,
smpProxyMode = smpProxyMode.value,
smpProxyFallback = smpProxyFallback.value,
smpWebPort = smpWebPort.value,
smpWebPortServers = smpWebPortServers.value,
tcpConnectTimeout = networkTCPConnectTimeout.value,
tcpTimeout = networkTCPTimeout.value,
tcpTimeoutPerKb = networkTCPTimeoutPerKb.value,
@@ -99,7 +100,7 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U
sessionMode.value = cfg.sessionMode
smpProxyMode.value = cfg.smpProxyMode
smpProxyFallback.value = cfg.smpProxyFallback
smpWebPort.value = cfg.smpWebPort
smpWebPortServers.value = cfg.smpWebPortServers
networkTCPConnectTimeout.value = cfg.tcpConnectTimeout
networkTCPTimeout.value = cfg.tcpTimeout
networkTCPTimeoutPerKb.value = cfg.tcpTimeoutPerKb
@@ -154,7 +155,7 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U
sessionMode = sessionMode,
smpProxyMode = smpProxyMode,
smpProxyFallback = smpProxyFallback,
smpWebPort,
smpWebPortServers,
networkTCPConnectTimeout,
networkTCPTimeout,
networkTCPTimeoutPerKb,
@@ -187,7 +188,7 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U
sessionMode: MutableState<TransportSessionMode>,
smpProxyMode: MutableState<SMPProxyMode>,
smpProxyFallback: MutableState<SMPProxyFallback>,
smpWebPort: MutableState<Boolean>,
smpWebPortServers: MutableState<SMPWebPortServers>,
networkTCPConnectTimeout: MutableState<Long>,
networkTCPTimeout: MutableState<Long>,
networkTCPTimeoutPerKb: MutableState<Long>,
@@ -226,11 +227,16 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U
}
SectionDividerSpaced()
SectionView(stringResource(MR.strings.network_smp_web_port_section_title).uppercase()) {
PreferenceToggle(stringResource(MR.strings.network_smp_web_port_toggle), checked = smpWebPort.value) {
smpWebPort.value = it
}
ExposedDropDownSettingRow(
stringResource(MR.strings.network_smp_web_port_toggle),
SMPWebPortServers.entries.map { it to stringResource(it.text) },
smpWebPortServers
) { smpWebPortServers.value = it }
}
SectionTextFooter(String.format(stringResource(MR.strings.network_smp_web_port_footer), if (smpWebPort.value) "443" else "5223"))
SectionTextFooter(
if (smpWebPortServers.value == SMPWebPortServers.Preset) stringResource(MR.strings.network_smp_web_port_preset_footer)
else String.format(stringResource(MR.strings.network_smp_web_port_footer), if (smpWebPortServers.value == SMPWebPortServers.All) "443" else "5223")
)
SectionDividerSpaced(maxTopPadding = true)
SectionView(stringResource(MR.strings.network_option_tcp_connection).uppercase()) {
@@ -320,7 +326,7 @@ private fun SMPProxyModePicker(
) {
val density = LocalDensity.current
val values = remember {
SMPProxyMode.values().map {
SMPProxyMode.entries.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))
@@ -355,7 +361,7 @@ private fun SMPProxyFallbackPicker(
) {
val density = LocalDensity.current
val values = remember {
SMPProxyFallback.values().map {
SMPProxyFallback.entries.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))
@@ -548,7 +554,7 @@ fun PreviewAdvancedNetworkSettingsLayout() {
sessionMode = remember { mutableStateOf(TransportSessionMode.User) },
smpProxyMode = remember { mutableStateOf(SMPProxyMode.Never) },
smpProxyFallback = remember { mutableStateOf(SMPProxyFallback.Allow) },
smpWebPort = remember { mutableStateOf(false) },
smpWebPortServers = remember { mutableStateOf(SMPWebPortServers.Preset) },
networkTCPConnectTimeout = remember { mutableStateOf(10_000000) },
networkTCPTimeout = remember { mutableStateOf(10_000000) },
networkTCPTimeoutPerKb = remember { mutableStateOf(10_000) },
@@ -964,6 +964,10 @@
<string name="network_smp_web_port_section_title">TCP port for messaging</string>
<string name="network_smp_web_port_toggle">Use web port</string>
<string name="network_smp_web_port_footer">Use TCP port %1$s when no port is specified.</string>
<string name="network_smp_web_port_preset_footer">Use TCP port 443 for preset servers only.</string>
<string name="network_smp_web_port_all">All servers</string>
<string name="network_smp_web_port_preset">Preset servers</string>
<string name="network_smp_web_port_off">Off</string>
<string name="appearance_settings">Appearance</string>
<string name="customize_theme_title">Customize theme</string>
<string name="theme_colors_section_title">INTERFACE COLORS</string>
@@ -2440,6 +2440,10 @@
<string name="report_compose_reason_header_other">Пожаловаться: увидят только модераторы группы.</string>
<string name="mute_all_chat">Выключить уведомления для всех</string>
<string name="network_smp_web_port_footer">Использовать TCP-порт %1$s, когда порт не указан.</string>
<string name="network_smp_web_port_preset_footer">Использовать TCP-порт 443 только для серверов по умолчанию.</string>
<string name="network_smp_web_port_all">Все серверы</string>
<string name="network_smp_web_port_preset">Серверы по умолчанию</string>
<string name="network_smp_web_port_off">Нет</string>
<string name="network_smp_web_port_toggle">Использовать веб-порт</string>
<string name="privacy_chat_list_open_links_no">Нет</string>
<string name="report_compose_reason_header_illegal">Пожаловаться на сообщение: увидят только модераторы группы.</string>