diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift
index af5f3c3ee9..be2cde7cd6 100644
--- a/apps/ios/Shared/Model/SimpleXAPI.swift
+++ b/apps/ios/Shared/Model/SimpleXAPI.swift
@@ -2914,23 +2914,19 @@ func processReceivedMsg(_ res: ChatEvent) async {
m.callInvitations[invitation.contact.id] = invitation
}
activateCall(invitation)
- case let .callOffer(_, contact, callType, offer, sharedKey, _):
+ case let .callOffer(_, contact, callType, offer, sharedKey, askConfirmation):
await withCall(contact) { call in
await MainActor.run {
call.callState = .offerReceived
call.sharedKey = sharedKey
}
- let useRelay = UserDefaults.standard.bool(forKey: DEFAULT_WEBRTC_POLICY_RELAY)
- let iceServers = getIceServers()
- logger.debug(".callOffer useRelay \(useRelay)")
- logger.debug(".callOffer iceServers \(String(describing: iceServers))")
- await m.callCommand.processCommand(.offer(
- offer: offer.rtcSession,
- iceCandidates: offer.rtcIceCandidates,
- media: callType.media, aesKey: sharedKey,
- iceServers: iceServers,
- relay: useRelay
- ))
+ if askConfirmation {
+ showUnencryptedCallAlert(call) {
+ Task { await processCallOffer(callType, offer, sharedKey) }
+ }
+ } else {
+ await processCallOffer(callType, offer, sharedKey)
+ }
}
case let .callAnswer(_, contact, answer):
await withCall(contact) { call in
@@ -3067,6 +3063,43 @@ func processReceivedMsg(_ res: ChatEvent) async {
logger.debug("processReceivedMsg: ignoring \(res.responseType), not in call with the contact \(contact.id)")
}
}
+
+ func processCallOffer(_ callType: CallType, _ offer: WebRTCSession, _ sharedKey: String?) async {
+ let useRelay = UserDefaults.standard.bool(forKey: DEFAULT_WEBRTC_POLICY_RELAY)
+ let iceServers = getIceServers()
+ logger.debug(".callOffer useRelay \(useRelay)")
+ logger.debug(".callOffer iceServers \(String(describing: iceServers))")
+ await m.callCommand.processCommand(.offer(
+ offer: offer.rtcSession,
+ iceCandidates: offer.rtcIceCandidates,
+ media: callType.media, aesKey: sharedKey,
+ iceServers: iceServers,
+ relay: useRelay
+ ))
+ }
+
+ func showUnencryptedCallAlert(_ call: Call, onContinue: @escaping () -> Void) {
+ DispatchQueue.main.async {
+ showAlert(
+ NSLocalizedString("Call is not encrypted", comment: "alert title"),
+ message: String.localizedStringWithFormat(NSLocalizedString("%@ accepted the call without end-to-end encryption.", comment: "alert message"), call.contact.displayName),
+ actions: {[
+ UIAlertAction(title: NSLocalizedString("End call", comment: "alert action"), style: .destructive) { _ in
+ // the call may have ended, or a new one started with the same contact, while the alert was shown
+ guard m.activeCall === call else { return }
+ if let uuid = call.callUUID {
+ CallController.shared.endCall(callUUID: uuid)
+ } else {
+ CallController.shared.endCall(call: call) {}
+ }
+ },
+ UIAlertAction(title: NSLocalizedString("Continue", comment: "alert action"), style: .default) { _ in
+ if m.activeCall === call { onContinue() }
+ }
+ ]}
+ )
+ }
+ }
}
func switchToLocalSession() {
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
index 96515fbde2..e3bea95c03 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
@@ -3371,20 +3371,13 @@ object ChatController {
chatModel.callManager.reportNewIncomingCall(r.callInvitation.copy(remoteHostId = rhId))
}
is CR.CallOffer -> {
- // TODO askConfirmation?
- // TODO check encryption is compatible
withCall(r, r.contact) { call ->
chatModel.activeCall.value = call.copy(callState = CallState.OfferReceived, hasSharedKey = r.sharedKey != null)
- val useRelay = appPrefs.webrtcPolicyRelay.get()
- val iceServers = getIceServers()
- chatModel.callCommand.add(WCallCommand.Offer(
- offer = r.offer.rtcSession,
- iceCandidates = r.offer.rtcIceCandidates,
- media = r.callType.media,
- aesKey = r.sharedKey,
- iceServers = iceServers,
- relay = useRelay
- ))
+ if (r.askConfirmation) {
+ showUnencryptedCallAlert(call) { processCallOffer(r) }
+ } else {
+ processCallOffer(r)
+ }
}
}
is CR.CallAnswer -> {
@@ -3640,6 +3633,39 @@ object ChatController {
}
}
+ private fun processCallOffer(r: CR.CallOffer) {
+ val useRelay = appPrefs.webrtcPolicyRelay.get()
+ val iceServers = getIceServers()
+ chatModel.callCommand.add(WCallCommand.Offer(
+ offer = r.offer.rtcSession,
+ iceCandidates = r.offer.rtcIceCandidates,
+ media = r.callType.media,
+ aesKey = r.sharedKey,
+ iceServers = iceServers,
+ relay = useRelay
+ ))
+ }
+
+ private fun showUnencryptedCallAlert(call: Call, onContinue: () -> Unit) {
+ // the call may have ended, or a new one started with the same contact, while the alert was shown
+ fun offerPending(): Boolean {
+ val c = chatModel.activeCall.value
+ return c != null && c.remoteHostId == call.remoteHostId && c.contact.id == call.contact.id && c.callState == CallState.OfferReceived
+ }
+ val endCall = { if (offerPending()) withBGApi { chatModel.callManager.endCall(call) } }
+ AlertManager.shared.showAlertDialog(
+ title = generalGetString(MR.strings.call_not_encrypted_title),
+ text = generalGetString(MR.strings.call_not_encrypted_desc).format(call.contact.displayName),
+ confirmText = generalGetString(MR.strings.call_service_notification_end_call),
+ onConfirm = { endCall() },
+ dismissText = generalGetString(MR.strings.continue_to_next_step),
+ onDismiss = { if (offerPending()) onContinue() },
+ onDismissRequest = { endCall() },
+ destructive = true,
+ parseHtml = false
+ )
+ }
+
suspend fun leaveGroup(rh: Long?, groupId: Long) {
val groupInfo = apiLeaveGroup(rh, groupId)
if (groupInfo != null) {
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml
index efe0423aff..8a8aad0cfa 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml
@@ -1415,6 +1415,8 @@
no e2e encryption
contact has e2e encryption
contact has no e2e encryption
+ Call is not encrypted
+ %s accepted the call without end-to-end encryption.
peer-to-peer
via relay
Hang up
diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs
index 19b78dc2ae..71337a6b0e 100644
--- a/src/Simplex/Chat/Library/Subscriber.hs
+++ b/src/Simplex/Chat/Library/Subscriber.hs
@@ -3053,7 +3053,7 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
| callVersion `isCompatible` callVR -> do
let sharedKey = callMediaKey callVersion callId <$> callDhPubKey <*> localDhPrivKey
callState' = CallOfferReceived {localCallType, peerCallType = callType, peerCallSession = rtcSession, sharedKey}
- askConfirmation = encryptedCall localCallType && not (encryptedCall callType)
+ askConfirmation = encryptedCall localCallType && isNothing sharedKey
toView CEvtCallOffer {user, contact = ct, callType, offer = rtcSession, sharedKey, askConfirmation}
pure (Just call {callState = callState'}, Just . ACIContent SMDSnd $ CISndCall CISCallAccepted 0)
| otherwise -> do