mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-04-26 13:08:02 +00:00
android: Allow configuring WebRTC ICE servers (#1090)
* Allow configuring WebRTC ICE servers * refactor Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
36d93f5b0e
commit
a9ba16b07a
@@ -87,6 +87,7 @@ class AppPreferences(val context: Context) {
|
||||
)
|
||||
val performLA = mkBoolPreference(SHARED_PREFS_PERFORM_LA, false)
|
||||
val laNoticeShown = mkBoolPreference(SHARED_PREFS_LA_NOTICE_SHOWN, false)
|
||||
val webrtcIceServers = mkStrPreference(SHARED_PREFS_WEBRTC_ICE_SERVERS, null)
|
||||
val privacyAcceptImages = mkBoolPreference(SHARED_PREFS_PRIVACY_ACCEPT_IMAGES, true)
|
||||
val privacyLinkPreviews = mkBoolPreference(SHARED_PREFS_PRIVACY_LINK_PREVIEWS, true)
|
||||
val experimentalCalls = mkBoolPreference(SHARED_PREFS_EXPERIMENTAL_CALLS, false)
|
||||
@@ -174,6 +175,7 @@ class AppPreferences(val context: Context) {
|
||||
private const val SHARED_PREFS_WEBRTC_CALLS_ON_LOCK_SCREEN = "CallsOnLockScreen"
|
||||
private const val SHARED_PREFS_PERFORM_LA = "PerformLA"
|
||||
private const val SHARED_PREFS_LA_NOTICE_SHOWN = "LANoticeShown"
|
||||
private const val SHARED_PREFS_WEBRTC_ICE_SERVERS = "WebrtcICEServers"
|
||||
private const val SHARED_PREFS_PRIVACY_ACCEPT_IMAGES = "PrivacyAcceptImages"
|
||||
private const val SHARED_PREFS_PRIVACY_LINK_PREVIEWS = "PrivacyLinkPreviews"
|
||||
private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls"
|
||||
@@ -960,7 +962,16 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
withCall(r, r.contact) { call ->
|
||||
chatModel.activeCall.value = call.copy(callState = CallState.OfferReceived, peerMedia = r.callType.media, sharedKey = r.sharedKey)
|
||||
val useRelay = chatModel.controller.appPrefs.webrtcPolicyRelay.get()
|
||||
chatModel.callCommand.value = WCallCommand.Offer(offer = r.offer.rtcSession, iceCandidates = r.offer.rtcIceCandidates, media = r.callType.media, aesKey = r.sharedKey, relay = useRelay)
|
||||
val iceServers = getIceServers()
|
||||
Log.d(TAG, ".callOffer iceServers $iceServers")
|
||||
chatModel.callCommand.value = WCallCommand.Offer(
|
||||
offer = r.offer.rtcSession,
|
||||
iceCandidates = r.offer.rtcIceCandidates,
|
||||
media = r.callType.media,
|
||||
aesKey = r.sharedKey,
|
||||
iceServers = iceServers,
|
||||
relay = useRelay
|
||||
)
|
||||
}
|
||||
}
|
||||
is CR.CallAnswer -> {
|
||||
|
||||
@@ -51,7 +51,14 @@ class CallManager(val chatModel: ChatModel) {
|
||||
)
|
||||
showCallView.value = true
|
||||
val useRelay = controller.appPrefs.webrtcPolicyRelay.get()
|
||||
callCommand.value = WCallCommand.Start (media = invitation.callType.media, aesKey = invitation.sharedKey, relay = useRelay)
|
||||
val iceServers = getIceServers()
|
||||
Log.d(TAG, "answerIncomingCall iceServers: $iceServers")
|
||||
callCommand.value = WCallCommand.Start(
|
||||
media = invitation.callType.media,
|
||||
aesKey = invitation.sharedKey,
|
||||
iceServers = iceServers,
|
||||
relay = useRelay
|
||||
)
|
||||
callInvitations.remove(invitation.contact.id)
|
||||
if (invitation.contact.id == activeCallInvitation.value?.contact?.id) {
|
||||
activeCallInvitation.value = null
|
||||
|
||||
@@ -3,11 +3,13 @@ package chat.simplex.app.views.call
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.SimplexApp
|
||||
import chat.simplex.app.model.Contact
|
||||
import chat.simplex.app.views.helpers.generalGetString
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.net.URI
|
||||
|
||||
data class Call(
|
||||
val contact: Contact,
|
||||
@@ -115,7 +117,7 @@ sealed class WCallResponse {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate
|
||||
@Serializable class RTCIceCandidate(val candidateType: RTCIceCandidateType?)
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer
|
||||
@Serializable class RTCIceServer(val urls: List<String>, val username: String? = null, val credential: String? = null)
|
||||
@Serializable data class RTCIceServer(val urls: List<String>, val username: String? = null, val credential: String? = null)
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate/type
|
||||
@Serializable
|
||||
@@ -153,4 +155,48 @@ class ConnectionState(
|
||||
val iceConnectionState: String,
|
||||
val iceGatheringState: String,
|
||||
val signalingState: String
|
||||
)
|
||||
)
|
||||
|
||||
// the servers are expected in this format:
|
||||
// stun:stun.simplex.im:443
|
||||
// turn:private:yleob6AVkiNI87hpR94Z@turn.simplex.im:443
|
||||
fun parseRTCIceServer(str: String): RTCIceServer? {
|
||||
var s = replaceScheme(str, "stun:")
|
||||
s = replaceScheme(s, "turn:")
|
||||
val u = runCatching { URI(s) }.getOrNull()
|
||||
if (u != null) {
|
||||
val scheme = u.scheme
|
||||
val host = u.host
|
||||
val port = u.port
|
||||
if (u.path == "" && (scheme == "stun" || scheme == "turn")) {
|
||||
val userInfo = u.userInfo?.split(":")
|
||||
return RTCIceServer(
|
||||
urls = listOf("$scheme:$host:$port"),
|
||||
username = userInfo?.getOrNull(0),
|
||||
credential = userInfo?.getOrNull(1)
|
||||
)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun replaceScheme(s: String, scheme: String): String = if (s.startsWith(scheme)) s.replace(scheme, "$scheme//") else s
|
||||
|
||||
fun parseRTCIceServers(servers: List<String>): List<RTCIceServer>? {
|
||||
val iceServers: ArrayList<RTCIceServer> = ArrayList()
|
||||
for (s in servers) {
|
||||
val server = parseRTCIceServer(s)
|
||||
if (server != null) {
|
||||
iceServers.add(server)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
return if (iceServers.isEmpty()) null else iceServers
|
||||
}
|
||||
|
||||
fun getIceServers(): List<RTCIceServer>? {
|
||||
val value = SimplexApp.context.chatController.appPrefs.webrtcIceServers.get() ?: return null
|
||||
val servers: List<String> = value.split("\n")
|
||||
return parseRTCIceServers(servers)
|
||||
}
|
||||
|
||||
@@ -6,8 +6,6 @@ import SectionView
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -19,10 +17,13 @@ import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.HighOrLowlight
|
||||
|
||||
@Composable
|
||||
fun CallSettingsView(m: ChatModel) {
|
||||
fun CallSettingsView(m: ChatModel,
|
||||
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
) {
|
||||
CallSettingsLayout(
|
||||
webrtcPolicyRelay = m.controller.appPrefs.webrtcPolicyRelay,
|
||||
callOnLockScreen = m.controller.appPrefs.callOnLockScreen
|
||||
callOnLockScreen = m.controller.appPrefs.callOnLockScreen,
|
||||
editIceServers = showModal { RTCServersView(m) }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -30,6 +31,7 @@ fun CallSettingsView(m: ChatModel) {
|
||||
fun CallSettingsLayout(
|
||||
webrtcPolicyRelay: Preference<Boolean>,
|
||||
callOnLockScreen: Preference<CallOnLockScreen>,
|
||||
editIceServers: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
@@ -58,6 +60,8 @@ fun CallSettingsLayout(
|
||||
SharedPreferenceRadioButton(stringResource(R.string.accept_call_on_lock_screen), lockCallState, callOnLockScreen, CallOnLockScreen.ACCEPT)
|
||||
}
|
||||
}
|
||||
SectionDivider()
|
||||
SectionItemView(editIceServers) { Text(stringResource(R.string.webrtc_ice_servers)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ fun NetworkAndServersView(
|
||||
Modifier.padding(start = 16.dp, bottom = 24.dp),
|
||||
style = MaterialTheme.typography.h1
|
||||
)
|
||||
SectionView {
|
||||
SectionView(generalGetString(R.string.settings_section_title_calls)) {
|
||||
SettingsActionItem(Icons.Outlined.Dns, stringResource(R.string.smp_servers), showModal { SMPServersView(it) })
|
||||
SectionDivider()
|
||||
SectionItemView {
|
||||
@@ -128,6 +128,10 @@ fun NetworkAndServersView(
|
||||
SettingsActionItem(Icons.Outlined.Cable, stringResource(R.string.network_settings), showSettingsModal { AdvancedNetworkSettingsView(it) })
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
SectionView(generalGetString(R.string.settings_section_title_calls)) {
|
||||
SettingsActionItem(Icons.Outlined.ElectricalServices, stringResource(R.string.webrtc_ice_servers), showModal { RTCServersView(it) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
package chat.simplex.app.views.usersettings
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.OpenInNew
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.ui.theme.HighOrLowlight
|
||||
import chat.simplex.app.views.call.parseRTCIceServers
|
||||
import chat.simplex.app.views.helpers.*
|
||||
|
||||
@Composable
|
||||
fun RTCServersView(
|
||||
chatModel: ChatModel
|
||||
) {
|
||||
var userRTCServers by remember {
|
||||
mutableStateOf(chatModel.controller.appPrefs.webrtcIceServers.get()?.split("\n") ?: listOf())
|
||||
}
|
||||
var isUserRTCServers by remember { mutableStateOf(userRTCServers.isNotEmpty()) }
|
||||
var editRTCServers by remember { mutableStateOf(!isUserRTCServers) }
|
||||
val userRTCServersStr = remember { mutableStateOf(if (isUserRTCServers) userRTCServers.joinToString(separator = "\n") else "") }
|
||||
fun saveUserRTCServers() {
|
||||
val srvs = userRTCServersStr.value.split("\n")
|
||||
if (srvs.isNotEmpty() && srvs.toSet().size == srvs.size && parseRTCIceServers(srvs) != null) {
|
||||
userRTCServers = srvs
|
||||
chatModel.controller.appPrefs.webrtcIceServers.set(srvs.joinToString(separator = "\n"))
|
||||
editRTCServers = false
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(R.string.error_saving_ICE_servers),
|
||||
generalGetString(R.string.ensure_ICE_server_address_are_correct_format_and_unique)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun resetRTCServers() {
|
||||
isUserRTCServers = false
|
||||
userRTCServers = listOf()
|
||||
chatModel.controller.appPrefs.webrtcIceServers.set(null)
|
||||
}
|
||||
|
||||
RTCServersLayout(
|
||||
isUserRTCServers = isUserRTCServers,
|
||||
editRTCServers = editRTCServers,
|
||||
userRTCServersStr = userRTCServersStr,
|
||||
isUserRTCServersOnOff = { switch ->
|
||||
if (switch) {
|
||||
isUserRTCServers = true
|
||||
} else if (userRTCServers.isNotEmpty()) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(R.string.use_simplex_chat_servers__question),
|
||||
text = generalGetString(R.string.saved_ICE_servers_will_be_removed),
|
||||
confirmText = generalGetString(R.string.confirm_verb),
|
||||
onConfirm = {
|
||||
resetRTCServers()
|
||||
isUserRTCServers = false
|
||||
userRTCServersStr.value = ""
|
||||
}
|
||||
)
|
||||
} else {
|
||||
isUserRTCServers = false
|
||||
userRTCServersStr.value = ""
|
||||
}
|
||||
},
|
||||
cancelEdit = {
|
||||
isUserRTCServers = userRTCServers.isNotEmpty()
|
||||
editRTCServers = !isUserRTCServers
|
||||
userRTCServersStr.value = if (isUserRTCServers) userRTCServers.joinToString(separator = "\n") else ""
|
||||
},
|
||||
saveRTCServers = ::saveUserRTCServers,
|
||||
editOn = { editRTCServers = true },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RTCServersLayout(
|
||||
isUserRTCServers: Boolean,
|
||||
editRTCServers: Boolean,
|
||||
userRTCServersStr: MutableState<String>,
|
||||
isUserRTCServersOnOff: (Boolean) -> Unit,
|
||||
cancelEdit: () -> Unit,
|
||||
saveRTCServers: () -> Unit,
|
||||
editOn: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.your_ICE_servers),
|
||||
Modifier.padding(bottom = 24.dp),
|
||||
style = MaterialTheme.typography.h1
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(stringResource(R.string.configure_ICE_servers), Modifier.padding(end = 24.dp))
|
||||
Switch(
|
||||
checked = isUserRTCServers,
|
||||
onCheckedChange = isUserRTCServersOnOff,
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colors.primary,
|
||||
uncheckedThumbColor = HighOrLowlight
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if (!isUserRTCServers) {
|
||||
Text(stringResource(R.string.using_simplex_chat_servers), lineHeight = 22.sp)
|
||||
} else {
|
||||
Text(stringResource(R.string.enter_one_ICE_server_per_line))
|
||||
if (editRTCServers) {
|
||||
TextEditor(Modifier.height(160.dp), text = userRTCServersStr)
|
||||
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.Start) {
|
||||
Row {
|
||||
Text(
|
||||
stringResource(R.string.cancel_verb),
|
||||
color = MaterialTheme.colors.primary,
|
||||
modifier = Modifier
|
||||
.clickable(onClick = cancelEdit)
|
||||
)
|
||||
Spacer(Modifier.padding(horizontal = 8.dp))
|
||||
Text(
|
||||
stringResource(R.string.save_servers_button),
|
||||
color = MaterialTheme.colors.primary,
|
||||
modifier = Modifier.clickable(onClick = {
|
||||
saveRTCServers()
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
howToButton()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.height(160.dp)
|
||||
.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
border = BorderStroke(1.dp, MaterialTheme.colors.secondary)
|
||||
) {
|
||||
SelectionContainer(
|
||||
Modifier.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Text(
|
||||
userRTCServersStr.value,
|
||||
Modifier
|
||||
.padding(vertical = 5.dp, horizontal = 7.dp),
|
||||
style = TextStyle(fontFamily = FontFamily.Monospace, fontSize = 14.sp),
|
||||
)
|
||||
}
|
||||
}
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.Start) {
|
||||
Text(
|
||||
stringResource(R.string.edit_verb),
|
||||
color = MaterialTheme.colors.primary,
|
||||
modifier = Modifier
|
||||
.clickable(onClick = editOn)
|
||||
)
|
||||
}
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
howToButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun howToButton() {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.clickable { uriHandler.openUri("https://github.com/simplex-chat/simplex-chat/blob/stable/docs/WEBRTC.md#configure-mobile-apps") }
|
||||
) {
|
||||
Text(stringResource(R.string.how_to), color = MaterialTheme.colors.primary)
|
||||
Icon(
|
||||
Icons.Outlined.OpenInNew, stringResource(R.string.how_to), tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier.padding(horizontal = 5.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ fun SMPServersView(chatModel: ChatModel) {
|
||||
if (userSMPServers.isNotEmpty()) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.use_simplex_chat_servers__question),
|
||||
text = generalGetString(R.string.saved_SMP_servers_will_br_removed),
|
||||
text = generalGetString(R.string.saved_SMP_servers_will_be_removed),
|
||||
confirmText = generalGetString(R.string.confirm_verb),
|
||||
onConfirm = {
|
||||
saveSMPServers(listOf())
|
||||
@@ -198,7 +198,7 @@ fun SMPServersLayout(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun howToButton() {
|
||||
private fun howToButton() {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
|
||||
@@ -122,7 +122,7 @@ fun SettingsLayout(
|
||||
SectionView(stringResource(R.string.settings_section_title_settings)) {
|
||||
SettingsActionItem(Icons.Outlined.Bolt, stringResource(R.string.notifications), showSettingsModal { NotificationsSettingsView(it, showCustomModal) })
|
||||
SectionDivider()
|
||||
SettingsActionItem(Icons.Outlined.Videocam, stringResource(R.string.settings_audio_video_calls), showSettingsModal { CallSettingsView(it) }, disabled = stopped)
|
||||
SettingsActionItem(Icons.Outlined.Videocam, stringResource(R.string.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped)
|
||||
SectionDivider()
|
||||
SettingsActionItem(Icons.Outlined.Lock, stringResource(R.string.privacy_and_security), showSettingsModal { PrivacySettingsView(it, setPerformLA) }, disabled = stopped)
|
||||
SectionDivider()
|
||||
|
||||
@@ -327,12 +327,18 @@
|
||||
<string name="smp_servers">SMP серверы</string>
|
||||
<string name="install_simplex_chat_for_terminal"><xliff:g id="appNameFull">SimpleX Chat</xliff:g> для терминала</string>
|
||||
<string name="use_simplex_chat_servers__question">Использовать серверы предосталенные <xliff:g id="appNameFull">SimpleX Chat</xliff:g>?</string>
|
||||
<string name="saved_SMP_servers_will_br_removed">Сохраненные SMP серверы будут удалены.</string>
|
||||
<string name="saved_SMP_servers_will_be_removed">Сохраненные SMP серверы будут удалены.</string>
|
||||
<string name="your_SMP_servers">Ваши SMP серверы</string>
|
||||
<string name="configure_SMP_servers">Настройка SMP серверов</string>
|
||||
<string name="using_simplex_chat_servers">Используются серверы предоставленные <xliff:g id="appNameFull">SimpleX Chat</xliff:g>.</string>
|
||||
<string name="enter_one_SMP_server_per_line">Введите SMP серверы, каждый сервер в отдельной строке:</string>
|
||||
<string name="how_to">Инфо</string>
|
||||
<string name="saved_ICE_servers_will_be_removed">Сохраненные WebRTC ICE серверы будут удалены.</string>
|
||||
<string name="your_ICE_servers">Ваши ICE серверы</string>
|
||||
<string name="configure_ICE_servers">Настройка ICE серверов</string>
|
||||
<string name="enter_one_ICE_server_per_line">ICE серверы (один на строке)</string>
|
||||
<string name="error_saving_ICE_servers">Ошибка при сохранении ICE серверов</string>
|
||||
<string name="ensure_ICE_server_address_are_correct_format_and_unique">Пожалуйста, проверьте, что адреса WebRTC ICE серверов имеют правильный формат, каждый адрес на отдельной строке и не повторяется.</string>
|
||||
<string name="save_servers_button">Сохранить</string>
|
||||
<string name="network_and_servers">Сеть и серверы</string>
|
||||
<string name="network_settings">Настройки сети</string>
|
||||
@@ -476,6 +482,8 @@
|
||||
<string name="accept_call_on_lock_screen">Принимать</string>
|
||||
<string name="show_call_on_lock_screen">Показывать</string>
|
||||
<string name="no_call_on_lock_screen">Выключить</string>
|
||||
<string name="your_ice_servers">Ваши ICE серверы</string>
|
||||
<string name="webrtc_ice_servers">WebRTC ICE серверы</string>
|
||||
|
||||
<!-- Call Lock Screen -->
|
||||
<string name="open_simplex_chat_to_accept_call">Откройте <xliff:g id="appNameFull">SimpleX Chat</xliff:g>\nчтобы принять звонок</string>
|
||||
@@ -533,6 +541,8 @@
|
||||
<string name="settings_section_title_socks">SOCKS ПРОКСИ</string>
|
||||
<string name="settings_section_title_icon">ИКОНКА</string>
|
||||
<string name="settings_section_title_themes">ТЕМЫ</string>
|
||||
<string name="settings_section_title_messages">СООБЩЕНИЯ</string>
|
||||
<string name="settings_section_title_calls">ЗВОНКИ</string>
|
||||
<string name="settings_section_title_incognito">Режим Инкогнито</string>
|
||||
|
||||
<!-- DatabaseView.kt -->
|
||||
|
||||
@@ -331,12 +331,18 @@
|
||||
<string name="smp_servers">SMP servers</string>
|
||||
<string name="install_simplex_chat_for_terminal">Install <xliff:g id="appNameFull">SimpleX Chat</xliff:g> for terminal</string>
|
||||
<string name="use_simplex_chat_servers__question">Use <xliff:g id="appNameFull">SimpleX Chat</xliff:g> servers?</string>
|
||||
<string name="saved_SMP_servers_will_br_removed">Saved SMP servers will be removed.</string>
|
||||
<string name="saved_SMP_servers_will_be_removed">Saved SMP servers will be removed.</string>
|
||||
<string name="your_SMP_servers">Your SMP servers</string>
|
||||
<string name="configure_SMP_servers">Configure SMP servers</string>
|
||||
<string name="using_simplex_chat_servers">Using <xliff:g id="appNameFull">SimpleX Chat</xliff:g> servers.</string>
|
||||
<string name="enter_one_SMP_server_per_line">Enter one SMP server per line:</string>
|
||||
<string name="how_to">How to</string>
|
||||
<string name="saved_ICE_servers_will_be_removed">Saved WebRTC ICE servers will be removed.</string>
|
||||
<string name="your_ICE_servers">Your ICE servers</string>
|
||||
<string name="configure_ICE_servers">Configure ICE servers</string>
|
||||
<string name="enter_one_ICE_server_per_line">ICE servers (one per line)</string>
|
||||
<string name="error_saving_ICE_servers">Error saving ICE servers</string>
|
||||
<string name="ensure_ICE_server_address_are_correct_format_and_unique">Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated.</string>
|
||||
<string name="save_servers_button">Save</string>
|
||||
<string name="network_and_servers">Network & servers</string>
|
||||
<string name="network_settings">Advanced network settings</string>
|
||||
@@ -477,6 +483,8 @@
|
||||
<string name="accept_call_on_lock_screen">Accept</string>
|
||||
<string name="show_call_on_lock_screen">Show</string>
|
||||
<string name="no_call_on_lock_screen">Disable</string>
|
||||
<string name="your_ice_servers">Your ICE servers</string>
|
||||
<string name="webrtc_ice_servers">WebRTC ICE servers</string>
|
||||
|
||||
<!-- Call Lock Screen -->
|
||||
<string name="open_simplex_chat_to_accept_call">Open <xliff:g id="appNameFull">SimpleX Chat</xliff:g> to accept call</string>
|
||||
@@ -534,6 +542,8 @@
|
||||
<string name="settings_section_title_socks">SOCKS PROXY</string>
|
||||
<string name="settings_section_title_icon">APP ICON</string>
|
||||
<string name="settings_section_title_themes">THEMES</string>
|
||||
<string name="settings_section_title_messages">MESSAGES</string>
|
||||
<string name="settings_section_title_calls">CALLS</string>
|
||||
<string name="settings_section_title_incognito">Incognito mode</string>
|
||||
|
||||
<!-- DatabaseView.kt -->
|
||||
|
||||
Reference in New Issue
Block a user