mobile: call settings, request camera on iOS on call start (#701)

* mobile: call settings, request camera on iOS on call start

* refactor preferences

* fix typo
This commit is contained in:
Evgeny Poberezkin 2022-05-27 16:36:33 +01:00 committed by GitHub
parent 79d9e90ab7
commit da13e6614b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 686 additions and 513 deletions

View file

@ -1,5 +1,8 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<ComposeCustomCodeStyleSettings>
<option name="USE_CUSTOM_FORMATTING_FOR_MODIFIERS" value="false" />
</ComposeCustomCodeStyleSettings>
<JetCodeStyleSettings>
<option name="SPACE_BEFORE_EXTEND_COLON" value="false" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="3" />
@ -123,9 +126,11 @@
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<option name="RIGHT_MARGIN" value="140" />
<option name="KEEP_FIRST_COLUMN_COMMENT" value="false" />
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="0" />
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="CALL_PARAMETERS_WRAP" value="0" />
<option name="METHOD_PARAMETERS_WRAP" value="0" />
<option name="EXTENDS_LIST_WRAP" value="0" />

View file

@ -1,6 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

View file

@ -96,8 +96,7 @@ const processCommand = (function () {
const pc = new RTCPeerConnection(config.peerConnectionConfig);
const remoteStream = new MediaStream();
const localCamera = VideoCamera.User;
const constraints = callMediaConstraints(mediaType, localCamera);
const localStream = await navigator.mediaDevices.getUserMedia(constraints);
const localStream = await getLocalMediaStream(mediaType, localCamera);
const iceCandidates = getIceCandidates(pc, config);
const call = { connection: pc, iceCandidates, localMedia: mediaType, localCamera, localStream, remoteStream, aesKey, useWorker };
await setupMediaStreams(call);
@ -156,11 +155,17 @@ const processCommand = (function () {
try {
switch (command.type) {
case "capabilities":
console.log("starting outgoing call - capabilities");
if (activeCall)
endCall();
// This request for local media stream is made to prompt for camera/mic permissions on call start
if (command.media)
await getLocalMediaStream(command.media, VideoCamera.User);
const encryption = supportsInsertableStreams(command.useWorker);
resp = { type: "capabilities", capabilities: { encryption } };
break;
case "start": {
console.log("starting call");
console.log("starting incoming call - create webrtc session");
if (activeCall)
endCall();
const { media, useWorker, iceServers, relay } = command;
@ -394,8 +399,7 @@ const processCommand = (function () {
for (const t of call.localStream.getTracks())
t.stop();
call.localCamera = camera;
const constraints = callMediaConstraints(call.localMedia, camera);
const localStream = await navigator.mediaDevices.getUserMedia(constraints);
const localStream = await getLocalMediaStream(call.localMedia, camera);
replaceTracks(pc, localStream.getVideoTracks());
replaceTracks(pc, localStream.getAudioTracks());
call.localStream = localStream;
@ -430,6 +434,10 @@ const processCommand = (function () {
console.log(`no ${operation}`);
}
}
function getLocalMediaStream(mediaType, facingMode) {
const constraints = callMediaConstraints(mediaType, facingMode);
return navigator.mediaDevices.getUserMedia(constraints);
}
function callMediaConstraints(mediaType, facingMode) {
switch (mediaType) {
case CallMediaType.Audio:

View file

@ -72,7 +72,7 @@ class MainActivity: FragmentActivity(), LifecycleEventObserver {
val m = vm.chatModel
val lastLAVal = lastLA.value
if (
m.controller.getPerformLA()
m.controller.prefPerformLA.get()
&& (lastLAVal == null || (System.nanoTime() - lastLAVal >= 30 * 1e+9))
) {
userAuthorized.value = false
@ -91,7 +91,7 @@ class MainActivity: FragmentActivity(), LifecycleEventObserver {
LAResult.Unavailable -> {
userAuthorized.value = true
m.performLA.value = false
m.controller.setPerformLA(false)
m.controller.prefPerformLA.set(false)
laUnavailableTurningOffAlert()
}
}
@ -106,13 +106,13 @@ class MainActivity: FragmentActivity(), LifecycleEventObserver {
}
private fun schedulePeriodicServiceRestartWorker() {
val workerVersion = chatController.getAutoRestartWorkerVersion()
val workerVersion = chatController.prefAutoRestartWorkerVersion.get()
val workPolicy = if (workerVersion == SimplexService.SERVICE_START_WORKER_VERSION) {
Log.d(TAG, "ServiceStartWorker version matches: choosing KEEP as existing work policy")
ExistingPeriodicWorkPolicy.KEEP
} else {
Log.d(TAG, "ServiceStartWorker version DOES NOT MATCH: choosing REPLACE as existing work policy")
chatController.setAutoRestartWorkerVersion(SimplexService.SERVICE_START_WORKER_VERSION)
chatController.prefAutoRestartWorkerVersion.set(SimplexService.SERVICE_START_WORKER_VERSION)
ExistingPeriodicWorkPolicy.REPLACE
}
val work = PeriodicWorkRequestBuilder<SimplexService.ServiceStartWorker>(SimplexService.SERVICE_START_WORKER_INTERVAL_MINUTES, TimeUnit.MINUTES)
@ -126,7 +126,7 @@ class MainActivity: FragmentActivity(), LifecycleEventObserver {
private fun setPerformLA(on: Boolean) {
val m = vm.chatModel
if (on) {
m.controller.setLANoticeShown(true)
m.controller.prefLANoticeShown.set(true)
authenticate(
generalGetString(R.string.auth_enable),
generalGetString(R.string.auth_confirm_credential),
@ -135,24 +135,24 @@ class MainActivity: FragmentActivity(), LifecycleEventObserver {
when (laResult) {
LAResult.Success -> {
m.performLA.value = true
m.controller.setPerformLA(true)
m.controller.prefPerformLA.set(true)
userAuthorized.value = true
lastLA.value = System.nanoTime()
laTurnedOnAlert()
}
is LAResult.Error -> {
m.performLA.value = false
m.controller.setPerformLA(false)
m.controller.prefPerformLA.set(false)
laErrorToast(applicationContext, laResult.errString)
}
LAResult.Failed -> {
m.performLA.value = false
m.controller.setPerformLA(false)
m.controller.prefPerformLA.set(false)
laFailedToast(applicationContext)
}
LAResult.Unavailable -> {
m.performLA.value = false
m.controller.setPerformLA(false)
m.controller.prefPerformLA.set(false)
laUnavailableInstructionAlert()
}
}
@ -167,21 +167,21 @@ class MainActivity: FragmentActivity(), LifecycleEventObserver {
when (laResult) {
LAResult.Success -> {
m.performLA.value = false
m.controller.setPerformLA(false)
m.controller.prefPerformLA.set(false)
}
is LAResult.Error -> {
m.performLA.value = true
m.controller.setPerformLA(true)
m.controller.prefPerformLA.set(true)
laErrorToast(applicationContext, laResult.errString)
}
LAResult.Failed -> {
m.performLA.value = true
m.controller.setPerformLA(true)
m.controller.prefPerformLA.set(true)
laFailedToast(applicationContext)
}
LAResult.Unavailable -> {
m.performLA.value = false
m.controller.setPerformLA(false)
m.controller.prefPerformLA.set(false)
laUnavailableTurningOffAlert()
}
}
@ -212,7 +212,7 @@ fun MainPage(
var showAdvertiseLAAlert by remember { mutableStateOf(false) }
LaunchedEffect(showAdvertiseLAAlert) {
if (
!chatModel.controller.getLANoticeShown()
!chatModel.controller.prefLANoticeShown.get()
&& showAdvertiseLAAlert
&& chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete
&& chatModel.chats.isNotEmpty()

View file

@ -61,7 +61,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
withApi {
when (event) {
Lifecycle.Event.ON_STOP ->
if (!chatController.getRunServiceInBackground()) SimplexService.stop(applicationContext)
if (!chatController.prefRunServiceInBackground.get()) SimplexService.stop(applicationContext)
Lifecycle.Event.ON_START ->
SimplexService.start(applicationContext)
Lifecycle.Event.ON_RESUME ->

View file

@ -50,9 +50,18 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
var chatModel = ChatModel(this)
private val sharedPreferences: SharedPreferences = appContext.getSharedPreferences(SHARED_PREFS_ID, Context.MODE_PRIVATE)
val prefRunServiceInBackground = mkBoolPreference(SHARED_PREFS_RUN_SERVICE_IN_BACKGROUND, true)
val prefBackgroundServiceNoticeShown = mkBoolPreference(SHARED_PREFS_SERVICE_NOTICE_SHOWN, false)
private val prefBackgroundServiceBatteryNoticeShown = mkBoolPreference(SHARED_PREFS_SERVICE_BATTERY_NOTICE_SHOWN, false)
val prefAutoRestartWorkerVersion = mkIntPreference(SHARED_PREFS_AUTO_RESTART_WORKER_VERSION, 0)
val prefWebrtcPolicyRelay = mkBoolPreference(SHARED_PREFS_WEBRTC_POLICY_RELAY, true)
val prefAcceptCallsFromLockScreen = mkBoolPreference(SHARED_PREFS_WEBRTC_ACCEPT_CALLS_FROM_LOCK_SCREEN, false)
val prefPerformLA = mkBoolPreference(SHARED_PREFS_PERFORM_LA, false)
val prefLANoticeShown = mkBoolPreference(SHARED_PREFS_LA_NOTICE_SHOWN, false)
init {
chatModel.runServiceInBackground.value = getRunServiceInBackground()
chatModel.performLA.value = getPerformLA()
chatModel.runServiceInBackground.value = prefRunServiceInBackground.get()
chatModel.performLA.value = prefPerformLA.get()
}
suspend fun startChat(user: User) {
@ -529,7 +538,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
if (invitation != null) {
chatModel.callManager.reportCallRemoteEnded(invitation = invitation)
}
withCall(r, r.contact) { call ->
withCall(r, r.contact) { _ ->
chatModel.callCommand.value = WCallCommand.End
withApi {
chatModel.activeCall.value = null
@ -583,7 +592,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
fun showBackgroundServiceNoticeIfNeeded() {
Log.d(TAG, "showBackgroundServiceNoticeIfNeeded")
if (!getBackgroundServiceNoticeShown()) {
if (!prefBackgroundServiceNoticeShown.get()) {
// the branch for the new users who has never seen service notice
if (isIgnoringBatteryOptimizations(appContext)) {
showBGServiceNotice()
@ -591,20 +600,20 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
showBGServiceNoticeIgnoreOptimization()
}
// set both flags, so that if the user doesn't allow ignoring optimizations, the service will be disabled without additional notice
setBackgroundServiceNoticeShown(true)
setBackgroundServiceBatteryNoticeShown(true)
} else if (!isIgnoringBatteryOptimizations(appContext) && getRunServiceInBackground()) {
prefBackgroundServiceNoticeShown.set(true)
prefBackgroundServiceBatteryNoticeShown.set(true)
} else if (!isIgnoringBatteryOptimizations(appContext) && prefRunServiceInBackground.get()) {
// the branch for users who have app installed, and have seen the service notice,
// but the battery optimization for the app is on (Android 12) AND the service is running
if (getBackgroundServiceBatteryNoticeShown()) {
if (prefBackgroundServiceBatteryNoticeShown.get()) {
// users have been presented with battery notice before - they did not allow ignoring optimizitions -> disable service
showDisablingServiceNotice()
setRunServiceInBackground(false)
prefRunServiceInBackground.set(false)
chatModel.runServiceInBackground.value = false
} else {
// show battery optimization notice
showBGServiceNoticeIgnoreOptimization()
setBackgroundServiceBatteryNoticeShown(true)
prefBackgroundServiceBatteryNoticeShown.set(true)
}
}
}
@ -695,8 +704,8 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
fun showLANotice(activity: FragmentActivity) {
Log.d(TAG, "showLANotice")
if (!getLANoticeShown()) {
setLANoticeShown(true)
if (!prefLANoticeShown.get()) {
prefLANoticeShown.set(true)
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.la_notice_title),
text = generalGetString(R.string.la_notice_text),
@ -710,22 +719,22 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
when (laResult) {
LAResult.Success -> {
chatModel.performLA.value = true
setPerformLA(true)
prefPerformLA.set(true)
laTurnedOnAlert()
}
is LAResult.Error -> {
chatModel.performLA.value = false
setPerformLA(false)
prefPerformLA.set(false)
laErrorToast(appContext, laResult.errString)
}
LAResult.Failed -> {
chatModel.performLA.value = false
setPerformLA(false)
prefPerformLA.set(false)
laFailedToast(appContext)
}
LAResult.Unavailable -> {
chatModel.performLA.value = false
setPerformLA(false)
prefPerformLA.set(false)
chatModel.showAdvertiseLAUnavailableAlert.value = true
}
}
@ -736,34 +745,6 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
}
}
fun getAutoRestartWorkerVersion(): Int = sharedPreferences.getInt(SHARED_PREFS_AUTO_RESTART_WORKER_VERSION, 0)
fun setAutoRestartWorkerVersion(version: Int) =
sharedPreferences.edit()
.putInt(SHARED_PREFS_AUTO_RESTART_WORKER_VERSION, version)
.apply()
fun getRunServiceInBackground(): Boolean = sharedPreferences.getBoolean(SHARED_PREFS_RUN_SERVICE_IN_BACKGROUND, true)
fun setRunServiceInBackground(runService: Boolean) =
sharedPreferences.edit()
.putBoolean(SHARED_PREFS_RUN_SERVICE_IN_BACKGROUND, runService)
.apply()
private fun getBackgroundServiceNoticeShown(): Boolean = sharedPreferences.getBoolean(SHARED_PREFS_SERVICE_NOTICE_SHOWN, false)
fun setBackgroundServiceNoticeShown(shown: Boolean) =
sharedPreferences.edit()
.putBoolean(SHARED_PREFS_SERVICE_NOTICE_SHOWN, shown)
.apply()
private fun getBackgroundServiceBatteryNoticeShown(): Boolean = sharedPreferences.getBoolean(SHARED_PREFS_SERVICE_BATTERY_NOTICE_SHOWN, false)
fun setBackgroundServiceBatteryNoticeShown(shown: Boolean) =
sharedPreferences.edit()
.putBoolean(SHARED_PREFS_SERVICE_BATTERY_NOTICE_SHOWN, shown)
.apply()
fun isIgnoringBatteryOptimizations(context: Context): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return true
val powerManager = context.getSystemService(Application.POWER_SERVICE) as PowerManager
@ -783,19 +764,17 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
}
}
fun getPerformLA(): Boolean = sharedPreferences.getBoolean(SHARED_PREFS_PERFORM_LA, false)
private fun mkIntPreference(prefName: String, default: Int) =
Preference(
get = fun() = sharedPreferences.getInt(prefName, default),
set = fun(value) = sharedPreferences.edit().putInt(prefName, value).apply()
)
fun setPerformLA(performLA: Boolean) =
sharedPreferences.edit()
.putBoolean(SHARED_PREFS_PERFORM_LA, performLA)
.apply()
fun getLANoticeShown(): Boolean = sharedPreferences.getBoolean(SHARED_PREFS_LA_NOTICE_SHOWN, false)
fun setLANoticeShown(shown: Boolean) =
sharedPreferences.edit()
.putBoolean(SHARED_PREFS_LA_NOTICE_SHOWN, shown)
.apply()
private fun mkBoolPreference(prefName: String, default: Boolean) =
Preference(
get = fun() = sharedPreferences.getBoolean(prefName, default),
set = fun(value) = sharedPreferences.edit().putBoolean(prefName, value).apply()
)
companion object {
private const val SHARED_PREFS_ID = "chat.simplex.app.SIMPLEX_APP_PREFS"
@ -803,11 +782,15 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
private const val SHARED_PREFS_RUN_SERVICE_IN_BACKGROUND = "RunServiceInBackground"
private const val SHARED_PREFS_SERVICE_NOTICE_SHOWN = "BackgroundServiceNoticeShown"
private const val SHARED_PREFS_SERVICE_BATTERY_NOTICE_SHOWN = "BackgroundServiceBatteryNoticeShown"
private const val SHARED_PREFS_WEBRTC_POLICY_RELAY = "WebrtcPolicyRelay"
private const val SHARED_PREFS_WEBRTC_ACCEPT_CALLS_FROM_LOCK_SCREEN = "AcceptCallsFromLockScreen"
private const val SHARED_PREFS_PERFORM_LA = "PerformLA"
private const val SHARED_PREFS_LA_NOTICE_SHOWN = "LANoticeShown"
}
}
class Preference<T>(val get: () -> T, val set: (T) -> Unit)
// ChatCommand
sealed class CC {
class Console(val cmd: String): CC()

View file

@ -156,7 +156,7 @@ private fun ActiveCallOverlayLayout(
ControlButton(call, Icons.Filled.FlipCameraAndroid, R.string.icon_descr_flip_camera, flipCamera)
ControlButton(call, Icons.Filled.Videocam, R.string.icon_descr_video_off, toggleVideo)
} else {
Spacer(Modifier.size(40.dp))
Spacer(Modifier.size(48.dp))
ControlButton(call, Icons.Outlined.VideocamOff, R.string.icon_descr_video_on, toggleVideo)
}
}

View file

@ -11,7 +11,6 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
@ -25,14 +24,15 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
import chat.simplex.app.model.Contact
import chat.simplex.app.model.NtfManager.Companion.OpenChatAction
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.ProfileImage
import chat.simplex.app.views.onboarding.SimpleXLogo
@ -93,7 +93,7 @@ fun IncomingCallActivityView(m: ChatModel, activity: IncomingCallActivity) {
activity.finish()
}
}
SimpleXTheme(darkTheme = true) {
SimpleXTheme {
Surface(
Modifier
.background(MaterialTheme.colors.background)
@ -104,33 +104,48 @@ fun IncomingCallActivityView(m: ChatModel, activity: IncomingCallActivity) {
if (invitation != null) IncomingCallAlertView(invitation, m)
}
} else if (invitation != null) {
IncomingCallLockScreenAlert(invitation, m)
IncomingCallLockScreenAlert(invitation, m, activity)
}
}
}
}
@Composable
fun IncomingCallLockScreenAlert(invitation: CallInvitation, chatModel: ChatModel) {
fun IncomingCallLockScreenAlert(invitation: CallInvitation, chatModel: ChatModel, activity: IncomingCallActivity) {
val cm = chatModel.callManager
val cxt = LocalContext.current
val scope = rememberCoroutineScope()
var acceptCallsFromLockScreen by remember { mutableStateOf(chatModel.controller.prefAcceptCallsFromLockScreen.get()) }
LaunchedEffect(true) { SoundPlayer.shared.start(cxt, scope, sound = true) }
DisposableEffect(true) { onDispose { SoundPlayer.shared.stop() } }
IncomingCallLockScreenAlertLayout(
invitation,
acceptCallsFromLockScreen,
rejectCall = { cm.endCall(invitation = invitation) },
ignoreCall = { chatModel.activeCallInvitation.value = null },
acceptCall = { cm.acceptIncomingCall(invitation = invitation) }
acceptCall = { cm.acceptIncomingCall(invitation = invitation) },
openApp = {
SoundPlayer.shared.stop()
var intent = Intent(activity, MainActivity::class.java)
.setAction(OpenChatAction)
.putExtra("chatId", invitation.contact.id)
activity.startActivity(intent)
activity.finish()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
getKeyguardManager(activity).requestDismissKeyguard(activity, null)
}
}
)
}
@Composable
fun IncomingCallLockScreenAlertLayout(
invitation: CallInvitation,
acceptCallsFromLockScreen: Boolean,
rejectCall: () -> Unit,
ignoreCall: () -> Unit,
acceptCall: () -> Unit
acceptCall: () -> Unit,
openApp: () -> Unit
) {
Column(
Modifier
@ -139,22 +154,24 @@ fun IncomingCallLockScreenAlertLayout(
horizontalAlignment = Alignment.CenterHorizontally
) {
IncomingCallInfo(invitation)
Spacer(
Modifier
.fillMaxHeight()
.weight(1f))
ProfileImage(size = 192.dp, image = invitation.contact.profile.image)
Text(invitation.contact.chatViewName, style = MaterialTheme.typography.h2)
Spacer(
Modifier
.fillMaxHeight()
.weight(1f))
Row {
LockScreenCallButton(stringResource(R.string.reject), Icons.Filled.CallEnd, Color.Red, rejectCall)
Spacer(Modifier.size(48.dp))
LockScreenCallButton(stringResource(R.string.ignore), Icons.Filled.Close, MaterialTheme.colors.primary, ignoreCall)
Spacer(Modifier.size(48.dp))
LockScreenCallButton(stringResource(R.string.accept), Icons.Filled.Check, SimplexGreen, acceptCall)
Spacer(Modifier.fillMaxHeight().weight(1f))
if (acceptCallsFromLockScreen) {
ProfileImage(size = 192.dp, image = invitation.contact.profile.image)
Text(invitation.contact.chatViewName, style = MaterialTheme.typography.h2)
Spacer(Modifier.fillMaxHeight().weight(1f))
Row {
LockScreenCallButton(stringResource(R.string.reject), Icons.Filled.CallEnd, Color.Red, rejectCall)
Spacer(Modifier.size(48.dp))
LockScreenCallButton(stringResource(R.string.ignore), Icons.Filled.Close, MaterialTheme.colors.primary, ignoreCall)
Spacer(Modifier.size(48.dp))
LockScreenCallButton(stringResource(R.string.accept), Icons.Filled.Check, SimplexGreen, acceptCall)
}
} else {
SimpleXLogo()
Text(stringResource(R.string.open_simplex_chat_to_accept_call), textAlign = TextAlign.Center, lineHeight = 22.sp)
Text(stringResource(R.string.allow_accepting_calls_from_lock_screen), textAlign = TextAlign.Center, style = MaterialTheme.typography.body2, lineHeight = 22.sp)
Spacer(Modifier.fillMaxHeight().weight(1f))
SimpleButton(text = stringResource(R.string.open_verb), icon = Icons.Filled.Check, click = openApp)
}
}
}
@ -166,7 +183,9 @@ private fun LockScreenCallButton(text: String, icon: ImageVector, color: Color,
color = Color.Transparent
) {
Column(
Modifier.defaultMinSize(minWidth = 50.dp).padding(4.dp),
Modifier
.defaultMinSize(minWidth = 50.dp)
.padding(4.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
IconButton(action) {
@ -185,16 +204,21 @@ private fun LockScreenCallButton(text: String, icon: ImageVector, color: Color,
@Composable
fun PreviewIncomingCallLockScreenAlert() {
SimpleXTheme(true) {
Surface(Modifier.background(MaterialTheme.colors.background).fillMaxSize()) {
Surface(
Modifier
.background(MaterialTheme.colors.background)
.fillMaxSize()) {
IncomingCallLockScreenAlertLayout(
invitation = CallInvitation(
contact = Contact.sampleData,
peerMedia = CallMediaType.Audio,
sharedKey = null
),
acceptCallsFromLockScreen = false,
rejectCall = {},
ignoreCall = {},
acceptCall = {}
acceptCall = {},
openApp = {},
)
}
}

View file

@ -0,0 +1,63 @@
package chat.simplex.app.views.usersettings
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.HighOrLowlight
@Composable
fun CallSettingsView(m: ChatModel) {
CallSettingsLayout(
webrtcPolicyRelay = m.controller.prefWebrtcPolicyRelay,
acceptCallsFromLockScreen = m.controller.prefAcceptCallsFromLockScreen
)
}
@Composable
fun CallSettingsLayout(
webrtcPolicyRelay: Preference<Boolean>,
acceptCallsFromLockScreen: Preference<Boolean>,
) {
Column(
Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
stringResource(R.string.call_settings),
Modifier.padding(bottom = 24.dp),
style = MaterialTheme.typography.h1
)
SharedPreferenceToggle(stringResource(R.string.connect_calls_via_relay), webrtcPolicyRelay)
SharedPreferenceToggle(stringResource(R.string.accept_calls_from_lock_screen), acceptCallsFromLockScreen)
}
}
@Composable
fun SharedPreferenceToggle(
text: String,
preference: Preference<Boolean>
) {
var preferenceState by remember { mutableStateOf(preference.get()) }
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Text(text, Modifier.padding(end = 24.dp))
Spacer(Modifier.fillMaxWidth().weight(1f))
Switch(
checked = preferenceState,
onCheckedChange = {
preference.set(it)
preferenceState = it
},
colors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colors.primary,
uncheckedThumbColor = HighOrLowlight
),
)
}
}

View file

@ -1,8 +1,9 @@
package chat.simplex.app.views.usersettings
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.*
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
@ -12,6 +13,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.UriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
@ -32,9 +34,9 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
val user = chatModel.currentUser.value
fun setRunServiceInBackground(on: Boolean) {
chatModel.controller.setRunServiceInBackground(on)
chatModel.controller.prefRunServiceInBackground.set(on)
if (on && !chatModel.controller.isIgnoringBatteryOptimizations(chatModel.controller.appContext)) {
chatModel.controller.setBackgroundServiceNoticeShown(false)
chatModel.controller.prefBackgroundServiceNoticeShown.set(false)
}
chatModel.controller.showBackgroundServiceNoticeIfNeeded()
chatModel.runServiceInBackground.value = on
@ -75,6 +77,7 @@ fun SettingsLayout(
Modifier
.background(MaterialTheme.colors.background)
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
Column(
Modifier
@ -83,6 +86,7 @@ fun SettingsLayout(
.padding(8.dp)
.padding(top = 16.dp)
) {
@Composable fun divider() = Divider(Modifier.padding(horizontal = 8.dp))
Text(
stringResource(R.string.your_settings),
style = MaterialTheme.typography.h1,
@ -93,150 +97,219 @@ fun SettingsLayout(
SettingsSectionView(showCustomModal { chatModel, close -> UserProfileView(chatModel, close) }, 80.dp) {
ProfilePreview(profile)
}
Divider(Modifier.padding(horizontal = 8.dp))
SettingsSectionView(showModal { UserAddressView(it) }) {
Icon(
Icons.Outlined.QrCode,
contentDescription = stringResource(R.string.icon_descr_address),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(stringResource(R.string.your_simplex_contact_address))
}
divider()
UserAddressSection(showModal)
Spacer(Modifier.height(24.dp))
SettingsSectionView(showModal { HelpView(it) }) {
Icon(
Icons.Outlined.HelpOutline,
contentDescription = stringResource(R.string.icon_descr_help),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(stringResource(R.string.how_to_use_simplex_chat))
}
Divider(Modifier.padding(horizontal = 8.dp))
SettingsSectionView(showModal { SimpleXInfo(it, onboarding = false) }) {
Icon(
Icons.Outlined.Info,
contentDescription = stringResource(R.string.icon_descr_help),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(stringResource(R.string.about_simplex_chat))
}
Divider(Modifier.padding(horizontal = 8.dp))
SettingsSectionView(showModal { MarkdownHelpView() }) {
Icon(
Icons.Outlined.TextFormat,
contentDescription = stringResource(R.string.markdown_help),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(stringResource(R.string.markdown_in_messages))
}
Divider(Modifier.padding(horizontal = 8.dp))
SettingsSectionView({ uriHandler.openUri(simplexTeamUri) }) {
Icon(
Icons.Outlined.Tag,
contentDescription = stringResource(R.string.icon_descr_simplex_team),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(
stringResource(R.string.chat_with_the_founder),
color = MaterialTheme.colors.primary
)
}
Divider(Modifier.padding(horizontal = 8.dp))
SettingsSectionView({ uriHandler.openUri("mailto:chat@simplex.chat") }) {
Icon(
Icons.Outlined.Email,
contentDescription = stringResource(R.string.icon_descr_email),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(
stringResource(R.string.send_us_an_email),
color = MaterialTheme.colors.primary
)
}
CallSettingsSection(showModal)
divider()
ChatLockSection(performLA, setPerformLA)
divider()
PrivateNotificationsSection(runServiceInBackground, setRunServiceInBackground)
divider()
SMPServersSection(showModal)
Spacer(Modifier.height(24.dp))
SettingsSectionView(showModal { SMPServersView(it) }) {
Icon(
Icons.Outlined.Dns,
contentDescription = stringResource(R.string.smp_servers),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(stringResource(R.string.smp_servers))
}
Divider(Modifier.padding(horizontal = 8.dp))
SettingsSectionView() {
Icon(
Icons.Outlined.Bolt,
contentDescription = stringResource(R.string.private_notifications),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(
stringResource(R.string.private_notifications), Modifier
.padding(end = 24.dp)
.fillMaxWidth()
.weight(1F)
)
Switch(
checked = runServiceInBackground.value,
onCheckedChange = { setRunServiceInBackground(it) },
colors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colors.primary,
uncheckedThumbColor = HighOrLowlight
),
modifier = Modifier.padding(end = 8.dp)
)
}
Divider(Modifier.padding(horizontal = 8.dp))
SettingsSectionView() {
Icon(
Icons.Outlined.Lock,
contentDescription = stringResource(R.string.chat_lock),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(
stringResource(R.string.chat_lock), Modifier
.padding(end = 24.dp)
.fillMaxWidth()
.weight(1F)
)
Switch(
checked = performLA.value,
onCheckedChange = { setPerformLA(it) },
colors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colors.primary,
uncheckedThumbColor = HighOrLowlight
),
modifier = Modifier.padding(end = 8.dp)
)
}
Divider(Modifier.padding(horizontal = 8.dp))
SettingsSectionView(showTerminal) {
Icon(
painter = painterResource(id = R.drawable.ic_outline_terminal),
contentDescription = stringResource(R.string.chat_console),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(stringResource(R.string.chat_console))
}
Divider(Modifier.padding(horizontal = 8.dp))
SettingsSectionView({ uriHandler.openUri("https://github.com/simplex-chat/simplex-chat") }) {
Icon(
painter = painterResource(id = R.drawable.ic_github),
contentDescription = "GitHub",
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(annotatedStringResource(R.string.install_simplex_chat_for_terminal))
}
Divider(Modifier.padding(horizontal = 8.dp))
// SettingsSectionView(showVideoChatPrototype) {
SettingsSectionView() {
Text("v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
}
HelpViewSection(showModal)
divider()
SimpleXInfoSection(showModal)
divider()
MarkdownHelpSection(showModal)
divider()
ConnectToDevelopersSection(uriHandler)
divider()
SendEmailSection(uriHandler)
Spacer(Modifier.height(24.dp))
ChatConsoleSection(showTerminal)
divider()
InstallTerminalAppSection(uriHandler)
divider()
AppVersionSection()
}
}
}
@Composable private fun UserAddressSection(showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)) {
SettingsSectionView(showModal { UserAddressView(it) }) {
Icon(
Icons.Outlined.QrCode,
contentDescription = stringResource(R.string.icon_descr_address),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(stringResource(R.string.your_simplex_contact_address))
}
}
@Composable private fun CallSettingsSection(showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)) {
SettingsSectionView(showModal { CallSettingsView(it) }) {
Icon(
Icons.Outlined.Videocam,
contentDescription = stringResource(R.string.call_settings),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(stringResource(R.string.call_settings))
}
}
@Composable private fun HelpViewSection(showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)) {
SettingsSectionView(showModal { HelpView(it) }) {
Icon(
Icons.Outlined.HelpOutline,
contentDescription = stringResource(R.string.icon_descr_help),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(stringResource(R.string.how_to_use_simplex_chat))
}
}
@Composable private fun SimpleXInfoSection(showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)) {
SettingsSectionView(showModal { SimpleXInfo(it, onboarding = false) }) {
Icon(
Icons.Outlined.Info,
contentDescription = stringResource(R.string.icon_descr_help),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(stringResource(R.string.about_simplex_chat))
}
}
@Composable private fun MarkdownHelpSection(showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)) {
SettingsSectionView(showModal { MarkdownHelpView() }) {
Icon(
Icons.Outlined.TextFormat,
contentDescription = stringResource(R.string.markdown_help),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(stringResource(R.string.markdown_in_messages))
}
}
@Composable private fun ConnectToDevelopersSection(uriHandler: UriHandler) {
SettingsSectionView({ uriHandler.openUri(simplexTeamUri) }) {
Icon(
Icons.Outlined.Tag,
contentDescription = stringResource(R.string.icon_descr_simplex_team),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(
stringResource(R.string.chat_with_the_founder),
color = MaterialTheme.colors.primary
)
}
}
@Composable private fun SendEmailSection(uriHandler: UriHandler) {
SettingsSectionView({ uriHandler.openUri("mailto:chat@simplex.chat") }) {
Icon(
Icons.Outlined.Email,
contentDescription = stringResource(R.string.icon_descr_email),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(
stringResource(R.string.send_us_an_email),
color = MaterialTheme.colors.primary
)
}
}
@Composable private fun SMPServersSection(showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)) {
SettingsSectionView(showModal { SMPServersView(it) }) {
Icon(
Icons.Outlined.Dns,
contentDescription = stringResource(R.string.smp_servers),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(stringResource(R.string.smp_servers))
}
}
@Composable private fun PrivateNotificationsSection(
runServiceInBackground: MutableState<Boolean>,
setRunServiceInBackground: (Boolean) -> Unit
) {
SettingsSectionView() {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
Icons.Outlined.Bolt,
contentDescription = stringResource(R.string.private_notifications),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(
stringResource(R.string.private_notifications),
Modifier
.padding(end = 24.dp)
.fillMaxWidth()
.weight(1f)
)
Switch(
checked = runServiceInBackground.value,
onCheckedChange = { setRunServiceInBackground(it) },
colors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colors.primary,
uncheckedThumbColor = HighOrLowlight
),
modifier = Modifier.padding(end = 8.dp)
)
}
}
}
@Composable private fun ChatLockSection(performLA: MutableState<Boolean>, setPerformLA: (Boolean) -> Unit) {
SettingsSectionView() {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
Icons.Outlined.Lock,
contentDescription = stringResource(R.string.chat_lock),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(
stringResource(R.string.chat_lock), Modifier
.padding(end = 24.dp)
.fillMaxWidth()
.weight(1F)
)
Switch(
checked = performLA.value,
onCheckedChange = { setPerformLA(it) },
colors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colors.primary,
uncheckedThumbColor = HighOrLowlight
),
modifier = Modifier.padding(end = 8.dp)
)
}
}
}
@Composable private fun ChatConsoleSection(showTerminal: () -> Unit) {
SettingsSectionView(showTerminal) {
Icon(
painter = painterResource(id = R.drawable.ic_outline_terminal),
contentDescription = stringResource(R.string.chat_console),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(stringResource(R.string.chat_console))
}
}
@Composable private fun InstallTerminalAppSection(uriHandler: UriHandler) {
SettingsSectionView({ uriHandler.openUri("https://github.com/simplex-chat/simplex-chat") }) {
Icon(
painter = painterResource(id = R.drawable.ic_github),
contentDescription = "GitHub",
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(annotatedStringResource(R.string.install_simplex_chat_for_terminal))
}
}
@Composable private fun AppVersionSection() {
SettingsSectionView() {
Text("v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
}
}
@Composable fun ProfilePreview(profileOf: NamedChat, size: Dp = 60.dp, color: Color = MaterialTheme.colors.secondary) {
ProfileImage(size = size, image = profileOf.image, color = color)
Spacer(Modifier.padding(horizontal = 4.dp))

View file

@ -377,6 +377,16 @@
<string name="icon_descr_video_call">видеозвонок</string>
<string name="icon_descr_audio_call">аудиозвонок</string>
<!-- Call settings -->
<string name="call_settings">Настройки звонков</string>
<string name="connect_calls_via_relay">Соединяться через сервер (relay)</string>
<string name="accept_calls_from_lock_screen">Принимать с экрана блокировки</string>
<!-- Call Lock Screen -->
<string name="open_simplex_chat_to_accept_call">Open <xliff:g id="appNameFull">SimpleX Chat</xliff:g>\nto accept call</string>
<string name="allow_accepting_calls_from_lock_screen">You can allow accepting calls from lock screen via Settings.</string>
<string name="open_verb">Open</string>
<!-- Call overlay -->
<string name="status_e2e_encrypted">e2e зашифровано</string>
<string name="status_no_e2e_encryption">нет e2e шифрования</string>

View file

@ -379,6 +379,16 @@
<string name="icon_descr_video_call">video call</string>
<string name="icon_descr_audio_call">audio call</string>
<!-- Call settings -->
<string name="call_settings">Call settings</string>
<string name="connect_calls_via_relay">Connect via relay</string>
<string name="accept_calls_from_lock_screen">Accept calls from lock screen</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>
<string name="allow_accepting_calls_from_lock_screen">Enable calls from lock screen via Settings.</string>
<string name="open_verb">Open</string>
<!-- Call overlay -->
<string name="status_e2e_encrypted">e2e encrypted</string>
<string name="status_no_e2e_encryption">no e2e encryption</string>

View file

@ -660,7 +660,9 @@ func processReceivedMsg(_ res: ChatResponse) {
call.callState = .offerReceived
call.peerMedia = callType.media
call.sharedKey = sharedKey
m.callCommand = .offer(offer: offer.rtcSession, iceCandidates: offer.rtcIceCandidates, media: callType.media, aesKey: sharedKey, useWorker: true)
let useRelay = UserDefaults.standard.bool(forKey: DEFAULT_WEBRTC_POLICY_RELAY)
logger.debug(".callOffer useRelay \(useRelay)")
m.callCommand = .offer(offer: offer.rtcSession, iceCandidates: offer.rtcIceCandidates, media: callType.media, aesKey: sharedKey, useWorker: true, relay: useRelay)
}
case let .callAnswer(contact, answer):
withCall(contact) { call in

View file

@ -21,6 +21,7 @@ struct SimpleXApp: App {
init() {
hs_init(0, nil)
UserDefaults.standard.register(defaults: appDefaults)
BGManager.shared.register()
NtfManager.shared.registerCategories()
}

View file

@ -19,7 +19,7 @@ class CallManager {
let m = ChatModel.shared
if let call = m.activeCall, call.callkitUUID == callUUID {
m.showCallView = true
m.callCommand = .capabilities(useWorker: true)
m.callCommand = .capabilities(media: call.localMedia, useWorker: true)
return true
}
return false
@ -45,7 +45,9 @@ class CallManager {
sharedKey: invitation.sharedKey
)
m.showCallView = true
m.callCommand = .start(media: invitation.peerMedia, aesKey: invitation.sharedKey, useWorker: true)
let useRelay = UserDefaults.standard.bool(forKey: DEFAULT_WEBRTC_POLICY_RELAY)
logger.debug("answerIncomingCall useRelay \(useRelay)")
m.callCommand = .start(media: invitation.peerMedia, aesKey: invitation.sharedKey, useWorker: true, relay: useRelay)
}
func endCall(callUUID: UUID, completed: @escaping (Bool) -> Void) {

View file

@ -102,7 +102,7 @@ struct WVAPIMessage: Equatable, Decodable, Encodable {
}
enum WCallCommand: Equatable, Encodable, Decodable {
case capabilities(useWorker: Bool? = nil)
case capabilities(media: CallMediaType, useWorker: Bool? = nil)
case start(media: CallMediaType, aesKey: String? = nil, useWorker: Bool? = nil, iceServers: [RTCIceServer]? = nil, relay: Bool? = nil)
case offer(offer: String, iceCandidates: String, media: CallMediaType, aesKey: String? = nil, useWorker: Bool? = nil, iceServers: [RTCIceServer]? = nil, relay: Bool? = nil)
case answer(answer: String, iceCandidates: String)
@ -143,8 +143,9 @@ enum WCallCommand: Equatable, Encodable, Decodable {
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case let .capabilities(useWorker):
case let .capabilities(media, useWorker):
try container.encode("capabilities", forKey: .type)
try container.encode(media, forKey: .media)
try container.encode(useWorker, forKey: .useWorker)
case let .start(media, aesKey, useWorker, iceServers, relay):
try container.encode("start", forKey: .type)
@ -186,8 +187,9 @@ enum WCallCommand: Equatable, Encodable, Decodable {
let type = try container.decode(String.self, forKey: CodingKeys.type)
switch type {
case "capabilities":
let media = try container.decode(CallMediaType.self, forKey: CodingKeys.media)
let useWorker = try container.decode((Bool?).self, forKey: CodingKeys.useWorker)
self = .capabilities(useWorker: useWorker)
self = .capabilities(media: media, useWorker: useWorker)
case "start":
let media = try container.decode(CallMediaType.self, forKey: CodingKeys.media)
let aesKey = try? container.decode(String.self, forKey: CodingKeys.aesKey)

View file

@ -0,0 +1,25 @@
//
// CallSettings.swift
// SimpleX (iOS)
//
// Created by Evgeny on 27/05/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
struct CallSettings: View {
@AppStorage(DEFAULT_WEBRTC_POLICY_RELAY) private var webrtcPolicyRelay = true
var body: some View {
List {
Toggle("Connect via relay", isOn: $webrtcPolicyRelay)
}
}
}
struct CallSettings_Previews: PreviewProvider {
static var previews: some View {
CallSettings()
}
}

View file

@ -16,6 +16,13 @@ let appBuild = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as?
let DEFAULT_USE_NOTIFICATIONS = "useNotifications"
let DEFAULT_PENDING_CONNECTIONS = "pendingConnections"
let DEFAULT_WEBRTC_POLICY_RELAY = "webrtcPolicyRelay"
let appDefaults: [String:Any] = [
DEFAULT_USE_NOTIFICATIONS: false,
DEFAULT_PENDING_CONNECTIONS: true,
DEFAULT_WEBRTC_POLICY_RELAY: true
]
private var indent: CGFloat = 36
@ -59,6 +66,12 @@ struct SettingsView: View {
} label: {
settingsRow("server.rack") { Text("SMP servers") }
}
NavigationLink {
CallSettings()
.navigationTitle("Call settings")
} label: {
settingsRow("video") { Text("Call settings") }
}
}
Section("Help") {

View file

@ -20,11 +20,6 @@
<target> (can be copied)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id=" wants to connect with you via " xml:space="preserve">
<source> wants to connect with you via </source>
<target> wants to connect with you via </target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="!1 colored!" xml:space="preserve">
<source>!1 colored!</source>
<target>!1 colored!</target>
@ -133,7 +128,8 @@
<trans-unit id="Accept" xml:space="preserve">
<source>Accept</source>
<target>Accept</target>
<note>accept contact request via notification</note>
<note>accept contact request via notification
accept incoming call via notification</note>
</trans-unit>
<trans-unit id="Accept contact" xml:space="preserve">
<source>Accept contact</source>
@ -165,11 +161,6 @@
<target>Already connected?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Answer" xml:space="preserve">
<source>Answer</source>
<target>Answer</target>
<note>accept incoming call via notification</note>
</trans-unit>
<trans-unit id="Answer call" xml:space="preserve">
<source>Answer call</source>
<target>Answer call</target>
@ -185,16 +176,16 @@
<target>Call already ended!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Call settings" xml:space="preserve">
<source>Call settings</source>
<target>Call settings</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Cancel" xml:space="preserve">
<source>Cancel</source>
<target>Cancel</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Capabilities" xml:space="preserve">
<source>Capabilities</source>
<target>Capabilities</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat console" xml:space="preserve">
<source>Chat console</source>
<target>Chat console</target>
@ -275,6 +266,11 @@
<target>Connect via one-time link?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via relay" xml:space="preserve">
<source>Connect via relay</source>
<target>Connect via relay</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect with the developers" xml:space="preserve">
<source>Connect with the developers</source>
<target>Connect with the developers</target>
@ -460,11 +456,6 @@
<target>Enable notifications? (BETA)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="End" xml:space="preserve">
<source>End</source>
<target>End</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enter one SMP server per line:" xml:space="preserve">
<source>Enter one SMP server per line:</source>
<target>Enter one SMP server per line:</target>
@ -540,11 +531,6 @@
<target>How to use markdown</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="ICE" xml:space="preserve">
<source>ICE</source>
<target>ICE</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="If you can't meet in person, **show QR code in the video call**, or share the link." xml:space="preserve">
<source>If you can't meet in person, **show QR code in the video call**, or share the link.</source>
<target>If you can't meet in person, **show QR code in the video call**, or share the link.</target>
@ -558,7 +544,7 @@
<trans-unit id="Ignore" xml:space="preserve">
<source>Ignore</source>
<target>Ignore</target>
<note>ignore incoming call via notification</note>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Image will be received when your contact is online, please wait or check later!" xml:space="preserve">
<source>Image will be received when your contact is online, please wait or check later!</source>
@ -748,7 +734,7 @@
<trans-unit id="Reject" xml:space="preserve">
<source>Reject</source>
<target>Reject</target>
<note>No comment provided by engineer.</note>
<note>reject incoming call via notification</note>
</trans-unit>
<trans-unit id="Reject contact (sender NOT notified)" xml:space="preserve">
<source>Reject contact (sender NOT notified)</source>
@ -795,11 +781,6 @@
<target>Scan contact's QR code</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Send" xml:space="preserve">
<source>Send</source>
<target>Send</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server connected" xml:space="preserve">
<source>Server connected</source>
<target>Server connected</target>
@ -830,11 +811,6 @@
<target>Show pending connections</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Start" xml:space="preserve">
<source>Start</source>
<target>Start</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Take picture" xml:space="preserve">
<source>Take picture</source>
<target>Take picture</target>
@ -1113,9 +1089,9 @@ SimpleX servers cannot see your profile.</target>
<target>above, then choose:</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="accepted" xml:space="preserve">
<source>accepted</source>
<target>accepted</target>
<trans-unit id="accepted call" xml:space="preserve">
<source>accepted call</source>
<target>accepted call</target>
<note>call status</note>
</trans-unit>
<trans-unit id="audio call (not e2e encrypted)" xml:space="preserve">
@ -1128,6 +1104,16 @@ SimpleX servers cannot see your profile.</target>
<target>bold</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="call error" xml:space="preserve">
<source>call error</source>
<target>call error</target>
<note>call status</note>
</trans-unit>
<trans-unit id="call in progress" xml:space="preserve">
<source>call in progress</source>
<target>call in progress</target>
<note>call status</note>
</trans-unit>
<trans-unit id="calling…" xml:space="preserve">
<source>calling…</source>
<target>calling…</target>
@ -1148,11 +1134,15 @@ SimpleX servers cannot see your profile.</target>
<target>connected</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="connecting call…" xml:space="preserve">
<source>connecting call…</source>
<target>connecting call…</target>
<note>call status</note>
</trans-unit>
<trans-unit id="connecting…" xml:space="preserve">
<source>connecting…</source>
<target>connecting…</target>
<note>call status
chat list item title</note>
<note>chat list item title</note>
</trans-unit>
<trans-unit id="connection established" xml:space="preserve">
<source>connection established</source>
@ -1184,19 +1174,14 @@ SimpleX servers cannot see your profile.</target>
<target>e2e encrypted</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="ended %@" xml:space="preserve">
<source>ended %@</source>
<target>ended %@</target>
<note>call status</note>
<trans-unit id="ended" xml:space="preserve">
<source>ended</source>
<target>ended</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="error" xml:space="preserve">
<source>error</source>
<target>error</target>
<note>call status</note>
</trans-unit>
<trans-unit id="in progress" xml:space="preserve">
<source>in progress</source>
<target>in progress</target>
<trans-unit id="ended call %@" xml:space="preserve">
<source>ended call %@</source>
<target>ended call %@</target>
<note>call status</note>
</trans-unit>
<trans-unit id="invited to connect" xml:space="preserve">
@ -1209,9 +1194,9 @@ SimpleX servers cannot see your profile.</target>
<target>italic</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="missed" xml:space="preserve">
<source>missed</source>
<target>missed</target>
<trans-unit id="missed call" xml:space="preserve">
<source>missed call</source>
<target>missed call</target>
<note>call status</note>
</trans-unit>
<trans-unit id="no e2e encryption" xml:space="preserve">
@ -1234,9 +1219,14 @@ SimpleX servers cannot see your profile.</target>
<target>received answer…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="rejected" xml:space="preserve">
<source>rejected</source>
<target>rejected</target>
<trans-unit id="received confirmation…" xml:space="preserve">
<source>received confirmation…</source>
<target>received confirmation…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="rejected call" xml:space="preserve">
<source>rejected call</source>
<target>rejected call</target>
<note>call status</note>
</trans-unit>
<trans-unit id="secret" xml:space="preserve">
@ -1299,11 +1289,6 @@ SimpleX servers cannot see your profile.</target>
<target>wants to connect to you!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="with e2e encryption" xml:space="preserve">
<source>with e2e encryption</source>
<target>with e2e encryption</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="you shared one-time link" xml:space="preserve">
<source>you shared one-time link</source>
<target>you shared one-time link</target>
@ -1410,9 +1395,9 @@ SimpleX servers cannot see your profile.</target>
<target>You can now send messages to %@</target>
<note>notification body</note>
</trans-unit>
<trans-unit id="accepted" xml:space="preserve">
<source>accepted</source>
<target>accepted</target>
<trans-unit id="accepted call" xml:space="preserve">
<source>accepted call</source>
<target>accepted call</target>
<note>call status</note>
</trans-unit>
<trans-unit id="audio call (not e2e encrypted)" xml:space="preserve">
@ -1420,16 +1405,30 @@ SimpleX servers cannot see your profile.</target>
<target>audio call (not e2e encrypted)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="call error" xml:space="preserve">
<source>call error</source>
<target>call error</target>
<note>call status</note>
</trans-unit>
<trans-unit id="call in progress" xml:space="preserve">
<source>call in progress</source>
<target>call in progress</target>
<note>call status</note>
</trans-unit>
<trans-unit id="calling…" xml:space="preserve">
<source>calling…</source>
<target>calling…</target>
<note>call status</note>
</trans-unit>
<trans-unit id="connecting call…" xml:space="preserve">
<source>connecting call…</source>
<target>connecting call…</target>
<note>call status</note>
</trans-unit>
<trans-unit id="connecting…" xml:space="preserve">
<source>connecting…</source>
<target>connecting…</target>
<note>call status
chat list item title</note>
<note>chat list item title</note>
</trans-unit>
<trans-unit id="connection established" xml:space="preserve">
<source>connection established</source>
@ -1446,19 +1445,9 @@ SimpleX servers cannot see your profile.</target>
<target>deleted</target>
<note>deleted chat item</note>
</trans-unit>
<trans-unit id="ended %@" xml:space="preserve">
<source>ended %@</source>
<target>ended %@</target>
<note>call status</note>
</trans-unit>
<trans-unit id="error" xml:space="preserve">
<source>error</source>
<target>error</target>
<note>call status</note>
</trans-unit>
<trans-unit id="in progress" xml:space="preserve">
<source>in progress</source>
<target>in progress</target>
<trans-unit id="ended call %@" xml:space="preserve">
<source>ended call %@</source>
<target>ended call %@</target>
<note>call status</note>
</trans-unit>
<trans-unit id="invited to connect" xml:space="preserve">
@ -1466,19 +1455,14 @@ SimpleX servers cannot see your profile.</target>
<target>invited to connect</target>
<note>chat list item title</note>
</trans-unit>
<trans-unit id="missed" xml:space="preserve">
<source>missed</source>
<target>missed</target>
<trans-unit id="missed call" xml:space="preserve">
<source>missed call</source>
<target>missed call</target>
<note>call status</note>
</trans-unit>
<trans-unit id="no e2e encryption" xml:space="preserve">
<source>no e2e encryption</source>
<target>no e2e encryption</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="rejected" xml:space="preserve">
<source>rejected</source>
<target>rejected</target>
<trans-unit id="rejected call" xml:space="preserve">
<source>rejected call</source>
<target>rejected call</target>
<note>call status</note>
</trans-unit>
<trans-unit id="via contact address link" xml:space="preserve">
@ -1496,11 +1480,6 @@ SimpleX servers cannot see your profile.</target>
<target>video call (not e2e encrypted)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="with e2e encryption" xml:space="preserve">
<source>with e2e encryption</source>
<target>with e2e encryption</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="you shared one-time link" xml:space="preserve">
<source>you shared one-time link</source>
<target>you shared one-time link</target>

View file

@ -20,11 +20,6 @@
<target> (можно скопировать)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id=" wants to connect with you via " xml:space="preserve">
<source> wants to connect with you via </source>
<target> хочет связаться с вами через </target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="!1 colored!" xml:space="preserve">
<source>!1 colored!</source>
<target>!1 цвет!</target>
@ -133,7 +128,8 @@
<trans-unit id="Accept" xml:space="preserve">
<source>Accept</source>
<target>Принять</target>
<note>accept contact request via notification</note>
<note>accept contact request via notification
accept incoming call via notification</note>
</trans-unit>
<trans-unit id="Accept contact" xml:space="preserve">
<source>Accept contact</source>
@ -165,11 +161,6 @@
<target>Соединение уже установлено?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Answer" xml:space="preserve">
<source>Answer</source>
<target>Ответить</target>
<note>accept incoming call via notification</note>
</trans-unit>
<trans-unit id="Answer call" xml:space="preserve">
<source>Answer call</source>
<target>Принять звонок</target>
@ -185,16 +176,16 @@
<target>Звонок уже завершен!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Call settings" xml:space="preserve">
<source>Call settings</source>
<target>Настройки звонков</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Cancel" xml:space="preserve">
<source>Cancel</source>
<target>Отменить</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Capabilities" xml:space="preserve">
<source>Capabilities</source>
<target>Возможности</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat console" xml:space="preserve">
<source>Chat console</source>
<target>Консоль</target>
@ -275,6 +266,11 @@
<target>Соединиться через одноразовую ссылку?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via relay" xml:space="preserve">
<source>Connect via relay</source>
<target>Соединяться через сервер (relay)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect with the developers" xml:space="preserve">
<source>Connect with the developers</source>
<target>Соединиться с разработчиками</target>
@ -460,11 +456,6 @@
<target>Включить уведомления? (БЕТА)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="End" xml:space="preserve">
<source>End</source>
<target>Завершить</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enter one SMP server per line:" xml:space="preserve">
<source>Enter one SMP server per line:</source>
<target>Введите SMP серверы, каждый на отдельной строке:</target>
@ -540,11 +531,6 @@
<target>Как форматировать</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="ICE" xml:space="preserve">
<source>ICE</source>
<target>ICE</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="If you can't meet in person, **show QR code in the video call**, or share the link." xml:space="preserve">
<source>If you can't meet in person, **show QR code in the video call**, or share the link.</source>
<target>Если вы не можете встретиться лично, вы можете **показать QR код во время видеозвонка**, или поделиться ссылкой.</target>
@ -558,7 +544,7 @@
<trans-unit id="Ignore" xml:space="preserve">
<source>Ignore</source>
<target>Не отвечать</target>
<note>ignore incoming call via notification</note>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Image will be received when your contact is online, please wait or check later!" xml:space="preserve">
<source>Image will be received when your contact is online, please wait or check later!</source>
@ -748,7 +734,7 @@
<trans-unit id="Reject" xml:space="preserve">
<source>Reject</source>
<target>Отклонить</target>
<note>No comment provided by engineer.</note>
<note>reject incoming call via notification</note>
</trans-unit>
<trans-unit id="Reject contact (sender NOT notified)" xml:space="preserve">
<source>Reject contact (sender NOT notified)</source>
@ -795,11 +781,6 @@
<target>Сосканировать QR код контакта</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Send" xml:space="preserve">
<source>Send</source>
<target>Отправить</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server connected" xml:space="preserve">
<source>Server connected</source>
<target>Установлено соединение с сервером</target>
@ -830,11 +811,6 @@
<target>Показать ожидаемые соединения</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Start" xml:space="preserve">
<source>Start</source>
<target>Начать</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Take picture" xml:space="preserve">
<source>Take picture</source>
<target>Сделать фото</target>
@ -1113,9 +1089,9 @@ SimpleX серверы не могут получить доступ к ваше
<target>наверху, затем выберите:</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="accepted" xml:space="preserve">
<source>accepted</source>
<target>принятый звонок</target>
<trans-unit id="accepted call" xml:space="preserve">
<source>accepted call</source>
<target> принятый звонок</target>
<note>call status</note>
</trans-unit>
<trans-unit id="audio call (not e2e encrypted)" xml:space="preserve">
@ -1128,6 +1104,16 @@ SimpleX серверы не могут получить доступ к ваше
<target>жирный</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="call error" xml:space="preserve">
<source>call error</source>
<target>ошибка звонка</target>
<note>call status</note>
</trans-unit>
<trans-unit id="call in progress" xml:space="preserve">
<source>call in progress</source>
<target>активный звонок</target>
<note>call status</note>
</trans-unit>
<trans-unit id="calling…" xml:space="preserve">
<source>calling…</source>
<target>входящий звонок…</target>
@ -1148,11 +1134,15 @@ SimpleX серверы не могут получить доступ к ваше
<target>соединение установлено</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="connecting call…" xml:space="preserve">
<source>connecting call…</source>
<target>звонок соединяется…</target>
<note>call status</note>
</trans-unit>
<trans-unit id="connecting…" xml:space="preserve">
<source>connecting…</source>
<target>соединяется…</target>
<note>call status
chat list item title</note>
<note>chat list item title</note>
</trans-unit>
<trans-unit id="connection established" xml:space="preserve">
<source>connection established</source>
@ -1184,19 +1174,14 @@ SimpleX серверы не могут получить доступ к ваше
<target>e2e зашифровано</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="ended %@" xml:space="preserve">
<source>ended %@</source>
<target>завершен %@</target>
<note>call status</note>
<trans-unit id="ended" xml:space="preserve">
<source>ended</source>
<target>завершён</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="error" xml:space="preserve">
<source>error</source>
<target>ошибка соединения</target>
<note>call status</note>
</trans-unit>
<trans-unit id="in progress" xml:space="preserve">
<source>in progress</source>
<target>активный звонок</target>
<trans-unit id="ended call %@" xml:space="preserve">
<source>ended call %@</source>
<target>завершённый звонок %@</target>
<note>call status</note>
</trans-unit>
<trans-unit id="invited to connect" xml:space="preserve">
@ -1209,8 +1194,8 @@ SimpleX серверы не могут получить доступ к ваше
<target>курсив</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="missed" xml:space="preserve">
<source>missed</source>
<trans-unit id="missed call" xml:space="preserve">
<source>missed call</source>
<target>пропущенный звонок</target>
<note>call status</note>
</trans-unit>
@ -1234,9 +1219,14 @@ SimpleX серверы не могут получить доступ к ваше
<target>получен ответ…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="rejected" xml:space="preserve">
<source>rejected</source>
<target>отклоненный звонок</target>
<trans-unit id="received confirmation…" xml:space="preserve">
<source>received confirmation…</source>
<target>получено подтверждение…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="rejected call" xml:space="preserve">
<source>rejected call</source>
<target>отклонённый звонок</target>
<note>call status</note>
</trans-unit>
<trans-unit id="secret" xml:space="preserve">
@ -1299,11 +1289,6 @@ SimpleX серверы не могут получить доступ к ваше
<target>хочет соединиться с вами!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="with e2e encryption" xml:space="preserve">
<source>with e2e encryption</source>
<target>e2e зашифровано</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="you shared one-time link" xml:space="preserve">
<source>you shared one-time link</source>
<target>вы создали ссылку</target>
@ -1410,9 +1395,9 @@ SimpleX серверы не могут получить доступ к ваше
<target>Вы можете отправлять сообщения %@</target>
<note>notification body</note>
</trans-unit>
<trans-unit id="accepted" xml:space="preserve">
<source>accepted</source>
<target>принятый звонок</target>
<trans-unit id="accepted call" xml:space="preserve">
<source>accepted call</source>
<target>принятный звонок</target>
<note>call status</note>
</trans-unit>
<trans-unit id="audio call (not e2e encrypted)" xml:space="preserve">
@ -1420,16 +1405,30 @@ SimpleX серверы не могут получить доступ к ваше
<target>аудиозвонок (не e2e зашифрованный)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="call error" xml:space="preserve">
<source>call error</source>
<target>ошибка звонка</target>
<note>call status</note>
</trans-unit>
<trans-unit id="call in progress" xml:space="preserve">
<source>call in progress</source>
<target>активный звонок</target>
<note>call status</note>
</trans-unit>
<trans-unit id="calling…" xml:space="preserve">
<source>calling…</source>
<target>входящий звонок…</target>
<note>call status</note>
</trans-unit>
<trans-unit id="connecting call…" xml:space="preserve">
<source>connecting call…</source>
<target>звонок соединяется…</target>
<note>call status</note>
</trans-unit>
<trans-unit id="connecting…" xml:space="preserve">
<source>connecting…</source>
<target>соединяется…</target>
<note>call status
chat list item title</note>
<note>chat list item title</note>
</trans-unit>
<trans-unit id="connection established" xml:space="preserve">
<source>connection established</source>
@ -1446,19 +1445,9 @@ SimpleX серверы не могут получить доступ к ваше
<target>удалено</target>
<note>deleted chat item</note>
</trans-unit>
<trans-unit id="ended %@" xml:space="preserve">
<source>ended %@</source>
<target>завершен %@</target>
<note>call status</note>
</trans-unit>
<trans-unit id="error" xml:space="preserve">
<source>error</source>
<target>ошибка соединения</target>
<note>call status</note>
</trans-unit>
<trans-unit id="in progress" xml:space="preserve">
<source>in progress</source>
<target>активный звонок</target>
<trans-unit id="ended call %@" xml:space="preserve">
<source>ended call %@</source>
<target>завершённый звонок %@</target>
<note>call status</note>
</trans-unit>
<trans-unit id="invited to connect" xml:space="preserve">
@ -1466,19 +1455,14 @@ SimpleX серверы не могут получить доступ к ваше
<target>приглашение соединиться</target>
<note>chat list item title</note>
</trans-unit>
<trans-unit id="missed" xml:space="preserve">
<source>missed</source>
<trans-unit id="missed call" xml:space="preserve">
<source>missed call</source>
<target>пропущенный звонок</target>
<note>call status</note>
</trans-unit>
<trans-unit id="no e2e encryption" xml:space="preserve">
<source>no e2e encryption</source>
<target>нет e2e шифрования</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="rejected" xml:space="preserve">
<source>rejected</source>
<target>отклоненный звонок</target>
<trans-unit id="rejected call" xml:space="preserve">
<source>rejected call</source>
<target>отклонённый звонок</target>
<note>call status</note>
</trans-unit>
<trans-unit id="via contact address link" xml:space="preserve">
@ -1496,11 +1480,6 @@ SimpleX серверы не могут получить доступ к ваше
<target>видеозвонок (не e2e зашифрованный)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="with e2e encryption" xml:space="preserve">
<source>with e2e encryption</source>
<target>e2e зашифровано</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="you shared one-time link" xml:space="preserve">
<source>you shared one-time link</source>
<target>вы создали одноразовую ссылку</target>

View file

@ -14,16 +14,24 @@
"Accept contact request from %@?" = "Принять запрос на соединение от %@?";
/* call status */
"accepted" = "принятый звонок";
"accepted call" = "принятный звонок";
/* No comment provided by engineer. */
"audio call (not e2e encrypted)" = "аудиозвонок (не e2e зашифрованный)";
/* call status */
"call error" = "ошибка звонка";
/* call status */
"call in progress" = "активный звонок";
/* call status */
"calling…" = "входящий звонок…";
/* call status
chat list item title */
/* call status */
"connecting call…" = "звонок соединяется…";
/* chat list item title */
"connecting…" = "соединяется…";
/* chat list item title (it should not be shown */
@ -36,13 +44,7 @@
"deleted" = "удалено";
/* call status */
"ended %@" = "завершен %@";
/* call status */
"error" = "ошибка соединения";
/* call status */
"in progress" = "активный звонок";
"ended call %@" = "завершённый звонок %@";
/* notification */
"Incoming audio call" = "Входящий аудиозвонок";
@ -54,13 +56,10 @@
"invited to connect" = "приглашение соединиться";
/* call status */
"missed" = "пропущенный звонок";
/* No comment provided by engineer. */
"no e2e encryption" = "нет e2e шифрования";
"missed call" = "пропущенный звонок";
/* call status */
"rejected" = "отклоненный звонок";
"rejected call" = "отклонённый звонок";
/* chat list item description */
"via contact address link" = "через ссылку-контакт";
@ -71,9 +70,6 @@
/* No comment provided by engineer. */
"video call (not e2e encrypted)" = "видеозвонок (не e2e зашифрованный)";
/* No comment provided by engineer. */
"with e2e encryption" = "e2e зашифровано";
/* notification body */
"You can now send messages to %@" = "Вы можете отправлять сообщения %@";

View file

@ -23,10 +23,8 @@
<false/>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>fetch</string>
<string>remote-notification</string>
<string>voip</string>
</array>
</dict>
</plist>

View file

@ -14,6 +14,7 @@
3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4727FF621E00354CDD /* CILinkView.swift */; };
5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA72837DBB3004A9677 /* CICallItemView.swift */; };
5C029EAA283942EA004A9677 /* CallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA9283942EA004A9677 /* CallController.swift */; };
5C05DF532840AA1D00C683F9 /* CallSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C05DF522840AA1D00C683F9 /* CallSettings.swift */; };
5C063D2727A4564100AEC577 /* ChatPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C063D2627A4564100AEC577 /* ChatPreviewView.swift */; };
5C116CDC27AABE0400E66D01 /* ContactRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */; };
5C13730B28156D2700F43030 /* ContactConnectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C13730A28156D2700F43030 /* ContactConnectionView.swift */; };
@ -138,6 +139,7 @@
3CDBCF4727FF621E00354CDD /* CILinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CILinkView.swift; sourceTree = "<group>"; };
5C029EA72837DBB3004A9677 /* CICallItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CICallItemView.swift; sourceTree = "<group>"; };
5C029EA9283942EA004A9677 /* CallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallController.swift; sourceTree = "<group>"; };
5C05DF522840AA1D00C683F9 /* CallSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallSettings.swift; sourceTree = "<group>"; };
5C063D2627A4564100AEC577 /* ChatPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPreviewView.swift; sourceTree = "<group>"; };
5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactRequestView.swift; sourceTree = "<group>"; };
5C13730A28156D2700F43030 /* ContactConnectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactConnectionView.swift; sourceTree = "<group>"; };
@ -463,6 +465,7 @@
children = (
5CB924D327A853F100ACCCDD /* SettingsButton.swift */,
5CB924D627A8563F00ACCCDD /* SettingsView.swift */,
5C05DF522840AA1D00C683F9 /* CallSettings.swift */,
5CB924E327A8683A00ACCCDD /* UserAddress.swift */,
5CB924E027A867BA00ACCCDD /* UserProfile.swift */,
5C577F7C27C83AA10006112D /* MarkdownHelp.swift */,
@ -624,7 +627,6 @@
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
ru,
);
mainGroup = 5CA059BD279559F40002BEB4;
@ -717,6 +719,7 @@
649BCDA0280460FD00C3A862 /* ComposeImageView.swift in Sources */,
5CA059ED279559F40002BEB4 /* ContentView.swift in Sources */,
5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */,
5C05DF532840AA1D00C683F9 /* CallSettings.swift in Sources */,
5CFE0921282EEAF60002594B /* ZoomableScrollView.swift in Sources */,
5C3A88CE27DF50170060F1C2 /* DetermineWidth.swift in Sources */,
5CB0BA962827143500B3292C /* MakeConnection.swift in Sources */,

View file

@ -49,8 +49,7 @@
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
showNonLocalizedStrings = "YES">
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference

View file

@ -7,9 +7,6 @@
/* No comment provided by engineer. */
" (can be copied)" = " (можно скопировать)";
/* No comment provided by engineer. */
" wants to connect with you via " = " хочет связаться с вами через ";
/* No comment provided by engineer. */
"_italic_" = "\\_курсив_";
@ -88,7 +85,8 @@
/* No comment provided by engineer. */
"above, then choose:" = "наверху, затем выберите:";
/* accept contact request via notification */
/* accept contact request via notification
accept incoming call via notification */
"Accept" = "Принять";
/* No comment provided by engineer. */
@ -98,7 +96,7 @@
"Accept contact request from %@?" = "Принять запрос на соединение от %@?";
/* call status */
"accepted" = "принятый звонок";
"accepted call" = " принятый звонок";
/* No comment provided by engineer. */
"Add contact to start a new chat" = "Добавьте контакт, чтобы начать разговор";
@ -112,9 +110,6 @@
/* No comment provided by engineer. */
"Already connected?" = "Соединение уже установлено?";
/* accept incoming call via notification */
"Answer" = "Ответить";
/* No comment provided by engineer. */
"Answer call" = "Принять звонок";
@ -130,15 +125,21 @@
/* No comment provided by engineer. */
"Call already ended!" = "Звонок уже завершен!";
/* call status */
"call error" = "ошибка звонка";
/* call status */
"call in progress" = "активный звонок";
/* No comment provided by engineer. */
"Call settings" = "Настройки звонков";
/* call status */
"calling…" = "входящий звонок…";
/* No comment provided by engineer. */
"Cancel" = "Отменить";
/* No comment provided by engineer. */
"Capabilities" = "Возможности";
/* No comment provided by engineer. */
"Chat console" = "Консоль";
@ -193,20 +194,25 @@
/* No comment provided by engineer. */
"Connect via one-time link?" = "Соединиться через одноразовую ссылку?";
/* No comment provided by engineer. */
"Connect via relay" = "Соединяться через сервер (relay)";
/* No comment provided by engineer. */
"Connect with the developers" = "Соединиться с разработчиками";
/* No comment provided by engineer. */
"connected" = "соединение установлено";
/* call status */
"connecting call…" = "звонок соединяется…";
/* No comment provided by engineer. */
"Connecting server…" = "Устанавливается соединение с сервером…";
/* No comment provided by engineer. */
"Connecting server… (error: %@)" = "Устанавливается соединение с сервером… (ошибка: %@)";
/* call status
chat list item title */
/* chat list item title */
"connecting…" = "соединяется…";
/* No comment provided by engineer. */
@ -330,17 +336,14 @@
"Enable notifications? (BETA)" = "Включить уведомления? (БЕТА)";
/* No comment provided by engineer. */
"End" = "Завершить";
"ended" = "завершён";
/* call status */
"ended %@" = "завершен %@";
"ended call %@" = "завершённый звонок %@";
/* No comment provided by engineer. */
"Enter one SMP server per line:" = "Введите SMP серверы, каждый на отдельной строке:";
/* call status */
"error" = "ошибка соединения";
/* No comment provided by engineer. */
"Error deleting token" = "Ошибка удаления токена";
@ -383,16 +386,13 @@
/* No comment provided by engineer. */
"How to use markdown" = "Как форматировать";
/* No comment provided by engineer. */
"ICE" = "ICE";
/* No comment provided by engineer. */
"If you can't meet in person, **show QR code in the video call**, or share the link." = "Если вы не можете встретиться лично, вы можете **показать QR код во время видеозвонка**, или поделиться ссылкой.";
/* No comment provided by engineer. */
"If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." = "Если вы не можете встретиться лично, вы можете **сосканировать QR код во время видеозвонка**, или ваш контакт может отправить вам ссылку.";
/* ignore incoming call via notification */
/* No comment provided by engineer. */
"Ignore" = "Не отвечать";
/* No comment provided by engineer. */
@ -404,9 +404,6 @@
/* No comment provided by engineer. */
"In person or via a video call the most secure way to connect." = "При встрече или в видеозвонке самый безопасный способ установить соединение";
/* call status */
"in progress" = "активный звонок";
/* notification */
"Incoming audio call" = "Входящий аудиозвонок";
@ -453,7 +450,7 @@
"Message delivery error" = "Ошибка доставки сообщения";
/* call status */
"missed" = "пропущенный звонок";
"missed call" = "пропущенный звонок";
/* No comment provided by engineer. */
"Most likely this contact has deleted the connection with you." = "Скорее всего, этот контакт удалил соединение с вами.";
@ -531,6 +528,9 @@
"received answer…" = "получен ответ…";
/* No comment provided by engineer. */
"received confirmation…" = "получено подтверждение…";
/* reject incoming call via notification */
"Reject" = "Отклонить";
/* No comment provided by engineer. */
@ -540,7 +540,7 @@
"Reject contact request" = "Отклонить запрос";
/* call status */
"rejected" = "отклоненный звонок";
"rejected call" = "отклонённый звонок";
/* No comment provided by engineer. */
"Reply" = "Ответить";
@ -563,9 +563,6 @@
/* No comment provided by engineer. */
"secret" = "секрет";
/* No comment provided by engineer. */
"Send" = "Отправить";
/* No comment provided by engineer. */
"Server connected" = "Установлено соединение с сервером";
@ -587,9 +584,6 @@
/* No comment provided by engineer. */
"SMP servers" = "SMP серверы";
/* No comment provided by engineer. */
"Start" = "Начать";
/* No comment provided by engineer. */
"starting…" = "инициализация…";
@ -698,9 +692,6 @@
/* No comment provided by engineer. */
"Welcome %@!" = "Здравствуйте %@!";
/* No comment provided by engineer. */
"with e2e encryption" = "e2e зашифровано";
/* No comment provided by engineer. */
"You" = "Вы";

View file

@ -53,6 +53,7 @@ interface IWCallResponse {
interface WCCapabilities extends IWCallCommand {
type: "capabilities"
media?: CallMediaType
useWorker?: boolean
}
@ -289,8 +290,7 @@ const processCommand = (function () {
const pc = new RTCPeerConnection(config.peerConnectionConfig)
const remoteStream = new MediaStream()
const localCamera = VideoCamera.User
const constraints = callMediaConstraints(mediaType, localCamera)
const localStream = await navigator.mediaDevices.getUserMedia(constraints)
const localStream = await getLocalMediaStream(mediaType, localCamera)
const iceCandidates = getIceCandidates(pc, config)
const call = {connection: pc, iceCandidates, localMedia: mediaType, localCamera, localStream, remoteStream, aesKey, useWorker}
await setupMediaStreams(call)
@ -352,11 +352,15 @@ const processCommand = (function () {
try {
switch (command.type) {
case "capabilities":
console.log("starting outgoing call - capabilities")
if (activeCall) endCall()
// This request for local media stream is made to prompt for camera/mic permissions on call start
if (command.media) await getLocalMediaStream(command.media, VideoCamera.User)
const encryption = supportsInsertableStreams(command.useWorker)
resp = {type: "capabilities", capabilities: {encryption}}
break
case "start": {
console.log("starting call")
console.log("starting incoming call - create webrtc session")
if (activeCall) endCall()
const {media, useWorker, iceServers, relay} = command
const encryption = supportsInsertableStreams(useWorker)
@ -582,8 +586,7 @@ const processCommand = (function () {
const pc = call.connection
for (const t of call.localStream.getTracks()) t.stop()
call.localCamera = camera
const constraints = callMediaConstraints(call.localMedia, camera)
const localStream = await navigator.mediaDevices.getUserMedia(constraints)
const localStream = await getLocalMediaStream(call.localMedia, camera)
replaceTracks(pc, localStream.getVideoTracks())
replaceTracks(pc, localStream.getAudioTracks())
call.localStream = localStream
@ -621,6 +624,11 @@ const processCommand = (function () {
}
}
function getLocalMediaStream(mediaType: CallMediaType, facingMode: VideoCamera): Promise<MediaStream> {
const constraints = callMediaConstraints(mediaType, facingMode)
return navigator.mediaDevices.getUserMedia(constraints)
}
function callMediaConstraints(mediaType: CallMediaType, facingMode: VideoCamera): MediaStreamConstraints {
switch (mediaType) {
case CallMediaType.Audio: