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"> <component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173"> <code_scheme name="Project" version="173">
<ComposeCustomCodeStyleSettings>
<option name="USE_CUSTOM_FORMATTING_FOR_MODIFIERS" value="false" />
</ComposeCustomCodeStyleSettings>
<JetCodeStyleSettings> <JetCodeStyleSettings>
<option name="SPACE_BEFORE_EXTEND_COLON" value="false" /> <option name="SPACE_BEFORE_EXTEND_COLON" value="false" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="3" /> <option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="3" />
@ -123,9 +126,11 @@
<codeStyleSettings language="kotlin"> <codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<option name="RIGHT_MARGIN" value="140" /> <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_DECLARATIONS" value="1" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="0" /> <option name="KEEP_BLANK_LINES_IN_CODE" value="0" />
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" 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="CALL_PARAMETERS_WRAP" value="0" />
<option name="METHOD_PARAMETERS_WRAP" value="0" /> <option name="METHOD_PARAMETERS_WRAP" value="0" />
<option name="EXTENDS_LIST_WRAP" value="0" /> <option name="EXTENDS_LIST_WRAP" value="0" />

View file

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

View file

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

View file

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

View file

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

View file

@ -50,9 +50,18 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
var chatModel = ChatModel(this) var chatModel = ChatModel(this)
private val sharedPreferences: SharedPreferences = appContext.getSharedPreferences(SHARED_PREFS_ID, Context.MODE_PRIVATE) 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 { init {
chatModel.runServiceInBackground.value = getRunServiceInBackground() chatModel.runServiceInBackground.value = prefRunServiceInBackground.get()
chatModel.performLA.value = getPerformLA() chatModel.performLA.value = prefPerformLA.get()
} }
suspend fun startChat(user: User) { suspend fun startChat(user: User) {
@ -529,7 +538,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
if (invitation != null) { if (invitation != null) {
chatModel.callManager.reportCallRemoteEnded(invitation = invitation) chatModel.callManager.reportCallRemoteEnded(invitation = invitation)
} }
withCall(r, r.contact) { call -> withCall(r, r.contact) { _ ->
chatModel.callCommand.value = WCallCommand.End chatModel.callCommand.value = WCallCommand.End
withApi { withApi {
chatModel.activeCall.value = null chatModel.activeCall.value = null
@ -583,7 +592,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
fun showBackgroundServiceNoticeIfNeeded() { fun showBackgroundServiceNoticeIfNeeded() {
Log.d(TAG, "showBackgroundServiceNoticeIfNeeded") Log.d(TAG, "showBackgroundServiceNoticeIfNeeded")
if (!getBackgroundServiceNoticeShown()) { if (!prefBackgroundServiceNoticeShown.get()) {
// the branch for the new users who has never seen service notice // the branch for the new users who has never seen service notice
if (isIgnoringBatteryOptimizations(appContext)) { if (isIgnoringBatteryOptimizations(appContext)) {
showBGServiceNotice() showBGServiceNotice()
@ -591,20 +600,20 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
showBGServiceNoticeIgnoreOptimization() showBGServiceNoticeIgnoreOptimization()
} }
// set both flags, so that if the user doesn't allow ignoring optimizations, the service will be disabled without additional notice // set both flags, so that if the user doesn't allow ignoring optimizations, the service will be disabled without additional notice
setBackgroundServiceNoticeShown(true) prefBackgroundServiceNoticeShown.set(true)
setBackgroundServiceBatteryNoticeShown(true) prefBackgroundServiceBatteryNoticeShown.set(true)
} else if (!isIgnoringBatteryOptimizations(appContext) && getRunServiceInBackground()) { } else if (!isIgnoringBatteryOptimizations(appContext) && prefRunServiceInBackground.get()) {
// the branch for users who have app installed, and have seen the service notice, // 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 // 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 // users have been presented with battery notice before - they did not allow ignoring optimizitions -> disable service
showDisablingServiceNotice() showDisablingServiceNotice()
setRunServiceInBackground(false) prefRunServiceInBackground.set(false)
chatModel.runServiceInBackground.value = false chatModel.runServiceInBackground.value = false
} else { } else {
// show battery optimization notice // show battery optimization notice
showBGServiceNoticeIgnoreOptimization() showBGServiceNoticeIgnoreOptimization()
setBackgroundServiceBatteryNoticeShown(true) prefBackgroundServiceBatteryNoticeShown.set(true)
} }
} }
} }
@ -695,8 +704,8 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
fun showLANotice(activity: FragmentActivity) { fun showLANotice(activity: FragmentActivity) {
Log.d(TAG, "showLANotice") Log.d(TAG, "showLANotice")
if (!getLANoticeShown()) { if (!prefLANoticeShown.get()) {
setLANoticeShown(true) prefLANoticeShown.set(true)
AlertManager.shared.showAlertDialog( AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.la_notice_title), title = generalGetString(R.string.la_notice_title),
text = generalGetString(R.string.la_notice_text), text = generalGetString(R.string.la_notice_text),
@ -710,22 +719,22 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
when (laResult) { when (laResult) {
LAResult.Success -> { LAResult.Success -> {
chatModel.performLA.value = true chatModel.performLA.value = true
setPerformLA(true) prefPerformLA.set(true)
laTurnedOnAlert() laTurnedOnAlert()
} }
is LAResult.Error -> { is LAResult.Error -> {
chatModel.performLA.value = false chatModel.performLA.value = false
setPerformLA(false) prefPerformLA.set(false)
laErrorToast(appContext, laResult.errString) laErrorToast(appContext, laResult.errString)
} }
LAResult.Failed -> { LAResult.Failed -> {
chatModel.performLA.value = false chatModel.performLA.value = false
setPerformLA(false) prefPerformLA.set(false)
laFailedToast(appContext) laFailedToast(appContext)
} }
LAResult.Unavailable -> { LAResult.Unavailable -> {
chatModel.performLA.value = false chatModel.performLA.value = false
setPerformLA(false) prefPerformLA.set(false)
chatModel.showAdvertiseLAUnavailableAlert.value = true 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 { fun isIgnoringBatteryOptimizations(context: Context): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return true if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return true
val powerManager = context.getSystemService(Application.POWER_SERVICE) as PowerManager 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) = private fun mkBoolPreference(prefName: String, default: Boolean) =
sharedPreferences.edit() Preference(
.putBoolean(SHARED_PREFS_PERFORM_LA, performLA) get = fun() = sharedPreferences.getBoolean(prefName, default),
.apply() set = fun(value) = sharedPreferences.edit().putBoolean(prefName, value).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()
companion object { companion object {
private const val SHARED_PREFS_ID = "chat.simplex.app.SIMPLEX_APP_PREFS" 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_RUN_SERVICE_IN_BACKGROUND = "RunServiceInBackground"
private const val SHARED_PREFS_SERVICE_NOTICE_SHOWN = "BackgroundServiceNoticeShown" private const val SHARED_PREFS_SERVICE_NOTICE_SHOWN = "BackgroundServiceNoticeShown"
private const val SHARED_PREFS_SERVICE_BATTERY_NOTICE_SHOWN = "BackgroundServiceBatteryNoticeShown" 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_PERFORM_LA = "PerformLA"
private const val SHARED_PREFS_LA_NOTICE_SHOWN = "LANoticeShown" private const val SHARED_PREFS_LA_NOTICE_SHOWN = "LANoticeShown"
} }
} }
class Preference<T>(val get: () -> T, val set: (T) -> Unit)
// ChatCommand // ChatCommand
sealed class CC { sealed class CC {
class Console(val cmd: String): 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.FlipCameraAndroid, R.string.icon_descr_flip_camera, flipCamera)
ControlButton(call, Icons.Filled.Videocam, R.string.icon_descr_video_off, toggleVideo) ControlButton(call, Icons.Filled.Videocam, R.string.icon_descr_video_off, toggleVideo)
} else { } else {
Spacer(Modifier.size(40.dp)) Spacer(Modifier.size(48.dp))
ControlButton(call, Icons.Outlined.VideocamOff, R.string.icon_descr_video_on, toggleVideo) 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.compose.setContent
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.* 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.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.* import chat.simplex.app.*
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.model.ChatModel import chat.simplex.app.model.ChatModel
import chat.simplex.app.model.Contact import chat.simplex.app.model.Contact
import chat.simplex.app.model.NtfManager.Companion.OpenChatAction
import chat.simplex.app.ui.theme.* import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.ProfileImage import chat.simplex.app.views.helpers.ProfileImage
import chat.simplex.app.views.onboarding.SimpleXLogo import chat.simplex.app.views.onboarding.SimpleXLogo
@ -93,7 +93,7 @@ fun IncomingCallActivityView(m: ChatModel, activity: IncomingCallActivity) {
activity.finish() activity.finish()
} }
} }
SimpleXTheme(darkTheme = true) { SimpleXTheme {
Surface( Surface(
Modifier Modifier
.background(MaterialTheme.colors.background) .background(MaterialTheme.colors.background)
@ -104,33 +104,48 @@ fun IncomingCallActivityView(m: ChatModel, activity: IncomingCallActivity) {
if (invitation != null) IncomingCallAlertView(invitation, m) if (invitation != null) IncomingCallAlertView(invitation, m)
} }
} else if (invitation != null) { } else if (invitation != null) {
IncomingCallLockScreenAlert(invitation, m) IncomingCallLockScreenAlert(invitation, m, activity)
} }
} }
} }
} }
@Composable @Composable
fun IncomingCallLockScreenAlert(invitation: CallInvitation, chatModel: ChatModel) { fun IncomingCallLockScreenAlert(invitation: CallInvitation, chatModel: ChatModel, activity: IncomingCallActivity) {
val cm = chatModel.callManager val cm = chatModel.callManager
val cxt = LocalContext.current val cxt = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var acceptCallsFromLockScreen by remember { mutableStateOf(chatModel.controller.prefAcceptCallsFromLockScreen.get()) }
LaunchedEffect(true) { SoundPlayer.shared.start(cxt, scope, sound = true) } LaunchedEffect(true) { SoundPlayer.shared.start(cxt, scope, sound = true) }
DisposableEffect(true) { onDispose { SoundPlayer.shared.stop() } } DisposableEffect(true) { onDispose { SoundPlayer.shared.stop() } }
IncomingCallLockScreenAlertLayout( IncomingCallLockScreenAlertLayout(
invitation, invitation,
acceptCallsFromLockScreen,
rejectCall = { cm.endCall(invitation = invitation) }, rejectCall = { cm.endCall(invitation = invitation) },
ignoreCall = { chatModel.activeCallInvitation.value = null }, 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 @Composable
fun IncomingCallLockScreenAlertLayout( fun IncomingCallLockScreenAlertLayout(
invitation: CallInvitation, invitation: CallInvitation,
acceptCallsFromLockScreen: Boolean,
rejectCall: () -> Unit, rejectCall: () -> Unit,
ignoreCall: () -> Unit, ignoreCall: () -> Unit,
acceptCall: () -> Unit acceptCall: () -> Unit,
openApp: () -> Unit
) { ) {
Column( Column(
Modifier Modifier
@ -139,22 +154,24 @@ fun IncomingCallLockScreenAlertLayout(
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
IncomingCallInfo(invitation) IncomingCallInfo(invitation)
Spacer( Spacer(Modifier.fillMaxHeight().weight(1f))
Modifier if (acceptCallsFromLockScreen) {
.fillMaxHeight() ProfileImage(size = 192.dp, image = invitation.contact.profile.image)
.weight(1f)) Text(invitation.contact.chatViewName, style = MaterialTheme.typography.h2)
ProfileImage(size = 192.dp, image = invitation.contact.profile.image) Spacer(Modifier.fillMaxHeight().weight(1f))
Text(invitation.contact.chatViewName, style = MaterialTheme.typography.h2) Row {
Spacer( LockScreenCallButton(stringResource(R.string.reject), Icons.Filled.CallEnd, Color.Red, rejectCall)
Modifier Spacer(Modifier.size(48.dp))
.fillMaxHeight() LockScreenCallButton(stringResource(R.string.ignore), Icons.Filled.Close, MaterialTheme.colors.primary, ignoreCall)
.weight(1f)) Spacer(Modifier.size(48.dp))
Row { LockScreenCallButton(stringResource(R.string.accept), Icons.Filled.Check, SimplexGreen, acceptCall)
LockScreenCallButton(stringResource(R.string.reject), Icons.Filled.CallEnd, Color.Red, rejectCall) }
Spacer(Modifier.size(48.dp)) } else {
LockScreenCallButton(stringResource(R.string.ignore), Icons.Filled.Close, MaterialTheme.colors.primary, ignoreCall) SimpleXLogo()
Spacer(Modifier.size(48.dp)) Text(stringResource(R.string.open_simplex_chat_to_accept_call), textAlign = TextAlign.Center, lineHeight = 22.sp)
LockScreenCallButton(stringResource(R.string.accept), Icons.Filled.Check, SimplexGreen, acceptCall) 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 color = Color.Transparent
) { ) {
Column( Column(
Modifier.defaultMinSize(minWidth = 50.dp).padding(4.dp), Modifier
.defaultMinSize(minWidth = 50.dp)
.padding(4.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
IconButton(action) { IconButton(action) {
@ -185,16 +204,21 @@ private fun LockScreenCallButton(text: String, icon: ImageVector, color: Color,
@Composable @Composable
fun PreviewIncomingCallLockScreenAlert() { fun PreviewIncomingCallLockScreenAlert() {
SimpleXTheme(true) { SimpleXTheme(true) {
Surface(Modifier.background(MaterialTheme.colors.background).fillMaxSize()) { Surface(
Modifier
.background(MaterialTheme.colors.background)
.fillMaxSize()) {
IncomingCallLockScreenAlertLayout( IncomingCallLockScreenAlertLayout(
invitation = CallInvitation( invitation = CallInvitation(
contact = Contact.sampleData, contact = Contact.sampleData,
peerMedia = CallMediaType.Audio, peerMedia = CallMediaType.Audio,
sharedKey = null sharedKey = null
), ),
acceptCallsFromLockScreen = false,
rejectCall = {}, rejectCall = {},
ignoreCall = {}, 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 package chat.simplex.app.views.usersettings
import android.content.res.Configuration import android.content.res.Configuration
import androidx.compose.foundation.background import androidx.compose.foundation.*
import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -12,6 +13,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.UriHandler
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@ -32,9 +34,9 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
val user = chatModel.currentUser.value val user = chatModel.currentUser.value
fun setRunServiceInBackground(on: Boolean) { fun setRunServiceInBackground(on: Boolean) {
chatModel.controller.setRunServiceInBackground(on) chatModel.controller.prefRunServiceInBackground.set(on)
if (on && !chatModel.controller.isIgnoringBatteryOptimizations(chatModel.controller.appContext)) { if (on && !chatModel.controller.isIgnoringBatteryOptimizations(chatModel.controller.appContext)) {
chatModel.controller.setBackgroundServiceNoticeShown(false) chatModel.controller.prefBackgroundServiceNoticeShown.set(false)
} }
chatModel.controller.showBackgroundServiceNoticeIfNeeded() chatModel.controller.showBackgroundServiceNoticeIfNeeded()
chatModel.runServiceInBackground.value = on chatModel.runServiceInBackground.value = on
@ -75,6 +77,7 @@ fun SettingsLayout(
Modifier Modifier
.background(MaterialTheme.colors.background) .background(MaterialTheme.colors.background)
.fillMaxSize() .fillMaxSize()
.verticalScroll(rememberScrollState())
) { ) {
Column( Column(
Modifier Modifier
@ -83,6 +86,7 @@ fun SettingsLayout(
.padding(8.dp) .padding(8.dp)
.padding(top = 16.dp) .padding(top = 16.dp)
) { ) {
@Composable fun divider() = Divider(Modifier.padding(horizontal = 8.dp))
Text( Text(
stringResource(R.string.your_settings), stringResource(R.string.your_settings),
style = MaterialTheme.typography.h1, style = MaterialTheme.typography.h1,
@ -93,150 +97,219 @@ fun SettingsLayout(
SettingsSectionView(showCustomModal { chatModel, close -> UserProfileView(chatModel, close) }, 80.dp) { SettingsSectionView(showCustomModal { chatModel, close -> UserProfileView(chatModel, close) }, 80.dp) {
ProfilePreview(profile) ProfilePreview(profile)
} }
Divider(Modifier.padding(horizontal = 8.dp)) divider()
SettingsSectionView(showModal { UserAddressView(it) }) { UserAddressSection(showModal)
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))
}
Spacer(Modifier.height(24.dp)) Spacer(Modifier.height(24.dp))
SettingsSectionView(showModal { HelpView(it) }) { CallSettingsSection(showModal)
Icon( divider()
Icons.Outlined.HelpOutline, ChatLockSection(performLA, setPerformLA)
contentDescription = stringResource(R.string.icon_descr_help), divider()
) PrivateNotificationsSection(runServiceInBackground, setRunServiceInBackground)
Spacer(Modifier.padding(horizontal = 4.dp)) divider()
Text(stringResource(R.string.how_to_use_simplex_chat)) SMPServersSection(showModal)
}
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
)
}
Spacer(Modifier.height(24.dp)) Spacer(Modifier.height(24.dp))
SettingsSectionView(showModal { SMPServersView(it) }) { HelpViewSection(showModal)
Icon( divider()
Icons.Outlined.Dns, SimpleXInfoSection(showModal)
contentDescription = stringResource(R.string.smp_servers), divider()
) MarkdownHelpSection(showModal)
Spacer(Modifier.padding(horizontal = 4.dp)) divider()
Text(stringResource(R.string.smp_servers)) ConnectToDevelopersSection(uriHandler)
} divider()
Divider(Modifier.padding(horizontal = 8.dp)) SendEmailSection(uriHandler)
SettingsSectionView() { Spacer(Modifier.height(24.dp))
Icon(
Icons.Outlined.Bolt, ChatConsoleSection(showTerminal)
contentDescription = stringResource(R.string.private_notifications), divider()
) InstallTerminalAppSection(uriHandler)
Spacer(Modifier.padding(horizontal = 4.dp)) divider()
Text( AppVersionSection()
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})")
}
} }
} }
} }
@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) { @Composable fun ProfilePreview(profileOf: NamedChat, size: Dp = 60.dp, color: Color = MaterialTheme.colors.secondary) {
ProfileImage(size = size, image = profileOf.image, color = color) ProfileImage(size = size, image = profileOf.image, color = color)
Spacer(Modifier.padding(horizontal = 4.dp)) Spacer(Modifier.padding(horizontal = 4.dp))

View file

@ -377,6 +377,16 @@
<string name="icon_descr_video_call">видеозвонок</string> <string name="icon_descr_video_call">видеозвонок</string>
<string name="icon_descr_audio_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 --> <!-- Call overlay -->
<string name="status_e2e_encrypted">e2e зашифровано</string> <string name="status_e2e_encrypted">e2e зашифровано</string>
<string name="status_no_e2e_encryption">нет 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_video_call">video call</string>
<string name="icon_descr_audio_call">audio 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 --> <!-- Call overlay -->
<string name="status_e2e_encrypted">e2e encrypted</string> <string name="status_e2e_encrypted">e2e encrypted</string>
<string name="status_no_e2e_encryption">no e2e encryption</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.callState = .offerReceived
call.peerMedia = callType.media call.peerMedia = callType.media
call.sharedKey = sharedKey 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): case let .callAnswer(contact, answer):
withCall(contact) { call in withCall(contact) { call in

View file

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

View file

@ -19,7 +19,7 @@ class CallManager {
let m = ChatModel.shared let m = ChatModel.shared
if let call = m.activeCall, call.callkitUUID == callUUID { if let call = m.activeCall, call.callkitUUID == callUUID {
m.showCallView = true m.showCallView = true
m.callCommand = .capabilities(useWorker: true) m.callCommand = .capabilities(media: call.localMedia, useWorker: true)
return true return true
} }
return false return false
@ -45,7 +45,9 @@ class CallManager {
sharedKey: invitation.sharedKey sharedKey: invitation.sharedKey
) )
m.showCallView = true 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) { func endCall(callUUID: UUID, completed: @escaping (Bool) -> Void) {

View file

@ -102,7 +102,7 @@ struct WVAPIMessage: Equatable, Decodable, Encodable {
} }
enum WCallCommand: Equatable, Encodable, Decodable { 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 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 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) case answer(answer: String, iceCandidates: String)
@ -143,8 +143,9 @@ enum WCallCommand: Equatable, Encodable, Decodable {
func encode(to encoder: Encoder) throws { func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self) var container = encoder.container(keyedBy: CodingKeys.self)
switch self { switch self {
case let .capabilities(useWorker): case let .capabilities(media, useWorker):
try container.encode("capabilities", forKey: .type) try container.encode("capabilities", forKey: .type)
try container.encode(media, forKey: .media)
try container.encode(useWorker, forKey: .useWorker) try container.encode(useWorker, forKey: .useWorker)
case let .start(media, aesKey, useWorker, iceServers, relay): case let .start(media, aesKey, useWorker, iceServers, relay):
try container.encode("start", forKey: .type) 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) let type = try container.decode(String.self, forKey: CodingKeys.type)
switch type { switch type {
case "capabilities": case "capabilities":
let media = try container.decode(CallMediaType.self, forKey: CodingKeys.media)
let useWorker = try container.decode((Bool?).self, forKey: CodingKeys.useWorker) let useWorker = try container.decode((Bool?).self, forKey: CodingKeys.useWorker)
self = .capabilities(useWorker: useWorker) self = .capabilities(media: media, useWorker: useWorker)
case "start": case "start":
let media = try container.decode(CallMediaType.self, forKey: CodingKeys.media) let media = try container.decode(CallMediaType.self, forKey: CodingKeys.media)
let aesKey = try? container.decode(String.self, forKey: CodingKeys.aesKey) 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_USE_NOTIFICATIONS = "useNotifications"
let DEFAULT_PENDING_CONNECTIONS = "pendingConnections" 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 private var indent: CGFloat = 36
@ -59,6 +66,12 @@ struct SettingsView: View {
} label: { } label: {
settingsRow("server.rack") { Text("SMP servers") } settingsRow("server.rack") { Text("SMP servers") }
} }
NavigationLink {
CallSettings()
.navigationTitle("Call settings")
} label: {
settingsRow("video") { Text("Call settings") }
}
} }
Section("Help") { Section("Help") {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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