From d971e7c31f33082813081b117c234fe7331f64d0 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 21 May 2022 12:13:37 +0100 Subject: [PATCH] mobile: call chat items (#680) * mobile: call chat items * android: call chat items --- .../chat/simplex/app/views/chat/ChatView.kt | 36 +++- .../app/views/chat/item/CICallViewItem.kt | 157 ++++++++++++++++++ .../app/views/chat/item/ChatItemView.kt | 38 +++-- .../app/src/main/res/values-ru/strings.xml | 9 + .../app/src/main/res/values/strings.xml | 9 + apps/ios/Shared/Model/ChatModel.swift | 3 +- apps/ios/Shared/Model/NtfManager.swift | 6 +- apps/ios/Shared/Model/Shared/ChatTypes.swift | 6 +- .../Shared/Views/Call/ActiveCallView.swift | 5 +- apps/ios/Shared/Views/Call/WebRTC.swift | 2 +- .../Views/Chat/ChatItem/CICallItemView.swift | 90 ++++++++++ apps/ios/Shared/Views/Chat/ChatItemView.swift | 44 +++-- apps/ios/Shared/Views/Chat/ChatView.swift | 8 +- .../Views/ChatList/ChatListNavLink.swift | 10 +- .../Shared/Views/ChatList/ChatListView.swift | 26 +-- .../ios/Shared/Views/Helpers/CIFileView.swift | 20 +-- apps/ios/SimpleX.xcodeproj/project.pbxproj | 4 + 17 files changed, 396 insertions(+), 77 deletions(-) create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CICallViewItem.kt create mode 100644 apps/ios/Shared/Views/Chat/ChatItem/CICallItemView.swift 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 5570d792e7..fd0d7dca7b 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 @@ -121,6 +121,22 @@ fun ChatView(chatModel: ChatModel) { chatModel.showCallView.value = true chatModel.callCommand.value = WCallCommand.Capabilities } + }, + acceptCall = { contact -> + val invitation = chatModel.callInvitations.remove(contact.id) + if (invitation == null) { + AlertManager.shared.showAlertMsg("Call already ended!") + } else { + chatModel.activeCallInvitation.value = null + chatModel.activeCall.value = Call( + contact = contact, + callState = CallState.InvitationReceived, + localMedia = invitation.peerMedia, + sharedKey = invitation.sharedKey + ) + chatModel.showCallView.value = true + chatModel.callCommand.value = WCallCommand.Start(media = invitation.peerMedia, aesKey = invitation.sharedKey) + } } ) } @@ -141,7 +157,8 @@ fun ChatLayout( openDirectChat: (Long) -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit, receiveFile: (Long) -> Unit, - startCall: (CallMediaType) -> Unit + startCall: (CallMediaType) -> Unit, + acceptCall: (Contact) -> Unit ) { Surface( Modifier @@ -167,7 +184,7 @@ fun ChatLayout( modifier = Modifier.navigationBarsWithImePadding() ) { contentPadding -> Box(Modifier.padding(contentPadding)) { - ChatItemsList(user, chat, composeState, chatItems, openDirectChat, deleteMessage, receiveFile) + ChatItemsList(user, chat, composeState, chatItems, openDirectChat, deleteMessage, receiveFile, acceptCall) } } } @@ -255,7 +272,8 @@ fun ChatItemsList( chatItems: List, openDirectChat: (Long) -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit, - receiveFile: (Long) -> Unit + receiveFile: (Long) -> Unit, + acceptCall: (Contact) -> Unit ) { val listState = rememberLazyListState(initialFirstVisibleItemIndex = chatItems.size - chatItems.count { it.isRcvNew }) val keyboardState by getKeyboardState() @@ -293,11 +311,11 @@ fun ChatItemsList( } else { Spacer(Modifier.size(42.dp)) } - ChatItemView(user, cItem, composeState, cxt, uriHandler, showMember = showMember, deleteMessage = deleteMessage, receiveFile = receiveFile) + ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, showMember = showMember, deleteMessage = deleteMessage, receiveFile = receiveFile, acceptCall = acceptCall) } } else { Box(Modifier.padding(start = 86.dp, end = 12.dp)) { - ChatItemView(user, cItem, composeState, cxt, uriHandler, deleteMessage = deleteMessage, receiveFile = receiveFile) + ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, deleteMessage = deleteMessage, receiveFile = receiveFile, acceptCall = acceptCall) } } } else { // direct message @@ -308,7 +326,7 @@ fun ChatItemsList( end = if (sent) 12.dp else 76.dp, ) ) { - ChatItemView(user, cItem, composeState, cxt, uriHandler, deleteMessage = deleteMessage, receiveFile = receiveFile) + ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, deleteMessage = deleteMessage, receiveFile = receiveFile, acceptCall = acceptCall) } } } @@ -377,7 +395,8 @@ fun PreviewChatLayout() { openDirectChat = {}, deleteMessage = { _, _ -> }, receiveFile = {}, - startCall = {} + startCall = {}, + acceptCall = { _ -> } ) } } @@ -422,7 +441,8 @@ fun PreviewGroupChatLayout() { openDirectChat = {}, deleteMessage = { _, _ -> }, receiveFile = {}, - startCall = {} + startCall = {}, + acceptCall = { _ -> } ) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CICallViewItem.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CICallViewItem.kt new file mode 100644 index 0000000000..a2c0e166c0 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CICallViewItem.kt @@ -0,0 +1,157 @@ +package chat.simplex.app.views.chat.item + +import androidx.compose.foundation.layout.* +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.PhoneInTalk +import androidx.compose.material.icons.outlined.* +import androidx.compose.runtime.Composable +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.unit.dp +import androidx.compose.ui.unit.sp +import chat.simplex.app.R +import chat.simplex.app.model.* +import chat.simplex.app.ui.theme.HighOrLowlight +import chat.simplex.app.ui.theme.SimpleButton + +@Composable +fun CICallItemView(cInfo: ChatInfo, cItem: ChatItem, status: CICallStatus, duration: Int, acceptCall: (Contact) -> Unit) { + val sent = cItem.chatDir.sent + Column( + Modifier + .padding(horizontal = 4.dp) + .padding(bottom = 8.dp), horizontalAlignment = Alignment.CenterHorizontally) { + @Composable fun ConnectingCallIcon() = Icon(Icons.Outlined.SettingsPhone, stringResource(R.string.icon_descr_call_connecting), tint = Color.Green) + when (status) { + CICallStatus.Pending -> if (sent) { + Icon(Icons.Outlined.Call, stringResource(R.string.icon_descr_call_pending_sent)) + } else { + AcceptCallButton(cInfo, acceptCall) + } + CICallStatus.Missed -> Icon(Icons.Outlined.Call, stringResource(R.string.icon_descr_call_missed), tint = Color.Red) + CICallStatus.Rejected -> Icon(Icons.Outlined.CallEnd, stringResource(R.string.icon_descr_call_rejected), tint = HighOrLowlight) + CICallStatus.Accepted -> ConnectingCallIcon() + CICallStatus.Negotiated -> ConnectingCallIcon() + CICallStatus.Progress -> Icon(Icons.Filled.PhoneInTalk, stringResource(R.string.icon_descr_call_progress), tint = Color.Green) + CICallStatus.Ended -> Row { + Icon(Icons.Outlined.CallEnd, stringResource(R.string.icon_descr_call_ended), tint = HighOrLowlight, modifier = Modifier.padding(end = 4.dp)) + Text(status.duration(duration), color = HighOrLowlight) + } + } + + Text( + cItem.timestampText, + color = HighOrLowlight, + fontSize = 14.sp, + modifier = Modifier.padding(start = 3.dp) + ) + } +} + +@Composable +fun AcceptCallButton(cInfo: ChatInfo, acceptCall: (Contact) -> Unit) { + if (cInfo is ChatInfo.Direct) { + SimpleButton(stringResource(R.string.answer_call), Icons.Outlined.RingVolume) { acceptCall(cInfo.contact) } + } else { + Icon(Icons.Outlined.RingVolume, stringResource(R.string.answer_call), tint = HighOrLowlight) + } +// if case let .direct(contact) = chatInfo { +// Button { +// if let invitation = m.callInvitations.removeValue(forKey: contact.id) { +// m.activeCallInvitation = nil +// m.activeCall = Call( +// contact: contact, +// callState: .invitationReceived, +// localMedia: invitation.peerMedia, +// sharedKey: invitation.sharedKey +// ) +// m.showCallView = true +// m.callCommand = .start(media: invitation.peerMedia, aesKey: invitation.sharedKey, useWorker: true) +// } else { +// AlertManager.shared.showAlertMsg(title: "Call already ended!") +// } +// } label: { +// Label("Answer call", systemImage: "phone.arrow.down.left") +// } +// } else { +// Image(systemName: "phone.arrow.down.left").foregroundColor(.secondary) +// } + +} + +//struct CICallItemView: View { +// @EnvironmentObject var m: ChatModel +// var chatInfo: ChatInfo +// var chatItem: ChatItem +// var status: CICallStatus +// var duration: Int +// +// var body: some View { +// switch status { +// case .pending: +// if sent { +// Image(systemName: "phone.arrow.up.right").foregroundColor(.secondary) +// } else { +// acceptCallButton() +// } +// case .missed: missedCallIcon(sent).foregroundColor(.red) +// case .rejected: Image(systemName: "phone.down").foregroundColor(.secondary) +// case .accepted: connectingCallIcon() +// case .negotiated: connectingCallIcon() +// case .progress: Image(systemName: "phone.and.waveform.fill").foregroundColor(.green) +// case .ended: endedCallIcon(sent) +// case .error: missedCallIcon(sent).foregroundColor(.orange) +// } +// +// chatItem.timestampText +// .font(.caption) +// .foregroundColor(.secondary) +// .padding(.bottom, 8) +// .padding(.horizontal, 12) +// } +// } +// +// private func missedCallIcon(_ sent: Bool) -> some View { +// Image(systemName: sent ? "phone.arrow.up.right" : "phone.arrow.down.left") +// } +// +// private func connectingCallIcon() -> some View { +// Image(systemName: "phone.connection").foregroundColor(.green) +// } +// +// @ViewBuilder private func endedCallIcon(_ sent: Bool) -> some View { +// HStack { +// Image(systemName: "phone.down") +// Text(CICallStatus.durationText(duration)).foregroundColor(.secondary) +// } +// } +// +// +// @ViewBuilder private func acceptCallButton() -> some View { +// if case let .direct(contact) = chatInfo { +// Button { +// if let invitation = m.callInvitations.removeValue(forKey: contact.id) { +// m.activeCallInvitation = nil +// m.activeCall = Call( +// contact: contact, +// callState: .invitationReceived, +// localMedia: invitation.peerMedia, +// sharedKey: invitation.sharedKey +// ) +// m.showCallView = true +// m.callCommand = .start(media: invitation.peerMedia, aesKey: invitation.sharedKey, useWorker: true) +// } else { +// AlertManager.shared.showAlertMsg(title: "Call already ended!") +// } +// } label: { +// Label("Answer call", systemImage: "phone.arrow.down.left") +// } +// } else { +// Image(systemName: "phone.arrow.down.left").foregroundColor(.secondary) +// } +// } +//} \ No newline at end of file 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 b8e45484c9..58b3e38504 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 @@ -30,13 +30,15 @@ import kotlinx.datetime.Clock @Composable fun ChatItemView( user: User, + cInfo: ChatInfo, cItem: ChatItem, composeState: MutableState, cxt: Context, uriHandler: UriHandler? = null, showMember: Boolean = false, deleteMessage: (Long, CIDeleteMode) -> Unit, - receiveFile: (Long) -> Unit + receiveFile: (Long) -> Unit, + acceptCall: (Contact) -> Unit ) { val context = LocalContext.current val sent = cItem.chatDir.sent @@ -54,18 +56,12 @@ fun ChatItemView( .clip(RoundedCornerShape(18.dp)) .combinedClickable(onLongClick = { showMenu.value = true }, onClick = {}) ) { - if (cItem.isMsgContent) { + @Composable fun ContentItem() { if (cItem.file == null && cItem.quotedItem == null && isShortEmoji(cItem.content.text)) { EmojiItemView(cItem) } else { FramedItemView(user, cItem, uriHandler, showMember = showMember, showMenu, receiveFile) } - } else if (cItem.isDeletedContent) { - DeletedItemView(cItem, showMember = showMember) - } else if (cItem.isCall) { - FramedItemView(user, cItem, uriHandler, showMember = showMember, showMenu, receiveFile) - } - if (cItem.isMsgContent) { DropdownMenu( expanded = showMenu.value, onDismissRequest = { showMenu.value = false }, @@ -116,7 +112,10 @@ fun ChatItemView( color = Color.Red ) } - } else if (cItem.isDeletedContent) { + } + + @Composable fun DeletedItem() { + DeletedItemView(cItem, showMember = showMember) DropdownMenu( expanded = showMenu.value, onDismissRequest = { showMenu.value = false }, @@ -133,6 +132,19 @@ fun ChatItemView( ) } } + + @Composable fun CallItem(status: CICallStatus, duration: Int) { + CICallItemView(cInfo, cItem, status, duration, acceptCall) + } + + when (val c = cItem.content) { + is CIContent.SndMsgContent -> ContentItem() + is CIContent.RcvMsgContent -> ContentItem() + is CIContent.SndDeleted -> DeletedItem() + is CIContent.RcvDeleted -> DeletedItem() + is CIContent.SndCall -> CallItem(c.status, c.duration) + is CIContent.RcvCall -> CallItem(c.status, c.duration) + } } } } @@ -186,13 +198,15 @@ fun PreviewChatItemView() { SimpleXTheme { ChatItemView( User.sampleData, + ChatInfo.Direct.sampleData, ChatItem.getSampleData( 1, CIDirection.DirectSnd(), Clock.System.now(), "hello" ), composeState = remember { mutableStateOf(ComposeState()) }, cxt = LocalContext.current, deleteMessage = { _, _ -> }, - receiveFile = {} + receiveFile = {}, + acceptCall = { _ -> } ) } } @@ -203,11 +217,13 @@ fun PreviewChatItemViewDeletedContent() { SimpleXTheme { ChatItemView( User.sampleData, + ChatInfo.Direct.sampleData, ChatItem.getDeletedContentSampleData(), composeState = remember { mutableStateOf(ComposeState()) }, cxt = LocalContext.current, deleteMessage = { _, _ -> }, - receiveFile = {} + receiveFile = {}, + acceptCall = { _ -> } ) } } 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 78a24fe245..ad219eef1c 100644 --- a/apps/android/app/src/main/res/values-ru/strings.xml +++ b/apps/android/app/src/main/res/values-ru/strings.xml @@ -367,4 +367,13 @@ Audio off Audio on Flip camera + + + Pending call + Missed call + Rejected call + Connecting call + Call in progress + Call ended + Answer call diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index 0bb3c800b9..2915b56851 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -368,4 +368,13 @@ Audio off Audio on Flip camera + + + Pending call + Missed call + Rejected call + Connecting call + Call in progress + Call ended + Answer call diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index aaca8ce3cb..e754c4d38f 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -27,10 +27,11 @@ final class ChatModel: ObservableObject { @Published var deviceToken: String? @Published var tokenStatus = NtfTknStatus.new // current WebRTC call - @Published var callInvitations: Dictionary = [:] + @Published var callInvitations: Dictionary = [:] @Published var activeCallInvitation: ContactRef? @Published var activeCall: Call? @Published var callCommand: WCallCommand? + @Published var showCallView = false var messageDelivery: Dictionary Void> = [:] diff --git a/apps/ios/Shared/Model/NtfManager.swift b/apps/ios/Shared/Model/NtfManager.swift index a4c53c52ea..dfb01bb37b 100644 --- a/apps/ios/Shared/Model/NtfManager.swift +++ b/apps/ios/Shared/Model/NtfManager.swift @@ -38,10 +38,11 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject { } else if content.categoryIdentifier == ntfCategoryCallInvitation && (action == ntfActionAcceptCall || action == ntfActionRejectCall), let chatId = content.userInfo["chatId"] as? String, case let .direct(contact) = chatModel.getChat(chatId)?.chatInfo, - let invitation = chatModel.callInvitations[chatId] { + let invitation = chatModel.callInvitations.removeValue(forKey: chatId) { if action == ntfActionAcceptCall { + chatModel.activeCallInvitation = nil chatModel.activeCall = Call(contact: contact, callState: .invitationReceived, localMedia: invitation.peerMedia) - chatModel.chatId = nil + chatModel.showCallView = true chatModel.callCommand = .start(media: invitation.peerMedia, aesKey: invitation.sharedKey) } else { Task { @@ -56,7 +57,6 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject { } } } - chatModel.callInvitations.removeValue(forKey: chatId) } else { chatModel.chatId = content.targetContentIdentifier } diff --git a/apps/ios/Shared/Model/Shared/ChatTypes.swift b/apps/ios/Shared/Model/Shared/ChatTypes.swift index 57d7b747fd..f737cc8912 100644 --- a/apps/ios/Shared/Model/Shared/ChatTypes.swift +++ b/apps/ios/Shared/Model/Shared/ChatTypes.swift @@ -884,8 +884,12 @@ enum CICallStatus: String, Decodable { 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 .ended: return String.localizedStringWithFormat(NSLocalizedString("ended %@", comment: "call status"), CICallStatus.durationText(sec)) case .error: return NSLocalizedString("error", comment: "call status") } } + + static func durationText(_ sec: Int) -> String { + String(format: "%02d:%02d", sec / 60, sec % 60) + } } diff --git a/apps/ios/Shared/Views/Call/ActiveCallView.swift b/apps/ios/Shared/Views/Call/ActiveCallView.swift index 995ccaab03..760c02e6c4 100644 --- a/apps/ios/Shared/Views/Call/ActiveCallView.swift +++ b/apps/ios/Shared/Views/Call/ActiveCallView.swift @@ -11,7 +11,6 @@ import SwiftUI struct ActiveCallView: View { @EnvironmentObject var chatModel: ChatModel @Environment(\.dismiss) private var dismiss - @Binding var showCallView: Bool @State private var coordinator: WebRTCCoordinator? = nil @State private var webViewReady: Bool = false @State private var webViewMsg: WVAPIMessage? = nil @@ -71,7 +70,7 @@ struct ActiveCallView: View { m.activeCall = nil m.activeCallInvitation = nil m.callCommand = nil - showCallView = false + m.showCallView = false case .ok: switch msg.command { case let .media(media, enable): @@ -85,7 +84,7 @@ struct ActiveCallView: View { m.activeCall = nil m.activeCallInvitation = nil m.callCommand = nil - showCallView = false + m.showCallView = false default: () } case let .error(message): diff --git a/apps/ios/Shared/Views/Call/WebRTC.swift b/apps/ios/Shared/Views/Call/WebRTC.swift index 4d87f263f8..066dcc7798 100644 --- a/apps/ios/Shared/Views/Call/WebRTC.swift +++ b/apps/ios/Shared/Views/Call/WebRTC.swift @@ -388,7 +388,7 @@ struct ConnectionInfo: Codable, Equatable { return "via relay" } else { let unknown = NSLocalizedString("unknown", comment: "connection info") - return "\(localCandidate?.candidateType?.rawValue ?? unknown)) / \(remoteCandidate?.candidateType?.rawValue ?? unknown)" + return "\(localCandidate?.candidateType?.rawValue ?? unknown) / \(remoteCandidate?.candidateType?.rawValue ?? unknown)" } } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CICallItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CICallItemView.swift new file mode 100644 index 0000000000..96465ae387 --- /dev/null +++ b/apps/ios/Shared/Views/Chat/ChatItem/CICallItemView.swift @@ -0,0 +1,90 @@ +// +// CICallItemView.swift +// SimpleX (iOS) +// +// Created by Evgeny on 20/05/2022. +// Copyright Β© 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +struct CICallItemView: View { + @EnvironmentObject var m: ChatModel + var chatInfo: ChatInfo + var chatItem: ChatItem + var status: CICallStatus + var duration: Int + + var body: some View { + let sent = chatItem.chatDir.sent + VStack(spacing: 4) { + switch status { + case .pending: + if sent { + Image(systemName: "phone.arrow.up.right").foregroundColor(.secondary) + } else { + acceptCallButton() + } + case .missed: missedCallIcon(sent).foregroundColor(.red) + case .rejected: Image(systemName: "phone.down").foregroundColor(.secondary) + case .accepted: connectingCallIcon() + case .negotiated: connectingCallIcon() + case .progress: Image(systemName: "phone.and.waveform.fill").foregroundColor(.green) + case .ended: endedCallIcon(sent) + case .error: missedCallIcon(sent).foregroundColor(.orange) + } + + chatItem.timestampText + .font(.caption) + .foregroundColor(.secondary) + .padding(.bottom, 8) + .padding(.horizontal, 12) + } + } + + private func missedCallIcon(_ sent: Bool) -> some View { + Image(systemName: sent ? "phone.arrow.up.right" : "phone.arrow.down.left") + } + + private func connectingCallIcon() -> some View { + Image(systemName: "phone.connection").foregroundColor(.green) + } + + @ViewBuilder private func endedCallIcon(_ sent: Bool) -> some View { + HStack { + Image(systemName: "phone.down") + Text(CICallStatus.durationText(duration)).foregroundColor(.secondary) + } + } + + + @ViewBuilder private func acceptCallButton() -> some View { + if case let .direct(contact) = chatInfo { + Button { + if let invitation = m.callInvitations.removeValue(forKey: contact.id) { + m.activeCallInvitation = nil + m.activeCall = Call( + contact: contact, + callState: .invitationReceived, + localMedia: invitation.peerMedia, + sharedKey: invitation.sharedKey + ) + m.showCallView = true + m.callCommand = .start(media: invitation.peerMedia, aesKey: invitation.sharedKey, useWorker: true) + } else { + AlertManager.shared.showAlertMsg(title: "Call already ended!") + } + } label: { + Label("Answer call", systemImage: "phone.arrow.down.left") + } + } else { + Image(systemName: "phone.arrow.down.left").foregroundColor(.secondary) + } + } +} + +//struct CICallItemView_Previews: PreviewProvider { +// static var previews: some View { +// CICallItemView() +// } +//} diff --git a/apps/ios/Shared/Views/Chat/ChatItemView.swift b/apps/ios/Shared/Views/Chat/ChatItemView.swift index 6d549a92b8..f86f4d7550 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemView.swift @@ -9,34 +9,48 @@ import SwiftUI struct ChatItemView: View { + var chatInfo: ChatInfo var chatItem: ChatItem var showMember = false var maxWidth: CGFloat = .infinity var body: some View { - if chatItem.isMsgContent() { - if (chatItem.quotedItem == nil && chatItem.file == nil && isShortEmoji(chatItem.content.text)) { - EmojiItemView(chatItem: chatItem) - } else { - FramedItemView(chatItem: chatItem, showMember: showMember, maxWidth: maxWidth) - } - } else if chatItem.isDeletedContent() { - DeletedItemView(chatItem: chatItem, showMember: showMember) - } else if chatItem.isCall() { + switch chatItem.content { + case .sndMsgContent: contentItemView() + case .rcvMsgContent: contentItemView() + case .sndDeleted: deletedItemView() + case .rcvDeleted: deletedItemView() + case let .sndCall(status, duration): callItemView(status, duration) + case let .rcvCall(status, duration): callItemView(status, duration) + } + } + + @ViewBuilder private func contentItemView() -> some View { + if (chatItem.quotedItem == nil && chatItem.file == nil && isShortEmoji(chatItem.content.text)) { + EmojiItemView(chatItem: chatItem) + } else { FramedItemView(chatItem: chatItem, showMember: showMember, maxWidth: maxWidth) } } + + private func deletedItemView() -> some View { + DeletedItemView(chatItem: chatItem, showMember: showMember) + } + + private func callItemView(_ status: CICallStatus, _ duration: Int) -> some View { + CICallItemView(chatInfo: chatInfo, chatItem: chatItem, status: status, duration: duration) + } } struct ChatItemView_Previews: PreviewProvider { static var previews: some View { Group{ - ChatItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "hello")) - ChatItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too")) - ChatItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "πŸ™‚")) - ChatItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "πŸ™‚πŸ™‚πŸ™‚πŸ™‚πŸ™‚")) - ChatItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "πŸ™‚πŸ™‚πŸ™‚πŸ™‚πŸ™‚πŸ™‚")) - ChatItemView(chatItem: ChatItem.getDeletedContentSample()) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello")) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too")) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "πŸ™‚")) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "πŸ™‚πŸ™‚πŸ™‚πŸ™‚πŸ™‚")) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "πŸ™‚πŸ™‚πŸ™‚πŸ™‚πŸ™‚πŸ™‚")) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getDeletedContentSample()) } .previewLayout(.fixed(width: 360, height: 70)) } diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index d19cb4baf8..0a92153a10 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -14,7 +14,6 @@ struct ChatView: View { @EnvironmentObject var chatModel: ChatModel @Environment(\.colorScheme) var colorScheme @ObservedObject var chat: Chat - @Binding var showCallView: Bool @State private var composeState = ComposeState() @State private var deletingItem: ChatItem? = nil @FocusState private var keyboardVisible: Bool @@ -125,7 +124,7 @@ struct ChatView: View { callState: .waitCapabilities, localMedia: media ) - showCallView = true + chatModel.showCallView = true chatModel.callCommand = .capabilities(useWorker: true) } label: { Image(systemName: imageName) @@ -134,7 +133,7 @@ struct ChatView: View { private func chatItemWithMenu(_ ci: ChatItem, _ maxWidth: CGFloat, showMember: Bool = false) -> some View { let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading - return ChatItemView(chatItem: ci, showMember: showMember, maxWidth: maxWidth) + return ChatItemView(chatInfo: chat.chatInfo, chatItem: ci, showMember: showMember, maxWidth: maxWidth) .contextMenu { if ci.isMsgContent() { Button { @@ -263,7 +262,6 @@ struct ChatView: View { struct ChatView_Previews: PreviewProvider { static var previews: some View { - @State var showCallView = false let chatModel = ChatModel() chatModel.chatId = "@1" chatModel.chatItems = [ @@ -277,7 +275,7 @@ struct ChatView_Previews: PreviewProvider { ChatItem.getSample(8, .directSnd, .now, "πŸ‘πŸ‘πŸ‘πŸ‘"), ChatItem.getSample(9, .directSnd, .now, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.") ] - return ChatView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []), showCallView: $showCallView) + return ChatView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: [])) .environmentObject(chatModel) } } diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index cd02499ec0..f23d29583c 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -11,7 +11,6 @@ import SwiftUI struct ChatListNavLink: View { @EnvironmentObject var chatModel: ChatModel @State var chat: Chat - @Binding var showCallView: Bool @State private var showContactRequestDialog = false var body: some View { @@ -28,7 +27,7 @@ struct ChatListNavLink: View { } private func chatView() -> some View { - ChatView(chat: chat, showCallView: $showCallView) + ChatView(chat: chat) .onAppear { do { let cInfo = chat.chatInfo @@ -279,20 +278,19 @@ struct ChatListNavLink: View { struct ChatListNavLink_Previews: PreviewProvider { static var previews: some View { @State var chatId: String? = "@1" - @State var showCallView = false return Group { ChatListNavLink(chat: Chat( chatInfo: ChatInfo.sampleData.direct, chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello")] - ), showCallView: $showCallView) + )) ChatListNavLink(chat: Chat( chatInfo: ChatInfo.sampleData.direct, chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello")] - ), showCallView: $showCallView) + )) ChatListNavLink(chat: Chat( chatInfo: ChatInfo.sampleData.contactRequest, chatItems: [] - ), showCallView: $showCallView) + )) } .previewLayout(.fixed(width: 360, height: 80)) } diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index 248f331769..13f511610d 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -13,7 +13,6 @@ struct ChatListView: View { // not really used in this view @State private var showSettings = false @State private var searchText = "" - @State private var showCallView = false @AppStorage(DEFAULT_PENDING_CONNECTIONS) private var pendingConnections = true var user: User @@ -22,7 +21,7 @@ struct ChatListView: View { let v = NavigationView { List { ForEach(filteredChats()) { chat in - ChatListNavLink(chat: chat, showCallView: $showCallView) + ChatListNavLink(chat: chat) .padding(.trailing, -16) } } @@ -50,11 +49,11 @@ struct ChatListView: View { NewChatButton() } } - .fullScreenCover(isPresented: $showCallView) { - ActiveCallView(showCallView: $showCallView) + .fullScreenCover(isPresented: $chatModel.showCallView) { + ActiveCallView() } - .onChange(of: showCallView) { _ in - if (showCallView) { return } + .onChange(of: chatModel.showCallView) { _ in + if (chatModel.showCallView) { return } if let call = chatModel.activeCall { Task { do { @@ -69,7 +68,7 @@ struct ChatListView: View { .onChange(of: chatModel.activeCallInvitation) { _ in if let contactRef = chatModel.activeCallInvitation, case let .direct(contact) = chatModel.getChat(contactRef.id)?.chatInfo, - let invitation = chatModel.callInvitations.removeValue(forKey: contactRef.id) { + let invitation = chatModel.callInvitations[contactRef.id] { answerCallAlert(contact, invitation) } } @@ -105,11 +104,8 @@ struct ChatListView: View { Text(invitation.callTypeText) + 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 { - AlertManager.shared.showAlertMsg(title: "Call already ended!") - } - } else { + if let activeCallInvitation = chatModel.activeCallInvitation { + chatModel.callInvitations.removeValue(forKey: activeCallInvitation.id) chatModel.activeCallInvitation = nil chatModel.activeCall = Call( contact: contact, @@ -117,8 +113,12 @@ struct ChatListView: View { localMedia: invitation.peerMedia, sharedKey: invitation.sharedKey ) - showCallView = true + chatModel.showCallView = true chatModel.callCommand = .start(media: invitation.peerMedia, aesKey: invitation.sharedKey, useWorker: true) + } else { + DispatchQueue.main.async { + AlertManager.shared.showAlertMsg(title: "Call already ended!") + } } }, secondaryButton: .cancel() diff --git a/apps/ios/Shared/Views/Helpers/CIFileView.swift b/apps/ios/Shared/Views/Helpers/CIFileView.swift index c192b0644f..a7a7119c74 100644 --- a/apps/ios/Shared/Views/Helpers/CIFileView.swift +++ b/apps/ios/Shared/Views/Helpers/CIFileView.swift @@ -148,16 +148,16 @@ struct CIFileView_Previews: PreviewProvider { file: nil ) Group{ - ChatItemView(chatItem: sentFile) - ChatItemView(chatItem: ChatItem.getFileMsgContentSample()) - ChatItemView(chatItem: ChatItem.getFileMsgContentSample(fileName: "some_long_file_name_here", fileStatus: .rcvInvitation)) - ChatItemView(chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvAccepted)) - ChatItemView(chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvTransfer)) - ChatItemView(chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvCancelled)) - ChatItemView(chatItem: ChatItem.getFileMsgContentSample(fileSize: 1_000_000_000, fileStatus: .rcvInvitation)) - ChatItemView(chatItem: ChatItem.getFileMsgContentSample(text: "Hello there", fileStatus: .rcvInvitation)) - ChatItemView(chatItem: ChatItem.getFileMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", fileStatus: .rcvInvitation)) - ChatItemView(chatItem: fileChatItemWtFile) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: sentFile) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample()) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileName: "some_long_file_name_here", fileStatus: .rcvInvitation)) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvAccepted)) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvTransfer)) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvCancelled)) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileSize: 1_000_000_000, fileStatus: .rcvInvitation)) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(text: "Hello there", fileStatus: .rcvInvitation)) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", fileStatus: .rcvInvitation)) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: fileChatItemWtFile) } .previewLayout(.fixed(width: 360, height: 360)) } diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 7664421024..f2331ef427 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 3C8C548928133C84000A3EC7 /* PasteToConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C8C548828133C84000A3EC7 /* PasteToConnectView.swift */; }; 3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */; }; 3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4727FF621E00354CDD /* CILinkView.swift */; }; + 5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA72837DBB3004A9677 /* CICallItemView.swift */; }; 5C063D2727A4564100AEC577 /* ChatPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C063D2627A4564100AEC577 /* ChatPreviewView.swift */; }; 5C116CDC27AABE0400E66D01 /* ContactRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */; }; 5C13730B28156D2700F43030 /* ContactConnectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C13730A28156D2700F43030 /* ContactConnectionView.swift */; }; @@ -128,6 +129,7 @@ 3C8C548828133C84000A3EC7 /* PasteToConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasteToConnectView.swift; sourceTree = ""; }; 3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeLinkView.swift; sourceTree = ""; }; 3CDBCF4727FF621E00354CDD /* CILinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CILinkView.swift; sourceTree = ""; }; + 5C029EA72837DBB3004A9677 /* CICallItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CICallItemView.swift; sourceTree = ""; }; 5C063D2627A4564100AEC577 /* ChatPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPreviewView.swift; sourceTree = ""; }; 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactRequestView.swift; sourceTree = ""; }; 5C13730A28156D2700F43030 /* ContactConnectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactConnectionView.swift; sourceTree = ""; }; @@ -496,6 +498,7 @@ 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */, 5C3A88D027DF57800060F1C2 /* FramedItemView.swift */, 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */, + 5C029EA72837DBB3004A9677 /* CICallItemView.swift */, ); path = ChatItem; sourceTree = ""; @@ -718,6 +721,7 @@ 5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */, 5C5E5D3D282447AB00B0488A /* CallTypes.swift in Sources */, 64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */, + 5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */, 5CE4407227ADB1D0007B033A /* Emoji.swift in Sources */, 5C1A4C1E27A715B700EAD5AD /* ChatItemView.swift in Sources */, 64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */,