mobile: timeout call invitations, more android options (#703)

* mobile: timeout call invitations, more android options

* close overlays when call is accepted via notification

* show incoming call above modals, dismiss modals when call is accepted

* fix clickable area of create profile button

* fix pending intent for rullscreen notification, update settings
This commit is contained in:
Evgeny Poberezkin
2022-05-28 09:06:38 +01:00
committed by GitHub
parent da13e6614b
commit ce2f3c0371
22 changed files with 271 additions and 146 deletions
@@ -72,7 +72,7 @@ class MainActivity: FragmentActivity(), LifecycleEventObserver {
val m = vm.chatModel
val lastLAVal = lastLA.value
if (
m.controller.prefPerformLA.get()
m.controller.appPrefs.performLA.get()
&& (lastLAVal == null || (System.nanoTime() - lastLAVal >= 30 * 1e+9))
) {
userAuthorized.value = false
@@ -91,7 +91,7 @@ class MainActivity: FragmentActivity(), LifecycleEventObserver {
LAResult.Unavailable -> {
userAuthorized.value = true
m.performLA.value = false
m.controller.prefPerformLA.set(false)
m.controller.appPrefs.performLA.set(false)
laUnavailableTurningOffAlert()
}
}
@@ -106,13 +106,13 @@ class MainActivity: FragmentActivity(), LifecycleEventObserver {
}
private fun schedulePeriodicServiceRestartWorker() {
val workerVersion = chatController.prefAutoRestartWorkerVersion.get()
val workerVersion = chatController.appPrefs.autoRestartWorkerVersion.get()
val workPolicy = if (workerVersion == SimplexService.SERVICE_START_WORKER_VERSION) {
Log.d(TAG, "ServiceStartWorker version matches: choosing KEEP as existing work policy")
ExistingPeriodicWorkPolicy.KEEP
} else {
Log.d(TAG, "ServiceStartWorker version DOES NOT MATCH: choosing REPLACE as existing work policy")
chatController.prefAutoRestartWorkerVersion.set(SimplexService.SERVICE_START_WORKER_VERSION)
chatController.appPrefs.autoRestartWorkerVersion.set(SimplexService.SERVICE_START_WORKER_VERSION)
ExistingPeriodicWorkPolicy.REPLACE
}
val work = PeriodicWorkRequestBuilder<SimplexService.ServiceStartWorker>(SimplexService.SERVICE_START_WORKER_INTERVAL_MINUTES, TimeUnit.MINUTES)
@@ -126,33 +126,34 @@ class MainActivity: FragmentActivity(), LifecycleEventObserver {
private fun setPerformLA(on: Boolean) {
val m = vm.chatModel
if (on) {
m.controller.prefLANoticeShown.set(true)
m.controller.appPrefs.laNoticeShown.set(true)
authenticate(
generalGetString(R.string.auth_enable),
generalGetString(R.string.auth_confirm_credential),
this@MainActivity,
completed = { laResult ->
val prefPerformLA = m.controller.appPrefs.performLA
when (laResult) {
LAResult.Success -> {
m.performLA.value = true
m.controller.prefPerformLA.set(true)
prefPerformLA.set(true)
userAuthorized.value = true
lastLA.value = System.nanoTime()
laTurnedOnAlert()
}
is LAResult.Error -> {
m.performLA.value = false
m.controller.prefPerformLA.set(false)
prefPerformLA.set(false)
laErrorToast(applicationContext, laResult.errString)
}
LAResult.Failed -> {
m.performLA.value = false
m.controller.prefPerformLA.set(false)
prefPerformLA.set(false)
laFailedToast(applicationContext)
}
LAResult.Unavailable -> {
m.performLA.value = false
m.controller.prefPerformLA.set(false)
prefPerformLA.set(false)
laUnavailableInstructionAlert()
}
}
@@ -164,24 +165,25 @@ class MainActivity: FragmentActivity(), LifecycleEventObserver {
generalGetString(R.string.auth_confirm_credential),
this@MainActivity,
completed = { laResult ->
val prefPerformLA = m.controller.appPrefs.performLA
when (laResult) {
LAResult.Success -> {
m.performLA.value = false
m.controller.prefPerformLA.set(false)
prefPerformLA.set(false)
}
is LAResult.Error -> {
m.performLA.value = true
m.controller.prefPerformLA.set(true)
prefPerformLA.set(true)
laErrorToast(applicationContext, laResult.errString)
}
LAResult.Failed -> {
m.performLA.value = true
m.controller.prefPerformLA.set(true)
prefPerformLA.set(true)
laFailedToast(applicationContext)
}
LAResult.Unavailable -> {
m.performLA.value = false
m.controller.prefPerformLA.set(false)
prefPerformLA.set(false)
laUnavailableTurningOffAlert()
}
}
@@ -212,7 +214,7 @@ fun MainPage(
var showAdvertiseLAAlert by remember { mutableStateOf(false) }
LaunchedEffect(showAdvertiseLAAlert) {
if (
!chatModel.controller.prefLANoticeShown.get()
!chatModel.controller.appPrefs.laNoticeShown.get()
&& showAdvertiseLAAlert
&& chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete
&& chatModel.chats.isNotEmpty()
@@ -226,6 +228,12 @@ fun MainPage(
laUnavailableInstructionAlert()
}
}
LaunchedEffect(chatModel.clearOverlays.value) {
if (chatModel.clearOverlays.value) {
ModalManager.shared.closeModals()
chatModel.clearOverlays.value = false
}
}
Box {
val onboarding = chatModel.onboardingStage.value
val userCreated = chatModel.userCreated.value
@@ -240,8 +248,6 @@ fun MainPage(
if (chatModel.chatId.value == null) ChatListView(chatModel, setPerformLA = { setPerformLA(it) })
else ChatView(chatModel)
}
val invitation = chatModel.activeCallInvitation.value
if (invitation != null) IncomingCallAlertView(invitation, chatModel)
}
}
onboarding == OnboardingStage.Step1_SimpleXInfo ->
@@ -251,6 +257,8 @@ fun MainPage(
onboarding == OnboardingStage.Step2_CreateProfile -> CreateProfile(chatModel)
}
ModalManager.shared.showInView()
val invitation = chatModel.activeCallInvitation.value
if (invitation != null) IncomingCallAlertView(invitation, chatModel)
AlertManager.shared.showInView()
}
}
@@ -273,7 +281,9 @@ fun processNotificationIntent(intent: Intent?, chatModel: ChatModel) {
}
NtfManager.AcceptCallAction -> {
val chatId = intent.getStringExtra("chatId")
if (chatId == null || chatId == "") return
Log.d(TAG, "processNotificationIntent: AcceptCallAction $chatId")
chatModel.clearOverlays.value = true
val invitation = chatModel.callInvitations[chatId]
if (invitation == null) {
AlertManager.shared.showAlertMsg(generalGetString(R.string.call_already_ended))
@@ -29,7 +29,7 @@ external fun chatRecvMsg(ctrl: ChatCtrl) : String
class SimplexApp: Application(), LifecycleEventObserver {
val chatController: ChatController by lazy {
val ctrl = chatInit(getFilesDirectory(applicationContext))
ChatController(ctrl, ntfManager, applicationContext)
ChatController(ctrl, ntfManager, applicationContext, appPreferences)
}
val chatModel: ChatModel by lazy {
@@ -37,7 +37,11 @@ class SimplexApp: Application(), LifecycleEventObserver {
}
private val ntfManager: NtfManager by lazy {
NtfManager(applicationContext)
NtfManager(applicationContext, appPreferences)
}
private val appPreferences: AppPreferences by lazy {
AppPreferences(applicationContext)
}
override fun onCreate() {
@@ -61,7 +65,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
withApi {
when (event) {
Lifecycle.Event.ON_STOP ->
if (!chatController.prefRunServiceInBackground.get()) SimplexService.stop(applicationContext)
if (!appPreferences.runServiceInBackground.get()) SimplexService.stop(applicationContext)
Lifecycle.Event.ON_START ->
SimplexService.start(applicationContext)
Lifecycle.Event.ON_RESUME ->
@@ -1,6 +1,7 @@
package chat.simplex.app.model
import android.app.*
import android.app.Notification.VISIBILITY_PUBLIC
import android.content.*
import android.graphics.BitmapFactory
import android.media.AudioAttributes
@@ -14,7 +15,7 @@ import chat.simplex.app.views.helpers.base64ToBitmap
import chat.simplex.app.views.helpers.generalGetString
import kotlinx.datetime.Clock
class NtfManager(val context: Context) {
class NtfManager(val context: Context, private val appPreferences: AppPreferences) {
companion object {
const val MessageChannel: String = "chat.simplex.app.MESSAGE_NOTIFICATION"
const val MessageGroup: String = "chat.simplex.app.MESSAGE_NOTIFICATION"
@@ -64,21 +65,25 @@ class NtfManager(val context: Context) {
}
fun notifyMessageReceived(cInfo: ChatInfo, cItem: ChatItem) {
Log.d(TAG, "notifyMessageReceived ${cInfo.id}")
notifyMessageReceived(chatId = cInfo.id, displayName = cInfo.displayName, msgText = hideSecrets(cItem))
}
fun notifyMessageReceived(chatId: String, displayName: String, msgText: String) {
Log.d(TAG, "notifyMessageReceived $chatId")
val now = Clock.System.now().toEpochMilliseconds()
val recentNotification = (now - prevNtfTime.getOrDefault(cInfo.id, 0) < msgNtfTimeoutMs)
prevNtfTime[cInfo.id] = now
val recentNotification = (now - prevNtfTime.getOrDefault(chatId, 0) < msgNtfTimeoutMs)
prevNtfTime[chatId] = now
val notification = NotificationCompat.Builder(context, MessageChannel)
.setContentTitle(cInfo.displayName)
.setContentText(hideSecrets(cItem))
.setContentTitle(displayName)
.setContentText(msgText)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setGroup(MessageGroup)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
.setSmallIcon(R.drawable.ntf_icon)
.setColor(0x88FFFF)
.setAutoCancel(true)
.setContentIntent(chatPendingIntent(OpenChatAction, cInfo.id))
.setContentIntent(chatPendingIntent(OpenChatAction, chatId))
.setSilent(recentNotification)
.build()
@@ -93,7 +98,7 @@ class NtfManager(val context: Context) {
with(NotificationManagerCompat.from(context)) {
// using cInfo.id only shows one notification per chat and updates it when the message arrives
notify(cInfo.id.hashCode(), notification)
notify(chatId.hashCode(), notification)
notify(0, summary)
}
}
@@ -105,11 +110,12 @@ class NtfManager(val context: Context) {
val keyguardManager = getKeyguardManager(context)
val image = invitation.contact.image
var ntfBuilder =
if (keyguardManager.isDeviceLocked) {
if (keyguardManager.isDeviceLocked && appPreferences.callOnLockScreen.get() != CallOnLockScreen.DISABLE) {
val fullScreenIntent = Intent(context, IncomingCallActivity::class.java)
val fullScreenPendingIntent = PendingIntent.getActivity(context, 0, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT)
val fullScreenPendingIntent = PendingIntent.getActivity(context, 0, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
NotificationCompat.Builder(context, LockScreenCallChannel)
.setFullScreenIntent(fullScreenPendingIntent, true)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setSilent(true)
} else {
val soundUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.packageName + "/" + R.raw.ring_once)
@@ -46,22 +46,76 @@ fun isAppOnForeground(context: Context): Boolean {
return false
}
open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager, val appContext: Context) {
var chatModel = ChatModel(this)
private val sharedPreferences: SharedPreferences = appContext.getSharedPreferences(SHARED_PREFS_ID, Context.MODE_PRIVATE)
enum class CallOnLockScreen {
DISABLE,
SHOW,
ACCEPT;
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)
companion object {
val default = SHOW
}
}
class AppPreferences(val context: Context) {
private val sharedPreferences: SharedPreferences = context.getSharedPreferences(SHARED_PREFS_ID, Context.MODE_PRIVATE)
val runServiceInBackground = mkBoolPreference(SHARED_PREFS_RUN_SERVICE_IN_BACKGROUND, true)
val backgroundServiceNoticeShown = mkBoolPreference(SHARED_PREFS_SERVICE_NOTICE_SHOWN, false)
val backgroundServiceBatteryNoticeShown = mkBoolPreference(SHARED_PREFS_SERVICE_BATTERY_NOTICE_SHOWN, false)
val autoRestartWorkerVersion = mkIntPreference(SHARED_PREFS_AUTO_RESTART_WORKER_VERSION, 0)
val webrtcPolicyRelay = mkBoolPreference(SHARED_PREFS_WEBRTC_POLICY_RELAY, true)
private val _callOnLockScreen = mkStrPreference(SHARED_PREFS_WEBRTC_CALLS_ON_LOCK_SCREEN, CallOnLockScreen.default.name)
val callOnLockScreen: Preference<CallOnLockScreen> = Preference(
get = fun(): CallOnLockScreen {
val value = _callOnLockScreen.get() ?: return CallOnLockScreen.default
return try {
CallOnLockScreen.valueOf(value)
} catch (e: Error) {
CallOnLockScreen.default
}
},
set = fun(action: CallOnLockScreen) { _callOnLockScreen.set(action.name) }
)
val performLA = mkBoolPreference(SHARED_PREFS_PERFORM_LA, false)
val laNoticeShown = mkBoolPreference(SHARED_PREFS_LA_NOTICE_SHOWN, false)
private fun mkIntPreference(prefName: String, default: Int) =
Preference(
get = fun() = sharedPreferences.getInt(prefName, default),
set = fun(value) = sharedPreferences.edit().putInt(prefName, value).apply()
)
private fun mkBoolPreference(prefName: String, default: Boolean) =
Preference(
get = fun() = sharedPreferences.getBoolean(prefName, default),
set = fun(value) = sharedPreferences.edit().putBoolean(prefName, value).apply()
)
private fun mkStrPreference(prefName: String, default: String?): Preference<String?> =
Preference(
get = fun() = sharedPreferences.getString(prefName, default),
set = fun(value) = sharedPreferences.edit().putString(prefName, value).apply()
)
companion object {
private const val SHARED_PREFS_ID = "chat.simplex.app.SIMPLEX_APP_PREFS"
private const val SHARED_PREFS_AUTO_RESTART_WORKER_VERSION = "AutoRestartWorkerVersion"
private const val SHARED_PREFS_RUN_SERVICE_IN_BACKGROUND = "RunServiceInBackground"
private const val SHARED_PREFS_SERVICE_NOTICE_SHOWN = "BackgroundServiceNoticeShown"
private const val SHARED_PREFS_SERVICE_BATTERY_NOTICE_SHOWN = "BackgroundServiceBatteryNoticeShown"
private const val SHARED_PREFS_WEBRTC_POLICY_RELAY = "WebrtcPolicyRelay"
private const val SHARED_PREFS_WEBRTC_CALLS_ON_LOCK_SCREEN = "CallsOnLockScreen"
private const val SHARED_PREFS_PERFORM_LA = "PerformLA"
private const val SHARED_PREFS_LA_NOTICE_SHOWN = "LANoticeShown"
}
}
open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager, val appContext: Context, val appPrefs: AppPreferences) {
val chatModel = ChatModel(this)
init {
chatModel.runServiceInBackground.value = prefRunServiceInBackground.get()
chatModel.performLA.value = prefPerformLA.get()
chatModel.runServiceInBackground.value = appPrefs.runServiceInBackground.get()
chatModel.performLA.value = appPrefs.performLA.get()
}
suspend fun startChat(user: User) {
@@ -511,7 +565,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
}
}
is CR.CallInvitation -> {
val invitation = CallInvitation(r.contact, r.callType.media, r.sharedKey)
val invitation = CallInvitation(r.contact, r.callType.media, r.sharedKey, r.callTs)
chatModel.callManager.reportNewIncomingCall(invitation)
}
is CR.CallOffer -> {
@@ -519,7 +573,8 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
// TODO check encryption is compatible
withCall(r, r.contact) { call ->
chatModel.activeCall.value = call.copy(callState = CallState.OfferReceived, peerMedia = r.callType.media, sharedKey = r.sharedKey)
chatModel.callCommand.value = WCallCommand.Offer(offer = r.offer.rtcSession, iceCandidates = r.offer.rtcIceCandidates, media = r.callType.media, aesKey = r.sharedKey)
val useRelay = chatModel.controller.appPrefs.webrtcPolicyRelay.get()
chatModel.callCommand.value = WCallCommand.Offer(offer = r.offer.rtcSession, iceCandidates = r.offer.rtcIceCandidates, media = r.callType.media, aesKey = r.sharedKey, relay = useRelay)
}
}
is CR.CallAnswer -> {
@@ -592,7 +647,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
fun showBackgroundServiceNoticeIfNeeded() {
Log.d(TAG, "showBackgroundServiceNoticeIfNeeded")
if (!prefBackgroundServiceNoticeShown.get()) {
if (!appPrefs.backgroundServiceNoticeShown.get()) {
// the branch for the new users who has never seen service notice
if (isIgnoringBatteryOptimizations(appContext)) {
showBGServiceNotice()
@@ -600,20 +655,20 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
showBGServiceNoticeIgnoreOptimization()
}
// set both flags, so that if the user doesn't allow ignoring optimizations, the service will be disabled without additional notice
prefBackgroundServiceNoticeShown.set(true)
prefBackgroundServiceBatteryNoticeShown.set(true)
} else if (!isIgnoringBatteryOptimizations(appContext) && prefRunServiceInBackground.get()) {
appPrefs.backgroundServiceNoticeShown.set(true)
appPrefs.backgroundServiceBatteryNoticeShown.set(true)
} else if (!isIgnoringBatteryOptimizations(appContext) && appPrefs.runServiceInBackground.get()) {
// the branch for users who have app installed, and have seen the service notice,
// but the battery optimization for the app is on (Android 12) AND the service is running
if (prefBackgroundServiceBatteryNoticeShown.get()) {
if (appPrefs.backgroundServiceBatteryNoticeShown.get()) {
// users have been presented with battery notice before - they did not allow ignoring optimizitions -> disable service
showDisablingServiceNotice()
prefRunServiceInBackground.set(false)
appPrefs.runServiceInBackground.set(false)
chatModel.runServiceInBackground.value = false
} else {
// show battery optimization notice
showBGServiceNoticeIgnoreOptimization()
prefBackgroundServiceBatteryNoticeShown.set(true)
appPrefs.backgroundServiceBatteryNoticeShown.set(true)
}
}
}
@@ -704,8 +759,8 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
fun showLANotice(activity: FragmentActivity) {
Log.d(TAG, "showLANotice")
if (!prefLANoticeShown.get()) {
prefLANoticeShown.set(true)
if (!appPrefs.laNoticeShown.get()) {
appPrefs.laNoticeShown.set(true)
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.la_notice_title),
text = generalGetString(R.string.la_notice_text),
@@ -719,22 +774,22 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
when (laResult) {
LAResult.Success -> {
chatModel.performLA.value = true
prefPerformLA.set(true)
appPrefs.performLA.set(true)
laTurnedOnAlert()
}
is LAResult.Error -> {
chatModel.performLA.value = false
prefPerformLA.set(false)
appPrefs.performLA.set(false)
laErrorToast(appContext, laResult.errString)
}
LAResult.Failed -> {
chatModel.performLA.value = false
prefPerformLA.set(false)
appPrefs.performLA.set(false)
laFailedToast(appContext)
}
LAResult.Unavailable -> {
chatModel.performLA.value = false
prefPerformLA.set(false)
appPrefs.performLA.set(false)
chatModel.showAdvertiseLAUnavailableAlert.value = true
}
}
@@ -763,30 +818,6 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
context.startActivity(this)
}
}
private fun mkIntPreference(prefName: String, default: Int) =
Preference(
get = fun() = sharedPreferences.getInt(prefName, default),
set = fun(value) = sharedPreferences.edit().putInt(prefName, value).apply()
)
private fun mkBoolPreference(prefName: String, default: Boolean) =
Preference(
get = fun() = sharedPreferences.getBoolean(prefName, default),
set = fun(value) = sharedPreferences.edit().putBoolean(prefName, value).apply()
)
companion object {
private const val SHARED_PREFS_ID = "chat.simplex.app.SIMPLEX_APP_PREFS"
private const val SHARED_PREFS_AUTO_RESTART_WORKER_VERSION = "AutoRestartWorkerVersion"
private const val SHARED_PREFS_RUN_SERVICE_IN_BACKGROUND = "RunServiceInBackground"
private const val SHARED_PREFS_SERVICE_NOTICE_SHOWN = "BackgroundServiceNoticeShown"
private const val SHARED_PREFS_SERVICE_BATTERY_NOTICE_SHOWN = "BackgroundServiceBatteryNoticeShown"
private const val SHARED_PREFS_WEBRTC_POLICY_RELAY = "WebrtcPolicyRelay"
private const val SHARED_PREFS_WEBRTC_ACCEPT_CALLS_FROM_LOCK_SCREEN = "AcceptCallsFromLockScreen"
private const val SHARED_PREFS_PERFORM_LA = "PerformLA"
private const val SHARED_PREFS_LA_NOTICE_SHOWN = "LANoticeShown"
}
}
class Preference<T>(val get: () -> T, val set: (T) -> Unit)
@@ -982,7 +1013,7 @@ sealed class CR {
@Serializable @SerialName("sndFileCancelled") class SndFileCancelled(val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR()
@Serializable @SerialName("sndFileRcvCancelled") class SndFileRcvCancelled(val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR()
@Serializable @SerialName("sndGroupFileCancelled") class SndGroupFileCancelled(val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta, val sndFileTransfers: List<SndFileTransfer>): CR()
@Serializable @SerialName("callInvitation") class CallInvitation(val contact: Contact, val callType: CallType, val sharedKey: String? = null): CR()
@Serializable @SerialName("callInvitation") class CallInvitation(val contact: Contact, val callType: CallType, val sharedKey: String? = null, val callTs: Instant): CR()
@Serializable @SerialName("callOffer") class CallOffer(val contact: Contact, val callType: CallType, val offer: WebRTCSession, val sharedKey: String? = null, val askConfirmation: Boolean): CR()
@Serializable @SerialName("callAnswer") class CallAnswer(val contact: Contact, val answer: WebRTCSession): CR()
@Serializable @SerialName("callExtraInfo") class CallExtraInfo(val contact: Contact, val extraInfo: WebRTCExtraInfo): CR()
@@ -87,7 +87,7 @@ fun CreateProfilePanel(chatModel: ChatModel) {
val createModifier: Modifier
val createColor: Color
if (enabled) {
createModifier = Modifier.padding(8.dp).clickable { createProfile(chatModel, displayName.value, fullName.value) }
createModifier = Modifier.clickable { createProfile(chatModel, displayName.value, fullName.value) }.padding(8.dp)
createColor = MaterialTheme.colors.primary
} else {
createModifier = Modifier.padding(8.dp)
@@ -3,19 +3,28 @@ package chat.simplex.app.views.call
import android.util.Log
import chat.simplex.app.TAG
import chat.simplex.app.model.ChatModel
import chat.simplex.app.views.helpers.ModalManager
import chat.simplex.app.views.helpers.withApi
import kotlinx.datetime.Clock
import kotlin.time.Duration.Companion.minutes
class CallManager(val chatModel: ChatModel) {
fun reportNewIncomingCall(invitation: CallInvitation) {
Log.d(TAG, "CallManager.reportNewIncomingCall")
with (chatModel) {
callInvitations[invitation.contact.id] = invitation
activeCallInvitation.value = invitation
controller.ntfManager.notifyCallInvitation(invitation)
if (Clock.System.now() - invitation.callTs <= 3.minutes) {
activeCallInvitation.value = invitation
controller.ntfManager.notifyCallInvitation(invitation)
} else {
val contact = invitation.contact
controller.ntfManager.notifyMessageReceived(chatId = contact.id, displayName = contact.displayName, msgText = invitation.callTypeText)
}
}
}
fun acceptIncomingCall(invitation: CallInvitation) {
ModalManager.shared.closeModals()
val call = chatModel.activeCall.value
if (call == null) {
justAcceptIncomingCall(invitation = invitation)
@@ -41,7 +50,8 @@ class CallManager(val chatModel: ChatModel) {
sharedKey = invitation.sharedKey
)
showCallView.value = true
callCommand.value = WCallCommand.Start (media = invitation.peerMedia, aesKey = invitation.sharedKey)
val useRelay = controller.appPrefs.webrtcPolicyRelay.get()
callCommand.value = WCallCommand.Start (media = invitation.peerMedia, aesKey = invitation.sharedKey, relay = useRelay)
callInvitations.remove(invitation.contact.id)
if (invitation.contact.id == activeCallInvitation.value?.contact?.id) {
activeCallInvitation.value = null
@@ -39,14 +39,10 @@ import kotlinx.serialization.encodeToString
@Composable
fun ActiveCallView(chatModel: ChatModel) {
val endCall = {
Log.d(TAG, "ActiveCallView: endCall")
chatModel.activeCall.value = null
chatModel.activeCallInvitation.value = null
chatModel.callCommand.value = null
chatModel.showCallView.value = false
}
BackHandler(onBack = endCall)
BackHandler(onBack = {
val call = chatModel.activeCall.value
if (call != null) withApi { chatModel.callManager.endCall(call) }
})
Box(Modifier.fillMaxSize()) {
WebRTCView(chatModel.callCommand) { apiMsg ->
Log.d(TAG, "received from WebRTCView: $apiMsg")
@@ -85,7 +81,8 @@ fun ActiveCallView(chatModel: ChatModel) {
}
is WCallResponse.Ended -> {
chatModel.activeCall.value = call.copy(callState = CallState.Ended)
endCall()
withApi { chatModel.callManager.endCall(call) }
chatModel.showCallView.value = false
}
is WCallResponse.Ok -> when (val cmd = apiMsg.command) {
is WCallCommand.Answer ->
@@ -102,7 +99,8 @@ fun ActiveCallView(chatModel: ChatModel) {
chatModel.callCommand.value = WCallCommand.Media(CallMediaType.Audio, enable = false)
}
}
is WCallCommand.End -> endCall()
is WCallCommand.End ->
chatModel.showCallView.value = false
else -> {}
}
is WCallResponse.Error -> {
@@ -112,21 +110,15 @@ fun ActiveCallView(chatModel: ChatModel) {
}
}
val call = chatModel.activeCall.value
if (call != null) ActiveCallOverlay(call, chatModel, endCall)
if (call != null) ActiveCallOverlay(call, chatModel)
}
}
@Composable
private fun ActiveCallOverlay(call: Call, chatModel: ChatModel, endCall: () -> Unit) {
private fun ActiveCallOverlay(call: Call, chatModel: ChatModel) {
ActiveCallOverlayLayout(
call = call,
dismiss = {
chatModel.callCommand.value = WCallCommand.End
withApi {
chatModel.controller.apiEndCall(call.contact)
endCall()
}
},
dismiss = { withApi { chatModel.callManager.endCall(call) } },
toggleAudio = { chatModel.callCommand.value = WCallCommand.Media(CallMediaType.Audio, enable = !call.audioEnabled) },
toggleVideo = { chatModel.callCommand.value = WCallCommand.Media(CallMediaType.Video, enable = !call.videoEnabled) },
flipCamera = { chatModel.callCommand.value = WCallCommand.Camera(call.localCamera.flipped) }
@@ -30,12 +30,12 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
import chat.simplex.app.model.Contact
import chat.simplex.app.model.*
import chat.simplex.app.model.NtfManager.Companion.OpenChatAction
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.ProfileImage
import chat.simplex.app.views.onboarding.SimpleXLogo
import kotlinx.datetime.Clock
class IncomingCallActivity: ComponentActivity() {
private val vm by viewModels<SimplexViewModel>()
@@ -115,12 +115,12 @@ fun IncomingCallLockScreenAlert(invitation: CallInvitation, chatModel: ChatModel
val cm = chatModel.callManager
val cxt = LocalContext.current
val scope = rememberCoroutineScope()
var acceptCallsFromLockScreen by remember { mutableStateOf(chatModel.controller.prefAcceptCallsFromLockScreen.get()) }
var callOnLockScreen by remember { mutableStateOf(chatModel.controller.appPrefs.callOnLockScreen.get()) }
LaunchedEffect(true) { SoundPlayer.shared.start(cxt, scope, sound = true) }
DisposableEffect(true) { onDispose { SoundPlayer.shared.stop() } }
IncomingCallLockScreenAlertLayout(
invitation,
acceptCallsFromLockScreen,
callOnLockScreen,
rejectCall = { cm.endCall(invitation = invitation) },
ignoreCall = { chatModel.activeCallInvitation.value = null },
acceptCall = { cm.acceptIncomingCall(invitation = invitation) },
@@ -141,7 +141,7 @@ fun IncomingCallLockScreenAlert(invitation: CallInvitation, chatModel: ChatModel
@Composable
fun IncomingCallLockScreenAlertLayout(
invitation: CallInvitation,
acceptCallsFromLockScreen: Boolean,
callOnLockScreen: CallOnLockScreen?,
rejectCall: () -> Unit,
ignoreCall: () -> Unit,
acceptCall: () -> Unit,
@@ -155,7 +155,7 @@ fun IncomingCallLockScreenAlertLayout(
) {
IncomingCallInfo(invitation)
Spacer(Modifier.fillMaxHeight().weight(1f))
if (acceptCallsFromLockScreen) {
if (callOnLockScreen == CallOnLockScreen.ACCEPT) {
ProfileImage(size = 192.dp, image = invitation.contact.profile.image)
Text(invitation.contact.chatViewName, style = MaterialTheme.typography.h2)
Spacer(Modifier.fillMaxHeight().weight(1f))
@@ -166,7 +166,7 @@ fun IncomingCallLockScreenAlertLayout(
Spacer(Modifier.size(48.dp))
LockScreenCallButton(stringResource(R.string.accept), Icons.Filled.Check, SimplexGreen, acceptCall)
}
} else {
} else if (callOnLockScreen == CallOnLockScreen.SHOW) {
SimpleXLogo()
Text(stringResource(R.string.open_simplex_chat_to_accept_call), textAlign = TextAlign.Center, lineHeight = 22.sp)
Text(stringResource(R.string.allow_accepting_calls_from_lock_screen), textAlign = TextAlign.Center, style = MaterialTheme.typography.body2, lineHeight = 22.sp)
@@ -212,9 +212,10 @@ fun PreviewIncomingCallLockScreenAlert() {
invitation = CallInvitation(
contact = Contact.sampleData,
peerMedia = CallMediaType.Audio,
sharedKey = null
sharedKey = null,
callTs = Clock.System.now()
),
acceptCallsFromLockScreen = false,
callOnLockScreen = null,
rejectCall = {},
ignoreCall = {},
acceptCall = {},
@@ -22,6 +22,7 @@ import chat.simplex.app.model.ChatModel
import chat.simplex.app.model.Contact
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.usersettings.ProfilePreview
import kotlinx.datetime.Clock
@Composable
fun IncomingCallAlertView(invitation: CallInvitation, chatModel: ChatModel) {
@@ -97,7 +98,8 @@ fun PreviewIncomingCallAlertLayout() {
invitation = CallInvitation(
contact = Contact.sampleData,
peerMedia = CallMediaType.Audio,
sharedKey = null
sharedKey = null,
callTs = Clock.System.now()
),
rejectCall = {},
ignoreCall = {},
@@ -5,6 +5,7 @@ import androidx.compose.ui.res.stringResource
import chat.simplex.app.R
import chat.simplex.app.model.Contact
import chat.simplex.app.views.helpers.generalGetString
import kotlinx.datetime.Instant
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@@ -89,7 +90,7 @@ sealed class WCallResponse {
@Serializable class WebRTCSession(val rtcSession: String, val rtcIceCandidates: String)
@Serializable class WebRTCExtraInfo(val rtcIceCandidates: String)
@Serializable class CallType(val media: CallMediaType, val capabilities: CallCapabilities)
@Serializable class CallInvitation(val contact: Contact, val peerMedia: CallMediaType, val sharedKey: String?) {
@Serializable class CallInvitation(val contact: Contact, val peerMedia: CallMediaType, val sharedKey: String?, val callTs: Instant) {
val callTypeText: String get() = generalGetString(when(peerMedia) {
CallMediaType.Video -> if (sharedKey == null) R.string.video_call_no_encryption else R.string.encrypted_video_call
CallMediaType.Audio -> if (sharedKey == null) R.string.audio_call_no_encryption else R.string.encrypted_audio_call
@@ -66,10 +66,8 @@ fun scaffoldController(): ScaffoldController {
@Composable
fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
val scaffoldCtrl = scaffoldController()
if (chatModel.clearOverlays.value) {
scaffoldCtrl.collapse()
ModalManager.shared.closeModal()
chatModel.clearOverlays.value = false
LaunchedEffect(chatModel.clearOverlays.value) {
if (chatModel.clearOverlays.value && scaffoldCtrl.expanded.value) scaffoldCtrl.collapse()
}
BottomSheetScaffold(
scaffoldState = scaffoldCtrl.state,
@@ -48,6 +48,10 @@ class ModalManager {
modalCount.value = modalViews.count()
}
fun closeModals() {
while (modalViews.isNotEmpty()) closeModal()
}
@Composable
fun showInView() {
if (modalCount.value > 0) modalViews.lastOrNull()?.invoke(::closeModal)
@@ -5,7 +5,10 @@ import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.model.*
@@ -14,28 +17,38 @@ import chat.simplex.app.ui.theme.HighOrLowlight
@Composable
fun CallSettingsView(m: ChatModel) {
CallSettingsLayout(
webrtcPolicyRelay = m.controller.prefWebrtcPolicyRelay,
acceptCallsFromLockScreen = m.controller.prefAcceptCallsFromLockScreen
webrtcPolicyRelay = m.controller.appPrefs.webrtcPolicyRelay,
callOnLockScreen = m.controller.appPrefs.callOnLockScreen
)
}
@Composable
fun CallSettingsLayout(
webrtcPolicyRelay: Preference<Boolean>,
acceptCallsFromLockScreen: Preference<Boolean>,
callOnLockScreen: Preference<CallOnLockScreen>,
) {
Column(
Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
val lockCallState = remember { mutableStateOf(callOnLockScreen.get()) }
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)
Column {
Text(stringResource(R.string.call_on_lock_screen))
Row {
SharedPreferenceRadioButton(stringResource(R.string.no_call_on_lock_screen), lockCallState, callOnLockScreen, CallOnLockScreen.DISABLE)
Spacer(Modifier.fillMaxWidth().weight(1f))
SharedPreferenceRadioButton(stringResource(R.string.show_call_on_lock_screen), lockCallState, callOnLockScreen, CallOnLockScreen.SHOW)
Spacer(Modifier.fillMaxWidth().weight(1f))
SharedPreferenceRadioButton(stringResource(R.string.accept_call_on_lock_screen), lockCallState, callOnLockScreen, CallOnLockScreen.ACCEPT)
}
}
}
}
@@ -61,3 +74,15 @@ fun SharedPreferenceToggle(
)
}
}
@Composable
fun <T>SharedPreferenceRadioButton(text: String, prefState: MutableState<T>, preference: Preference<T>, value: T) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(text)
val colors = RadioButtonDefaults.colors(selectedColor = MaterialTheme.colors.primary)
RadioButton(selected = prefState.value == value, colors = colors, onClick = {
preference.set(value)
prefState.value = value
})
}
}
@@ -34,9 +34,9 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
val user = chatModel.currentUser.value
fun setRunServiceInBackground(on: Boolean) {
chatModel.controller.prefRunServiceInBackground.set(on)
chatModel.controller.appPrefs.runServiceInBackground.set(on)
if (on && !chatModel.controller.isIgnoringBatteryOptimizations(chatModel.controller.appContext)) {
chatModel.controller.prefBackgroundServiceNoticeShown.set(false)
chatModel.controller.appPrefs.backgroundServiceNoticeShown.set(false)
}
chatModel.controller.showBackgroundServiceNoticeIfNeeded()
chatModel.runServiceInBackground.value = on
@@ -135,6 +135,7 @@ fun SettingsLayout(
Icon(
Icons.Outlined.QrCode,
contentDescription = stringResource(R.string.icon_descr_address),
tint = HighOrLowlight,
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(stringResource(R.string.your_simplex_contact_address))
@@ -146,6 +147,7 @@ fun SettingsLayout(
Icon(
Icons.Outlined.Videocam,
contentDescription = stringResource(R.string.call_settings),
tint = HighOrLowlight,
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(stringResource(R.string.call_settings))
@@ -157,6 +159,7 @@ fun SettingsLayout(
Icon(
Icons.Outlined.HelpOutline,
contentDescription = stringResource(R.string.icon_descr_help),
tint = HighOrLowlight,
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(stringResource(R.string.how_to_use_simplex_chat))
@@ -168,6 +171,7 @@ fun SettingsLayout(
Icon(
Icons.Outlined.Info,
contentDescription = stringResource(R.string.icon_descr_help),
tint = HighOrLowlight,
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(stringResource(R.string.about_simplex_chat))
@@ -179,6 +183,7 @@ fun SettingsLayout(
Icon(
Icons.Outlined.TextFormat,
contentDescription = stringResource(R.string.markdown_help),
tint = HighOrLowlight,
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(stringResource(R.string.markdown_in_messages))
@@ -190,6 +195,7 @@ fun SettingsLayout(
Icon(
Icons.Outlined.Tag,
contentDescription = stringResource(R.string.icon_descr_simplex_team),
tint = HighOrLowlight,
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(
@@ -204,6 +210,7 @@ fun SettingsLayout(
Icon(
Icons.Outlined.Email,
contentDescription = stringResource(R.string.icon_descr_email),
tint = HighOrLowlight,
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(
@@ -218,6 +225,7 @@ fun SettingsLayout(
Icon(
Icons.Outlined.Dns,
contentDescription = stringResource(R.string.smp_servers),
tint = HighOrLowlight,
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(stringResource(R.string.smp_servers))
@@ -233,6 +241,7 @@ fun SettingsLayout(
Icon(
Icons.Outlined.Bolt,
contentDescription = stringResource(R.string.private_notifications),
tint = HighOrLowlight,
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(
@@ -261,6 +270,7 @@ fun SettingsLayout(
Icon(
Icons.Outlined.Lock,
contentDescription = stringResource(R.string.chat_lock),
tint = HighOrLowlight,
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(
@@ -287,6 +297,7 @@ fun SettingsLayout(
Icon(
painter = painterResource(id = R.drawable.ic_outline_terminal),
contentDescription = stringResource(R.string.chat_console),
tint = HighOrLowlight,
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(stringResource(R.string.chat_console))
@@ -298,6 +309,7 @@ fun SettingsLayout(
Icon(
painter = painterResource(id = R.drawable.ic_github),
contentDescription = "GitHub",
tint = HighOrLowlight,
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(annotatedStringResource(R.string.install_simplex_chat_for_terminal))
@@ -380,12 +380,15 @@
<!-- Call settings -->
<string name="call_settings">Настройки звонков</string>
<string name="connect_calls_via_relay">Соединяться через сервер (relay)</string>
<string name="accept_calls_from_lock_screen">Принимать с экрана блокировки</string>
<string name="call_on_lock_screen">Звонки на экране блокировки:</string>
<string name="accept_call_on_lock_screen">Принимать</string>
<string name="show_call_on_lock_screen">Показывать</string>
<string name="no_call_on_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>
<string name="open_simplex_chat_to_accept_call">Откройте <xliff:g id="appNameFull">SimpleX Chat</xliff:g>\nчтобы принять звонок</string>
<string name="allow_accepting_calls_from_lock_screen">Вы можете разрешить принимать звонки на экране блокировки через Настройки.</string>
<string name="open_verb">Открыть</string>
<!-- Call overlay -->
<string name="status_e2e_encrypted">e2e зашифровано</string>
@@ -382,7 +382,10 @@
<!-- 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>
<string name="call_on_lock_screen">Calls on lock screen:</string>
<string name="accept_call_on_lock_screen">Accept</string>
<string name="show_call_on_lock_screen">Show</string>
<string name="no_call_on_lock_screen">Disable</string>
<!-- Call Lock Screen -->
<string name="open_simplex_chat_to_accept_call">Open <xliff:g id="appNameFull">SimpleX Chat</xliff:g> to accept call</string>
+2 -2
View File
@@ -199,7 +199,7 @@ enum ChatResponse: Decodable, Error {
case sndFileCancelled(chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
case sndFileRcvCancelled(chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
case sndGroupFileCancelled(chatItem: AChatItem, fileTransferMeta: FileTransferMeta, sndFileTransfers: [SndFileTransfer])
case callInvitation(contact: Contact, callType: CallType, sharedKey: String?)
case callInvitation(contact: Contact, callType: CallType, sharedKey: String?, callTs: Date)
case callOffer(contact: Contact, callType: CallType, offer: WebRTCSession, sharedKey: String?, askConfirmation: Bool)
case callAnswer(contact: Contact, answer: WebRTCSession)
case callExtraInfo(contact: Contact, extraInfo: WebRTCExtraInfo)
@@ -322,7 +322,7 @@ enum ChatResponse: Decodable, Error {
case let .sndFileCancelled(chatItem, _): return String(describing: chatItem)
case let .sndFileRcvCancelled(chatItem, _): return String(describing: chatItem)
case let .sndGroupFileCancelled(chatItem, _, _): return String(describing: chatItem)
case let .callInvitation(contact, callType, sharedKey): return "contact: \(contact.id)\ncallType: \(String(describing: callType))\nsharedKey: \(sharedKey ?? "")"
case let .callInvitation(contact, callType, sharedKey, _): return "contact: \(contact.id)\ncallType: \(String(describing: callType))\nsharedKey: \(sharedKey ?? "")"
case let .callOffer(contact, callType, offer, sharedKey, askConfirmation): return "contact: \(contact.id)\ncallType: \(String(describing: callType))\nsharedKey: \(sharedKey ?? "")\naskConfirmation: \(askConfirmation)\noffer: \(String(describing: offer))"
case let .callAnswer(contact, answer): return "contact: \(contact.id)\nanswer: \(String(describing: answer))"
case let .callExtraInfo(contact, extraInfo): return "contact: \(contact.id)\nextraInfo: \(String(describing: extraInfo))"
+3 -1
View File
@@ -28,6 +28,7 @@ struct CallInvitation {
var callkitUUID: UUID?
var peerMedia: CallMediaType
var sharedKey: String?
var callTs: Date
var callTypeText: LocalizedStringKey {
get {
switch peerMedia {
@@ -39,7 +40,8 @@ struct CallInvitation {
static let sampleData = CallInvitation(
contact: Contact.sampleData,
peerMedia: .audio
peerMedia: .audio,
callTs: .now
)
}
+2 -2
View File
@@ -629,9 +629,9 @@ func processReceivedMsg(_ res: ChatResponse) {
let fileName = cItem.file?.filePath {
removeFile(fileName)
}
case let .callInvitation(contact, callType, sharedKey):
case let .callInvitation(contact, callType, sharedKey, callTs):
let uuid = UUID()
var invitation = CallInvitation(contact: contact, callkitUUID: uuid, peerMedia: callType.media, sharedKey: sharedKey)
var invitation = CallInvitation(contact: contact, callkitUUID: uuid, peerMedia: callType.media, sharedKey: sharedKey, callTs: callTs)
m.callInvitations[contact.id] = invitation
CallController.shared.reportNewIncomingCall(invitation: invitation) { error in
if let error = error {
@@ -127,7 +127,9 @@ class CallController: NSObject, CXProviderDelegate, ObservableObject {
provider.reportNewIncomingCall(with: uuid, update: update, completion: completion)
} else {
NtfManager.shared.notifyCallInvitation(invitation)
activeCallInvitation = invitation
if invitation.callTs.timeIntervalSinceNow >= -180 {
activeCallInvitation = invitation
}
}
}
@@ -12,8 +12,31 @@ struct CallSettings: View {
@AppStorage(DEFAULT_WEBRTC_POLICY_RELAY) private var webrtcPolicyRelay = true
var body: some View {
List {
Toggle("Connect via relay", isOn: $webrtcPolicyRelay)
VStack {
List {
Section("Settings") {
Toggle("Connect via relay", isOn: $webrtcPolicyRelay)
}
Section("Limitations") {
VStack(alignment: .leading, spacing: 8) {
textListItem("1.", "Do NOT use SimpleX for emergency calls.")
textListItem("2.", "Pre-arrange the calls, as notifications arrive with a delay (we are improving it).")
textListItem("3.", "The microphone does not work when the app is in the background.")
textListItem("4.", "To prevent the call interruption, enable Do Not Disturb mode.")
textListItem("5.", "If the video fails to connect, flip the camera to resolve it.")
}
.font(.callout)
.padding(.vertical, 8)
}
}
}
}
private func textListItem(_ n: String, _ text: LocalizedStringKey) -> some View {
ZStack(alignment: .topLeading) {
Text(n)
Text(text).frame(maxWidth: .infinity, alignment: .leading).padding(.leading, 20)
}
}
}
@@ -119,6 +119,7 @@ struct SettingsView: View {
Image(colorScheme == .dark ? "github_light" : "github")
.resizable()
.frame(width: 24, height: 24)
.opacity(0.5)
Text("Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)")
.padding(.leading, indent)
}
@@ -128,12 +129,7 @@ struct SettingsView: View {
// notificationsToggle(token)
// }
// }
// NavigationLink {
// CallViewDebug()
// .frame(maxHeight: .infinity, alignment: .top)
// } label: {
Text("v\(appVersion ?? "?") (\(appBuild ?? "?"))")
// }
Text("v\(appVersion ?? "?") (\(appBuild ?? "?"))")
}
}
.navigationTitle("Your settings")
@@ -142,7 +138,7 @@ struct SettingsView: View {
private func settingsRow<Content : View>(_ icon: String, content: @escaping () -> Content) -> some View {
ZStack(alignment: .leading) {
Image(systemName: icon).frame(maxWidth: 24, maxHeight: 24, alignment: .center)
Image(systemName: icon).frame(maxWidth: 24, maxHeight: 24, alignment: .center).foregroundColor(.secondary)
content().padding(.leading, indent)
}
}