diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt
index 12719b7e58..088341544c 100644
--- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt
+++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt
@@ -513,7 +513,7 @@ open class ChatController(private val ctrl: ChatCtrl, private val ntfManager: Nt
onConfirm = {
if (chatModel.activeCallInvitation.value == null) {
AlertManager.shared.hideAlert()
- AlertManager.shared.showAlertMsg(generalGetString(R.string.call_already_ended))
+ withApi { AlertManager.shared.showAlertMsg(generalGetString(R.string.call_already_ended)) }
} else {
chatModel.activeCallInvitation.value = null
chatModel.activeCall.value = Call(
@@ -546,18 +546,20 @@ open class ChatController(private val ctrl: ChatCtrl, private val ntfManager: Nt
}
}
is CR.CallExtraInfo -> {
- withCall(r, r.contact) { call ->
+ withCall(r, r.contact) { _ ->
chatModel.callCommand.value = WCallCommand.Ice(iceCandidates = r.extraInfo.rtcIceCandidates)
}
}
is CR.CallEnded -> {
- withCall(r, r.contact) { call ->
+ withCall(r, r.contact) { _ ->
chatModel.callCommand.value = WCallCommand.End
- chatModel.activeCall.value = null
- chatModel.activeCallInvitation.value = null
- chatModel.callCommand.value = null
- chatModel.showCallView.value = false
+ withApi {
+ chatModel.activeCall.value = null
+ chatModel.callCommand.value = null
+ }
}
+ chatModel.activeCallInvitation.value = null
+ chatModel.showCallView.value = false
}
else ->
Log.d(TAG , "unsupported event: ${r.responseType}")
@@ -836,7 +838,7 @@ sealed class CC {
is ApiSendCallAnswer -> "/_call answer @${contact.apiId} ${json.encodeToString(answer)}"
is ApiSendCallExtraInfo -> "/_call extra @${contact.apiId} ${json.encodeToString(extraInfo)}"
is ApiEndCall -> "/_call end @${contact.apiId}"
- is ApiCallStatus -> "/_call status @${contact.apiId} ${callStatus}"
+ is ApiCallStatus -> "/_call status @${contact.apiId} ${callStatus.value}"
is ApiChatRead -> "/_read chat ${chatRef(type, id)} from=${range.from} to=${range.to}"
is ReceiveFile -> "/freceive $fileId"
}
diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/call/CallView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/call/CallView.kt
index 473d94e2ab..57a816eb3c 100644
--- a/apps/android/app/src/main/java/chat/simplex/app/views/call/CallView.kt
+++ b/apps/android/app/src/main/java/chat/simplex/app/views/call/CallView.kt
@@ -3,6 +3,7 @@ package chat.simplex.app.views.call
import android.Manifest
import android.content.ClipData
import android.content.ClipboardManager
+import android.graphics.fonts.FontStyle
import android.os.Build
import android.service.controls.templates.ControlButton
import android.util.Log
@@ -26,6 +27,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.capitalize
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@@ -189,7 +191,7 @@ private fun ActiveCallOverlayLayout(
private fun ControlButton(call: Call, icon: ImageVector, @StringRes iconText: Int, action: () -> Unit) {
if (call.hasMedia) {
IconButton(onClick = action) {
- Icon(icon, stringResource(iconText), tint = Color.White, modifier = Modifier.size(40.dp))
+ Icon(icon, stringResource(iconText), tint = Color(0xFFFFFFD8), modifier = Modifier.size(40.dp))
}
} else {
Spacer(Modifier.size(40.dp))
@@ -207,10 +209,16 @@ private fun ToggleAudioButton(call: Call, toggleAudio: () -> Unit) {
@Composable
private fun CallInfoView(call: Call, alignment: Alignment.Horizontal) {
+ @Composable fun InfoText(text: String, style: TextStyle = MaterialTheme.typography.body2) =
+ Text(text, color = Color(0xFFFFFFD8), style = style)
Column(horizontalAlignment = alignment) {
- Text(call.contact.chatViewName, color = Color.White, style = MaterialTheme.typography.body2)
- Text(call.callState.text, color = Color.White, style = MaterialTheme.typography.body2)
- Text(call.encryptionStatus, color = Color.White, style = MaterialTheme.typography.body2)
+ InfoText(call.contact.chatViewName, style = MaterialTheme.typography.h2)
+ InfoText(call.callState.text)
+
+ val connInfo =
+ if (call.connectionInfo == null) ""
+ else " (${call.connectionInfo.text})"
+ InfoText(call.encryptionStatus + connInfo)
}
}
@@ -390,7 +398,8 @@ fun PreviewActiveCallOverlayVideo() {
contact = Contact.sampleData,
callState = CallState.Negotiated,
localMedia = CallMediaType.Video,
- peerMedia = CallMediaType.Video
+ peerMedia = CallMediaType.Video,
+ connectionInfo = ConnectionInfo(RTCIceCandidate(RTCIceCandidateType.Host), RTCIceCandidate(RTCIceCandidateType.Host))
),
dismiss = {},
toggleAudio = {},
@@ -409,7 +418,8 @@ fun PreviewActiveCallOverlayAudio() {
contact = Contact.sampleData,
callState = CallState.Negotiated,
localMedia = CallMediaType.Audio,
- peerMedia = CallMediaType.Audio
+ peerMedia = CallMediaType.Audio,
+ connectionInfo = ConnectionInfo(RTCIceCandidate(RTCIceCandidateType.Host), RTCIceCandidate(RTCIceCandidateType.Host))
),
dismiss = {},
toggleAudio = {},
diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/call/WebRTC.kt b/apps/android/app/src/main/java/chat/simplex/app/views/call/WebRTC.kt
index 81c7d65857..a415710319 100644
--- a/apps/android/app/src/main/java/chat/simplex/app/views/call/WebRTC.kt
+++ b/apps/android/app/src/main/java/chat/simplex/app/views/call/WebRTC.kt
@@ -96,7 +96,16 @@ sealed class WCallResponse {
})
}
@Serializable class CallCapabilities(val encryption: Boolean)
-@Serializable class ConnectionInfo(val localCandidate: RTCIceCandidate?, val remoteCandidate: RTCIceCandidate)
+@Serializable class ConnectionInfo(val localCandidate: RTCIceCandidate?, val remoteCandidate: RTCIceCandidate) {
+ val text: String @Composable get() = when {
+ localCandidate?.candidateType == RTCIceCandidateType.Host && remoteCandidate?.candidateType == RTCIceCandidateType.Host ->
+ stringResource(R.string.call_connection_peer_to_peer)
+ localCandidate?.candidateType == RTCIceCandidateType.Relay && remoteCandidate?.candidateType == RTCIceCandidateType.Relay ->
+ stringResource(R.string.call_connection_via_relay)
+ else ->
+ "${localCandidate?.candidateType?.value ?: "unknown"} / ${remoteCandidate?.candidateType?.value ?: "unknown"}"
+ }
+}
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate
@Serializable class RTCIceCandidate(val candidateType: RTCIceCandidateType?)
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer
@@ -104,19 +113,19 @@ sealed class WCallResponse {
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate/type
@Serializable
-enum class RTCIceCandidateType {
- @SerialName("host") Host,
- @SerialName("srflx") ServerReflexive,
- @SerialName("prflx") PeerReflexive,
- @SerialName("relay") Relay
+enum class RTCIceCandidateType(val value: String) {
+ @SerialName("host") Host("host"),
+ @SerialName("srflx") ServerReflexive("srflx"),
+ @SerialName("prflx") PeerReflexive("prflx"),
+ @SerialName("relay") Relay("relay")
}
@Serializable
-enum class WebRTCCallStatus {
- @SerialName("connected") Connected,
- @SerialName("connecting") Connecting,
- @SerialName("disconnected") Disconnected,
- @SerialName("failed") Failed
+enum class WebRTCCallStatus(val value: String) {
+ @SerialName("connected") Connected("connected"),
+ @SerialName("connecting") Connecting("connecting"),
+ @SerialName("disconnected") Disconnected("disconnected"),
+ @SerialName("failed") Failed("failed")
}
@Serializable
diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt
index dbaec141e9..583029e350 100644
--- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt
+++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt
@@ -3,6 +3,7 @@ package chat.simplex.app.views.chat
import android.content.res.Configuration
import android.util.Log
import androidx.activity.compose.BackHandler
+import androidx.annotation.StringRes
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.*
@@ -18,12 +19,14 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.TAG
@@ -174,42 +177,42 @@ fun ChatLayout(
@Composable
fun ChatInfoToolbar(chat: Chat, back: () -> Unit, info: () -> Unit, startCall: (CallMediaType) -> Unit) {
+ @Composable fun toolbarButton(icon: ImageVector, @StringRes textId: Int, modifier: Modifier = Modifier.padding(0.dp), onClick: () -> Unit) {
+ IconButton(onClick, modifier = modifier) {
+ Icon(icon, stringResource(textId), tint = MaterialTheme.colors.primary)
+ }
+ }
Column {
Box(
Modifier
.fillMaxWidth()
.height(52.dp)
.background(if (isSystemInDarkTheme()) ToolbarDark else ToolbarLight)
- .padding(horizontal = 8.dp),
+ .padding(horizontal = 4.dp),
contentAlignment = Alignment.CenterStart,
) {
- IconButton(onClick = back) {
- Icon(
- Icons.Outlined.ArrowBackIos,
- stringResource(R.string.back),
- tint = MaterialTheme.colors.primary,
- modifier = Modifier.padding(10.dp)
- )
- }
- Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) {
- IconButton(onClick = { startCall(CallMediaType.Video) }) {
- Icon(
- Icons.Outlined.Videocam,
- "video call",
- tint = MaterialTheme.colors.primary,
- modifier = Modifier.padding(10.dp)
- )
+ val cInfo = chat.chatInfo
+ toolbarButton(Icons.Outlined.ArrowBackIos, R.string.back, onClick = back)
+ if (cInfo is ChatInfo.Direct) {
+ Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) {
+ Box(Modifier.width(85.dp), contentAlignment = Alignment.CenterStart) {
+ toolbarButton(Icons.Outlined.Phone, R.string.icon_descr_audio_call) {
+ startCall(CallMediaType.Audio)
+ }
+ }
+ toolbarButton(Icons.Outlined.Videocam, R.string.icon_descr_video_call) {
+ startCall(CallMediaType.Video)
+ }
}
}
Row(
Modifier
- .padding(horizontal = 68.dp)
+ .padding(horizontal = 80.dp)
.fillMaxWidth()
.clickable(onClick = info),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
- val cInfo = chat.chatInfo
ChatInfoImage(cInfo, size = 40.dp)
Column(
Modifier.padding(start = 8.dp),
diff --git a/apps/android/app/src/main/res/values-ru/strings.xml b/apps/android/app/src/main/res/values-ru/strings.xml
index 9c500b363b..5c17693412 100644
--- a/apps/android/app/src/main/res/values-ru/strings.xml
+++ b/apps/android/app/src/main/res/values-ru/strings.xml
@@ -295,7 +295,7 @@
принятый звонок
соединяется…
активный звонок
- звонок завершён %1$s!
+ звонок завершён %1$s
ошибка соединения
@@ -347,15 +347,19 @@
e2e encrypted video call
audio call (not e2e encrypted)
e2e encrypted audio call
- If you accept this call, your IP address might be visible to your contact, unless you connect via relay.
+ If you accept this call and you don\'t use relay, your IP address might be visible to your contact.
Answer
Call already ended!
+ video call
+ audio call
e2e encrypted
no e2e encryption
contact has e2e encryption
contact has no e2e encryption
+ peer-to-peer
+ via relay
Hang up
Video off
Video on
diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml
index e85ccaf6b7..6ed5cad3f5 100644
--- a/apps/android/app/src/main/res/values/strings.xml
+++ b/apps/android/app/src/main/res/values/strings.xml
@@ -296,7 +296,7 @@
accepted
connecting…
in progress
- ended %1$s!
+ ended %1$s
error
@@ -348,15 +348,19 @@
e2e encrypted video call
audio call (not e2e encrypted)
e2e encrypted audio call
- If you accept this call, your IP address might be visible to your contact, unless you connect via relay.
+ If you accept this call and you don\'t use relay, your IP address might be visible to your contact.
Answer
Call already ended!
+ video call
+ audio call
e2e encrypted
no e2e encryption
contact has e2e encryption
contact has no e2e encryption
+ peer-to-peer
+ via relay
Hang up
Video off
Video on
diff --git a/apps/ios/Shared/Views/Call/ActiveCallView.swift b/apps/ios/Shared/Views/Call/ActiveCallView.swift
index cda1a52939..47b1bb45ed 100644
--- a/apps/ios/Shared/Views/Call/ActiveCallView.swift
+++ b/apps/ios/Shared/Views/Call/ActiveCallView.swift
@@ -174,7 +174,12 @@ struct ActiveCallOverlay: View {
.frame(maxWidth: .infinity, alignment: alignment)
Group {
Text(call.callState.text)
- Text(call.encryptionStatus)
+ HStack {
+ Text(call.encryptionStatus)
+ if let connInfo = call.connectionInfo?.text {
+ Text("(") + Text(connInfo) + Text(")")
+ }
+ }
}
.font(.subheadline)
.frame(maxWidth: .infinity, alignment: alignment)
diff --git a/apps/ios/Shared/Views/Call/WebRTC.swift b/apps/ios/Shared/Views/Call/WebRTC.swift
index 0382c73284..4d87f263f8 100644
--- a/apps/ios/Shared/Views/Call/WebRTC.swift
+++ b/apps/ios/Shared/Views/Call/WebRTC.swift
@@ -379,6 +379,19 @@ struct ConnectionState: Codable, Equatable {
struct ConnectionInfo: Codable, Equatable {
var localCandidate: RTCIceCandidate?
var remoteCandidate: RTCIceCandidate?
+
+ var text: LocalizedStringKey {
+ get {
+ if localCandidate?.candidateType == .host && remoteCandidate?.candidateType == .host {
+ return "peer-to-peer"
+ } else if localCandidate?.candidateType == .relay && remoteCandidate?.candidateType == .relay {
+ return "via relay"
+ } else {
+ let unknown = NSLocalizedString("unknown", comment: "connection info")
+ return "\(localCandidate?.candidateType?.rawValue ?? unknown)) / \(remoteCandidate?.candidateType?.rawValue ?? unknown)"
+ }
+ }
+ }
}
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate
diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift
index 1334a62373..248f331769 100644
--- a/apps/ios/Shared/Views/ChatList/ChatListView.swift
+++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift
@@ -103,7 +103,7 @@ struct ChatListView: View {
message: Text(contact.profile.displayName).bold() +
Text(" wants to connect with you via ") +
Text(invitation.callTypeText) +
- Text("\nIf you accept this call, your IP address might be visible to your contact, unless you connect via relay."),
+ Text("\nIf you accept this call and you don't use relay, your IP address might be visible to your contact."),
primaryButton: .default(Text("Answer")) {
if chatModel.activeCallInvitation == nil {
DispatchQueue.main.async {
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index 8472caca41..a2e75dc4b0 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -848,6 +848,7 @@ callStatusItemContent userId Contact {contactId} chatItemId receivedStatus = do
(Just CISCallProgress, WCSConnected) -> Nothing -- if call in-progress received connected -> no change
(Just CISCallProgress, WCSDisconnected) -> Just (CISCallEnded, callDuration) -- calculate in-progress duration
(Just CISCallProgress, WCSFailed) -> Just (CISCallEnded, callDuration) -- whether call disconnected or failed
+ (Just CISCallPending, WCSDisconnected) -> Just (CISCallMissed, 0)
(Just CISCallEnded, _) -> Nothing -- if call already ended or failed -> no change
(Just CISCallError, _) -> Nothing
(Just _, WCSConnected) -> Just (CISCallProgress, 0) -- if call ended that was never connected, duration = 0