diff --git a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt index 09160bd55..d1b106b29 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt @@ -17,8 +17,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.lifecycle.AndroidViewModel import androidx.work.* -import chat.simplex.app.model.ChatModel -import chat.simplex.app.model.NtfManager +import chat.simplex.app.model.* import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.views.SplashView import chat.simplex.app.views.WelcomeView @@ -29,7 +28,6 @@ import chat.simplex.app.views.helpers.* import chat.simplex.app.views.newchat.connectViaUri import chat.simplex.app.views.newchat.withUriAction import java.util.concurrent.TimeUnit - //import kotlinx.serialization.decodeFromString class MainActivity: ComponentActivity() { diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index a3fd1ffc4..56e69929b 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.text.style.TextDecoration import chat.simplex.app.R import chat.simplex.app.ui.theme.SecretColor import chat.simplex.app.ui.theme.SimplexBlue +import chat.simplex.app.views.call.* import chat.simplex.app.views.helpers.generalGetString import kotlinx.datetime.* import kotlinx.serialization.* @@ -19,23 +20,29 @@ import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.* class ChatModel(val controller: ChatController) { - var currentUser = mutableStateOf(null) - var userCreated = mutableStateOf(null) - var chats = mutableStateListOf() - var chatId = mutableStateOf(null) - var chatItems = mutableStateListOf() + val currentUser = mutableStateOf(null) + val userCreated = mutableStateOf(null) + val chats = mutableStateListOf() + val chatId = mutableStateOf(null) + val chatItems = mutableStateListOf() var connReqInvitation: String? = null - var terminalItems = mutableStateListOf() - var userAddress = mutableStateOf(null) - var userSMPServers = mutableStateOf<(List)?>(null) + val terminalItems = mutableStateListOf() + val userAddress = mutableStateOf(null) + val userSMPServers = mutableStateOf<(List)?>(null) // set when app opened from external intent - var clearOverlays = mutableStateOf(false) + val clearOverlays = mutableStateOf(false) // set when app is opened via contact or invitation URI - var appOpenUrl = mutableStateOf(null) - var runServiceInBackground = mutableStateOf(true) + val appOpenUrl = mutableStateOf(null) + val runServiceInBackground = mutableStateOf(true) + + // current WebRTC call + val callInvitations = mutableStateMapOf() + val activeCallInvitation = mutableStateOf(null) + val activeCall = mutableStateOf(null) + val callCommand = mutableStateOf(null) fun updateUserProfile(profile: Profile) { val user = currentUser.value @@ -654,6 +661,13 @@ data class ChatItem ( else -> false } + val isCall: Boolean get() = + when (content) { + is CIContent.SndCall -> true + is CIContent.RcvCall -> true + else -> false + } + companion object { fun getSampleData( id: Long = 1, @@ -709,26 +723,16 @@ data class ChatItem ( @Serializable sealed class CIDirection { - abstract val sent: Boolean + @Serializable @SerialName("directSnd") class DirectSnd: CIDirection() + @Serializable @SerialName("directRcv") class DirectRcv: CIDirection() + @Serializable @SerialName("groupSnd") class GroupSnd: CIDirection() + @Serializable @SerialName("groupRcv") class GroupRcv(val groupMember: GroupMember): CIDirection() - @Serializable @SerialName("directSnd") - class DirectSnd: CIDirection() { - override val sent get() = true - } - - @Serializable @SerialName("directRcv") - class DirectRcv: CIDirection() { - override val sent get() = false - } - - @Serializable @SerialName("groupSnd") - class GroupSnd: CIDirection() { - override val sent get() = true - } - - @Serializable @SerialName("groupRcv") - class GroupRcv(val groupMember: GroupMember): CIDirection() { - override val sent get() = false + val sent: Boolean get() = when(this) { + is DirectSnd -> true + is DirectRcv -> false + is GroupSnd -> true + is GroupRcv -> false } } @@ -775,23 +779,12 @@ fun getTimestampText(t: Instant): String { @Serializable sealed class CIStatus { - @Serializable @SerialName("sndNew") - class SndNew: CIStatus() - - @Serializable @SerialName("sndSent") - class SndSent: CIStatus() - - @Serializable @SerialName("sndErrorAuth") - class SndErrorAuth: CIStatus() - - @Serializable @SerialName("sndError") - class SndError(val agentError: AgentErrorType): CIStatus() - - @Serializable @SerialName("rcvNew") - class RcvNew: CIStatus() - - @Serializable @SerialName("rcvRead") - class RcvRead: CIStatus() + @Serializable @SerialName("sndNew") class SndNew: CIStatus() + @Serializable @SerialName("sndSent") class SndSent: CIStatus() + @Serializable @SerialName("sndErrorAuth") class SndErrorAuth: CIStatus() + @Serializable @SerialName("sndError") class SndError(val agentError: AgentErrorType): CIStatus() + @Serializable @SerialName("rcvNew") class RcvNew: CIStatus() + @Serializable @SerialName("rcvRead") class RcvRead: CIStatus() } @Serializable @@ -806,29 +799,22 @@ interface ItemContent { @Serializable sealed class CIContent: ItemContent { - abstract override val text: String abstract val msgContent: MsgContent? - @Serializable @SerialName("sndMsgContent") - class SndMsgContent(override val msgContent: MsgContent): CIContent() { - override val text get() = msgContent.text - } + @Serializable @SerialName("sndMsgContent") class SndMsgContent(override val msgContent: MsgContent): CIContent() + @Serializable @SerialName("rcvMsgContent") class RcvMsgContent(override val msgContent: MsgContent): CIContent() + @Serializable @SerialName("sndDeleted") class SndDeleted(val deleteMode: CIDeleteMode): CIContent() { override val msgContent get() = null } + @Serializable @SerialName("rcvDeleted") class RcvDeleted(val deleteMode: CIDeleteMode): CIContent() { override val msgContent get() = null } + @Serializable @SerialName("sndCall") class SndCall(val status: CICallStatus, val duration: Int): CIContent() { override val msgContent get() = null } + @Serializable @SerialName("rcvCall") class RcvCall(val status: CICallStatus, val duration: Int): CIContent() { override val msgContent get() = null } - @Serializable @SerialName("rcvMsgContent") - class RcvMsgContent(override val msgContent: MsgContent): CIContent() { - override val text get() = msgContent.text - } - - @Serializable @SerialName("sndDeleted") - class SndDeleted(val deleteMode: CIDeleteMode): CIContent() { - override val text get() = generalGetString(R.string.deleted_description) - override val msgContent get() = null - } - - @Serializable @SerialName("rcvDeleted") - class RcvDeleted(val deleteMode: CIDeleteMode): CIContent() { - override val text get() = generalGetString(R.string.deleted_description) - override val msgContent get() = null + override val text: String get() = when(this) { + is SndMsgContent -> msgContent.text + is RcvMsgContent -> msgContent.text + is SndDeleted -> generalGetString(R.string.deleted_description) + is RcvDeleted -> generalGetString(R.string.deleted_description) + is SndCall -> status.text(duration) + is RcvCall -> status.text(duration) } } @@ -904,20 +890,11 @@ enum class CIFileStatus { sealed class MsgContent { abstract val text: String - @Serializable(with = MsgContentSerializer::class) - class MCText(override val text: String): MsgContent() - - @Serializable(with = MsgContentSerializer::class) - class MCLink(override val text: String, val preview: LinkPreview): MsgContent() - - @Serializable(with = MsgContentSerializer::class) - class MCImage(override val text: String, val image: String): MsgContent() - - @Serializable(with = MsgContentSerializer::class) - class MCFile(override val text: String): MsgContent() - - @Serializable(with = MsgContentSerializer::class) - class MCUnknown(val type: String? = null, override val text: String, val json: JsonElement): MsgContent() + @Serializable(with = MsgContentSerializer::class) class MCText(override val text: String): MsgContent() + @Serializable(with = MsgContentSerializer::class) class MCLink(override val text: String, val preview: LinkPreview): MsgContent() + @Serializable(with = MsgContentSerializer::class) class MCImage(override val text: String, val image: String): MsgContent() + @Serializable(with = MsgContentSerializer::class) class MCFile(override val text: String): MsgContent() + @Serializable(with = MsgContentSerializer::class) class MCUnknown(val type: String? = null, override val text: String, val json: JsonElement): MsgContent() val cmdString: String get() = when (this) { is MCText -> "text $text" @@ -1073,3 +1050,28 @@ class SndFileTransfer() {} @Serializable class FileTransferMeta() {} + +@Serializable +enum class CICallStatus { + @SerialName("pending") Pending, + @SerialName("missed") Missed, + @SerialName("rejected") Rejected, + @SerialName("accepted") Accepted, + @SerialName("negotiated") Negotiated, + @SerialName("progress") Progress, + @SerialName("ended") Ended, + @SerialName("error") Error; + + fun text(sec: Int): String = when (this) { + Pending -> generalGetString(R.string.callstatus_calling) + Missed -> generalGetString(R.string.callstatus_missed) + Rejected -> generalGetString(R.string.callstatus_rejected) + Accepted -> generalGetString(R.string.callstatus_accepted) + Negotiated -> generalGetString(R.string.callstatus_connecting) + Progress -> generalGetString(R.string.callstatus_in_progress) + Ended -> String.format(generalGetString(R.string.callstatus_ended), duration(sec)) + Error -> generalGetString(R.string.callstatus_error) + } + + fun duration(sec: Int): String = "%02d:%02d".format(sec / 60, sec % 60) +} 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 d82011b7c..c34d68da1 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 @@ -47,7 +47,7 @@ open class ChatController(private val ctrl: ChatCtrl, private val ntfManager: Nt val chats = apiGetChats() chatModel.chats.clear() chatModel.chats.addAll(chats) - chatModel.currentUser = mutableStateOf(user) + chatModel.currentUser.value = user chatModel.userCreated.value = true Log.d(TAG, "started chat") } catch(e: Error) { @@ -713,7 +713,7 @@ class APIResponse(val resp: CR, val corr: String? = null) { json.decodeFromString(str) } catch(e: Exception) { try { - Log.d(TAG, e.localizedMessage) + Log.d(TAG, e.localizedMessage ?: "") val data = json.parseToJsonElement(str).jsonObject APIResponse( resp = CR.Response(data["resp"]!!.jsonObject["type"]?.toString() ?: "invalid", json.encodeToString(data)), 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 8895c44e4..b85c6b59b 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 @@ -27,7 +27,7 @@ import chat.simplex.app.TAG import chat.simplex.app.views.helpers.TextEditor import com.google.accompanist.permissions.rememberMultiplePermissionsState -//@SuppressLint("JavascriptInterface") +@SuppressLint("SetJavaScriptEnabled") @Composable fun VideoCallView(close: () -> Unit) { BackHandler(onBack = close) @@ -57,8 +57,7 @@ fun VideoCallView(close: () -> Unit) { } } val localContext = LocalContext.current - val iceCandidateCommand = remember { mutableStateOf("") } - val commandToShow = remember { mutableStateOf("processCommand({type: 'start', media: 'video', aesKey: 'FwW+t6UbnwHoapYOfN4mUBUuqR7UtvYWxW16iBqM29U='})") } + val commandToShow = remember { mutableStateOf("processCommand({command: {type: 'start', media: 'video'}})") } //, aesKey: 'FwW+t6UbnwHoapYOfN4mUBUuqR7UtvYWxW16iBqM29U='})") } val assetLoader = WebViewAssetLoader.Builder() .addPathHandler("/assets/www/", WebViewAssetLoader.AssetsPathHandler(localContext)) .build() @@ -96,9 +95,7 @@ fun VideoCallView(close: () -> Unit) { override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean { val rtnValue = super.onConsoleMessage(consoleMessage) val msg = consoleMessage?.message() as String - if (msg.startsWith("{\"action\":\"processIceCandidates\"")) { - iceCandidateCommand.value = "processCommand($msg)" - } else if (msg.startsWith("{")) { + if (msg.startsWith("{")) { commandToShow.value = "processCommand($msg)" } return rtnValue @@ -142,15 +139,9 @@ fun VideoCallView(close: () -> Unit) { wv.evaluateJavascript(commandToShow.value, null) commandToShow.value = "" }) {Text("Send")} - Button( onClick = { - commandToShow.value = iceCandidateCommand.value - }) {Text("ICE")} Button( onClick = { commandToShow.value = "" }) {Text("Clear")} - Button( onClick = { - wv.evaluateJavascript("endCall()", null) - }) {Text("End Call")} } } } 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 31af62839..cdf9ebf45 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 @@ -1,10 +1,47 @@ package chat.simplex.app.views.call -import chat.simplex.app.model.CR -import chat.simplex.app.model.User +import chat.simplex.app.R +import chat.simplex.app.model.* +import chat.simplex.app.views.helpers.generalGetString import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +class Call( + val contact: Contact, + val callState: CallState, + val localMedia: CallMediaType, + val localCapabilities: CallCapabilities? = null, + val peerMedia: CallMediaType? = null, + val sharedKey: String? = null, + val audioEnabled: Boolean = true, + val videoEnabled: Boolean = localMedia == CallMediaType.Video +) { + val encrypted: Boolean get() = (localCapabilities?.encryption ?: false) && sharedKey != null +} + +enum class CallState { + WaitCapabilities, + InvitationSent, + InvitationReceived, + OfferSent, + OfferReceived, + Negotiated, + Connected; + + val text: String get() = when(this) { + WaitCapabilities -> generalGetString(R.string.callstate_starting) + InvitationSent -> generalGetString(R.string.callstate_waiting_for_answer) + InvitationReceived -> generalGetString(R.string.callstate_starting) + OfferSent -> generalGetString(R.string.callstate_waiting_for_confirmation) + OfferReceived -> generalGetString(R.string.callstate_received_answer) + Negotiated -> generalGetString(R.string.callstate_connecting) + Connected -> generalGetString(R.string.callstate_connected) + } +} + +@Serializable class WVAPICall(val corrId: Int? = null, val command: WCallCommand) +@Serializable class WVAPIMessage(val corrId: Int? = null, val resp: WCallResponse, val command: WCallCommand?) + @Serializable sealed class WCallCommand { @Serializable @SerialName("capabilities") class Capabilities(): WCallCommand() @@ -12,6 +49,7 @@ sealed class WCallCommand { @Serializable @SerialName("accept") class Accept(val offer: String, val iceCandidates: List, val media: CallMediaType, val aesKey: String? = null): WCallCommand() @Serializable @SerialName("answer") class Answer (val answer: String, val iceCandidates: List): WCallCommand() @Serializable @SerialName("ice") class Ice(val iceCandidates: List): WCallCommand() + @Serializable @SerialName("media") class Media(val media: CallMediaType, val enable: Boolean): WCallCommand() @Serializable @SerialName("end") class End(): WCallCommand() } @@ -30,17 +68,12 @@ sealed class WCallResponse { @Serializable class Invalid(val str: String): WCallResponse() } -@Serializable -class WebRTCCallOffer(val callType: CallType, val rtcSession: WebRTCSession) - -@Serializable -class WebRTCSession(val rtcSession: String, val rtcIceCandidates: List) - -@Serializable -class WebRTCExtraInfo(val rtcIceCandidates: List) - -@Serializable -class CallType(val media: CallMediaType, val capabilities: CallCapabilities) +@Serializable class WebRTCCallOffer(val callType: CallType, val rtcSession: WebRTCSession) +@Serializable class WebRTCSession(val rtcSession: String, val rtcIceCandidates: List) +@Serializable class WebRTCExtraInfo(val rtcIceCandidates: List) +@Serializable class CallType(val media: CallMediaType, val capabilities: CallCapabilities) +@Serializable class CallInvitation(val peerMedia: CallMediaType?, val sharedKey: String?) +@Serializable class CallCapabilities(val encryption: Boolean) enum class WebRTCCallStatus(val status: String) { Connected("connected"), @@ -54,9 +87,6 @@ enum class CallMediaType(val media: String) { Audio("audio") } -@Serializable -class CallCapabilities(val encryption: Boolean) - @Serializable class ConnectionState( val connectionState: String, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt index 3e484d74e..1d43b0afd 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt @@ -62,6 +62,8 @@ fun ChatItemView( } } else if (cItem.isDeletedContent) { DeletedItemView(cItem, showMember = showMember) + } else if (cItem.isCall) { + FramedItemView(user, cItem, uriHandler, showMember = showMember, showMenu, receiveFile) } if (cItem.isMsgContent) { DropdownMenu( diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt index 02bc964b6..4857025af 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt @@ -39,7 +39,8 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { suspend fun openChat(chatModel: ChatModel, cInfo: ChatInfo) { val chat = chatModel.controller.apiGetChat(cInfo.chatType, cInfo.apiId) if (chat != null) { - chatModel.chatItems = chat.chatItems.toMutableStateList() + chatModel.chatItems.clear() + chatModel.chatItems.addAll(chat.chatItems) chatModel.chatId.value = cInfo.id } } 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 6cc298589..ac1acc980 100644 --- a/apps/android/app/src/main/res/values-ru/strings.xml +++ b/apps/android/app/src/main/res/values-ru/strings.xml @@ -275,4 +275,21 @@ Вы также можете соединиться, открыв ссылку там, где вы её получили. Если ссылка откроется в браузере, нажмите кнопку Open in mobile app. Добавьте контакт, чтобы начать разговор: + + входящий звонок… + пропущенный звонок + отклоненный звонок + принятый звонок + соединяется… + активный звонок + звонок завершён %1$s! + ошибка соединения + + + инициализация… + ожидается ответ… + ожидается подтверждение… + получен ответ… + соединяется… + соединено diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index 50d46ea54..774d6e8c2 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -276,4 +276,21 @@ colored secret + + calling… + missed + rejected + accepted + connecting… + in progress + ended %1$s! + error + + + starting… + waiting for answer… + waiting for confirmation… + received answer… + connecting… + connected diff --git a/apps/ios/Shared/Model/Shared/ChatTypes.swift b/apps/ios/Shared/Model/Shared/ChatTypes.swift index 9e3827ed8..3ab6dc2c8 100644 --- a/apps/ios/Shared/Model/Shared/ChatTypes.swift +++ b/apps/ios/Shared/Model/Shared/ChatTypes.swift @@ -725,7 +725,6 @@ enum MsgContent { } } -// TODO define Encodable extension MsgContent: Decodable { init(from decoder: Decoder) throws { do { @@ -863,15 +862,14 @@ enum CICallStatus: String, Decodable { func text(_ sec: Int) -> String { switch self { - case .pending: return "calling..." - case .negotiated: return "connecting..." - case .progress: return "in progress" - case .ended: return "ended \(duration(sec))" - default: return self.rawValue + case .pending: return NSLocalizedString("calling…", comment: "call status") + case .missed: return NSLocalizedString("missed…", comment: "call status") + case .rejected: return NSLocalizedString("rejected", comment: "call status") + case .accepted: return NSLocalizedString("accepted", comment: "call status") + case .negotiated: return NSLocalizedString("connecting…", comment: "call status") + case .progress: return NSLocalizedString("in progress", comment: "call status") + case .ended: return String.localizedStringWithFormat(NSLocalizedString("ended %02d:%02d", comment: "call status"), sec / 60, sec % 60) + case .error: return NSLocalizedString("error", comment: "call status") } } - - func duration(_ sec: Int) -> String { - String(format: "%02d:%02d", sec / 60, sec % 60) - } } diff --git a/apps/ios/Shared/Views/Call/WebRTC.swift b/apps/ios/Shared/Views/Call/WebRTC.swift index d05833c85..ec3842bb0 100644 --- a/apps/ios/Shared/Views/Call/WebRTC.swift +++ b/apps/ios/Shared/Views/Call/WebRTC.swift @@ -81,12 +81,12 @@ enum CallState { var text: LocalizedStringKey { switch self { - case .waitCapabilities: return "starting..." - case .invitationSent: return "waiting for answer..." - case .invitationReceived: return "starting..." - case .offerSent: return "waiting for confirmation..." - case .offerReceived: return "received answer..." - case .negotiated: return "connecting..." + case .waitCapabilities: return "starting…" + case .invitationSent: return "waiting for answer…" + case .invitationReceived: return "starting…" + case .offerSent: return "waiting for confirmation…" + case .offerReceived: return "received answer…" + case .negotiated: return "connecting…" case .connected: return "connected" } } diff --git a/apps/ios/Shared/Views/Call/WebRTCView.swift b/apps/ios/Shared/Views/Call/WebRTCView.swift index b97d0f280..1dad35e67 100644 --- a/apps/ios/Shared/Views/Call/WebRTCView.swift +++ b/apps/ios/Shared/Views/Call/WebRTCView.swift @@ -91,14 +91,15 @@ struct WebRTCView: UIViewRepresentable { } //struct CallViewDebug: View { -// @State var coordinator: WebRTCCoordinator? = nil -// @State var commandStr = "" -// @State private var webViewMsg: WCallResponse? = nil +// @State private var coordinator: WebRTCCoordinator? = nil +// @State private var commandStr = "" +// @State private var webViewReady: Bool = false +// @State private var webViewMsg: WVAPIMessage? = nil // @FocusState private var keyboardVisible: Bool // // var body: some View { // VStack(spacing: 30) { -// WebRTCView(coordinator: $coordinator, webViewMsg: $webViewMsg).frame(maxHeight: 260) +// WebRTCView(coordinator: $coordinator, webViewReady: $webViewReady, webViewMsg: $webViewMsg).frame(maxHeight: 260) // .onChange(of: webViewMsg) { _ in // if let resp = webViewMsg { // commandStr = encodeJSON(resp) @@ -126,13 +127,9 @@ struct WebRTCView: UIViewRepresentable { // commandStr = "" // } // Button("Send") { -// do { -// if let c = coordinator, -// let command: WCallCommand = decodeJSON(commandStr) { -// c.sendCommand(command: command) -// } -// } catch { -// print(error) +// if let c = coordinator, +// let command: WCallCommand = decodeJSON(commandStr) { +// c.sendCommand(command: command) // } // } // }