diff --git a/Session/Calls/Call Management/SessionCall.swift b/Session/Calls/Call Management/SessionCall.swift index e2e8b4a20..50a2a414d 100644 --- a/Session/Calls/Call Management/SessionCall.swift +++ b/Session/Calls/Call Management/SessionCall.swift @@ -9,7 +9,9 @@ public final class SessionCall: NSObject, WebRTCSessionDelegate { let sessionID: String let mode: Mode let webRTCSession: WebRTCSession + let isOutgoing: Bool var remoteSDP: RTCSessionDescription? = nil + var callMessageTimestamp: UInt64? var isWaitingForRemoteSDP = false var contactName: String { let contact = Storage.shared.getContact(with: self.sessionID) @@ -59,6 +61,12 @@ public final class SessionCall: NSObject, WebRTCSessionDelegate { case answer } + // MARK: End call mode + enum EndCallMode { + case local + case remote + } + // MARK: Call State Properties var connectingDate: Date? { didSet { @@ -115,16 +123,20 @@ public final class SessionCall: NSObject, WebRTCSessionDelegate { guard let connectedDate = connectedDate else { return 0 } + if let endDate = endDate { + return endDate.timeIntervalSince(connectedDate) + } return Date().timeIntervalSince(connectedDate) } // MARK: Initialization - init(for sessionID: String, uuid: String, mode: Mode) { + init(for sessionID: String, uuid: String, mode: Mode, outgoing: Bool = false) { self.sessionID = sessionID self.uuid = UUID(uuidString: uuid)! self.mode = mode self.webRTCSession = WebRTCSession.current ?? WebRTCSession(for: sessionID, with: uuid) + self.isOutgoing = outgoing WebRTCSession.current = self.webRTCSession super.init() self.webRTCSession.delegate = self @@ -160,8 +172,9 @@ public final class SessionCall: NSObject, WebRTCSessionDelegate { }, completion: { [weak self] in let _ = promise.done { Storage.shared.write { transaction in - self?.webRTCSession.sendOffer(to: self!.sessionID, using: transaction as! YapDatabaseReadWriteTransaction).done { + self?.webRTCSession.sendOffer(to: self!.sessionID, using: transaction as! YapDatabaseReadWriteTransaction).done { timestamp in self?.hasStartedConnecting = true + self?.callMessageTimestamp = timestamp }.retainUntilComplete() } } @@ -186,11 +199,49 @@ public final class SessionCall: NSObject, WebRTCSessionDelegate { hasEnded = true } + // MARK: Update call message + func updateCallMessage(mode: EndCallMode) { + guard let callMessageTimestamp = callMessageTimestamp else { return } + Storage.write { transaction in + let tsMessage: TSMessage? + if self.isOutgoing { + tsMessage = TSOutgoingMessage.find(withTimestamp: callMessageTimestamp) + } else { + tsMessage = TSIncomingMessage.find(withAuthorId: self.sessionID, timestamp: callMessageTimestamp, transaction: transaction) + } + if let messageToUpdate = tsMessage { + var shouldMarkAsRead = false + let newMessageBody: String + if self.duration > 0 { + let durationString = NSString.formatDurationSeconds(UInt32(self.duration), useShortFormat: true) + newMessageBody = "\(self.isOutgoing ? NSLocalizedString("call_outgoing", comment: "") : NSLocalizedString("call_incoming", comment: "")): \(durationString)" + shouldMarkAsRead = true + } else { + switch mode { + case .local: + newMessageBody = self.isOutgoing ? NSLocalizedString("call_cancelled", comment: "") : NSLocalizedString("call_rejected", comment: "") + shouldMarkAsRead = true + case .remote: + newMessageBody = self.isOutgoing ? NSLocalizedString("call_rejected", comment: "") : NSLocalizedString("call_missing", comment: "") + } + } + messageToUpdate.updateCall(withNewBody: newMessageBody, transaction: transaction) + if let incomingMessage = tsMessage as? TSIncomingMessage, shouldMarkAsRead { + incomingMessage.markAsReadNow(withSendReadReceipt: false, transaction: transaction) + } + } + } + } + // MARK: Renderer func attachRemoteVideoRenderer(_ renderer: RTCVideoRenderer) { webRTCSession.attachRemoteRenderer(renderer) } + func removeRemoteVideoRenderer(_ renderer: RTCVideoRenderer) { + webRTCSession.removeRemoteRenderer(renderer) + } + func attachLocalVideoRenderer(_ renderer: RTCVideoRenderer) { webRTCSession.attachLocalRenderer(renderer) } diff --git a/Session/Calls/Call Management/SessionCallManager.swift b/Session/Calls/Call Management/SessionCallManager.swift index 7e4c2ffc0..8397dbe77 100644 --- a/Session/Calls/Call Management/SessionCallManager.swift +++ b/Session/Calls/Call Management/SessionCallManager.swift @@ -95,6 +95,9 @@ public final class SessionCallManager: NSObject { guard let call = currentCall else { return } if let reason = reason { self.provider.reportCall(with: call.uuid, endedAt: nil, reason: reason) + call.updateCallMessage(mode: .remote) + } else { + call.updateCallMessage(mode: .local) } self.currentCall?.webRTCSession.dropConnection() self.currentCall = nil diff --git a/Session/Calls/Views & Modals/MiniCallView.swift b/Session/Calls/Views & Modals/MiniCallView.swift index 4e991f996..f7d5869ce 100644 --- a/Session/Calls/Views & Modals/MiniCallView.swift +++ b/Session/Calls/Views & Modals/MiniCallView.swift @@ -125,6 +125,7 @@ final class MiniCallView: UIView { UIView.animate(withDuration: 0.5, delay: 0, options: [], animations: { self.alpha = 0.0 }, completion: { _ in + self.callVC.call.removeRemoteVideoRenderer(self.remoteVideoView) MiniCallView.current = nil self.removeFromSuperview() }) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 7be62ee46..7bf6de1c4 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -32,7 +32,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc if userDefaults[.hasSeenCallIPExposureWarning] { guard let contactSessionID = (thread as? TSContactThread)?.contactSessionID() else { return } guard AppEnvironment.shared.callManager.currentCall == nil else { return } - let call = SessionCall(for: contactSessionID, uuid: UUID().uuidString, mode: .offer) + let call = SessionCall(for: contactSessionID, uuid: UUID().uuidString, mode: .offer, outgoing: true) let callVC = CallVC(for: call) callVC.conversationVC = self self.inputAccessoryView?.isHidden = true diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index afe933801..20a92a884 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -6,34 +6,34 @@ import UIKit extension AppDelegate { // MARK: Call handling - func createNewIncomingCall(caller: String, uuid: String) { - DispatchQueue.main.async { - let call = SessionCall(for: caller, uuid: uuid, mode: .answer) - if CurrentAppContext().isMainAppAndActive { - guard let presentingVC = CurrentAppContext().frontmostViewController() else { preconditionFailure() } // TODO: Handle more gracefully - if let conversationVC = presentingVC as? ConversationVC, let contactThread = conversationVC.thread as? TSContactThread, contactThread.contactSessionID() == caller { - let callVC = CallVC(for: call) - callVC.conversationVC = conversationVC - conversationVC.inputAccessoryView?.isHidden = true - conversationVC.inputAccessoryView?.alpha = 0 - presentingVC.present(callVC, animated: true, completion: nil) - } - } - call.reportIncomingCallIfNeeded{ error in - if let error = error { - SNLog("[Calls] Failed to report incoming call to CallKit due to error: \(error)") - let incomingCallBanner = IncomingCallBanner(for: call) - incomingCallBanner.show() - } - } - } - } - @objc func setUpCallHandling() { // Pre offer messages MessageReceiver.handlePreOfferCallMessage = { message in guard CurrentAppContext().isMainApp else { return } - self.createNewIncomingCall(caller: message.sender!, uuid: message.uuid!) + DispatchQueue.main.async { + if let caller = message.sender, let uuid = message.uuid { + let call = SessionCall(for: caller, uuid: uuid, mode: .answer) + call.callMessageTimestamp = message.sentTimestamp + if CurrentAppContext().isMainAppAndActive { + guard let presentingVC = CurrentAppContext().frontmostViewController() else { preconditionFailure() } // TODO: Handle more gracefully + if let conversationVC = presentingVC as? ConversationVC, let contactThread = conversationVC.thread as? TSContactThread, contactThread.contactSessionID() == caller { + let callVC = CallVC(for: call) + callVC.conversationVC = conversationVC + conversationVC.inputAccessoryView?.isHidden = true + conversationVC.inputAccessoryView?.alpha = 0 + presentingVC.present(callVC, animated: true, completion: nil) + } + } + call.reportIncomingCallIfNeeded{ error in + if let error = error { + SNLog("[Calls] Failed to report incoming call to CallKit due to error: \(error)") + let incomingCallBanner = IncomingCallBanner(for: call) + incomingCallBanner.show() + } + } + } + + } } // Offer messages MessageReceiver.handleOfferCallMessage = { message in diff --git a/Session/Meta/Translations/en.lproj/Localizable.strings b/Session/Meta/Translations/en.lproj/Localizable.strings index 2f50889d2..31673a38b 100644 --- a/Session/Meta/Translations/en.lproj/Localizable.strings +++ b/Session/Meta/Translations/en.lproj/Localizable.strings @@ -577,6 +577,9 @@ "OPEN_SETTINGS_BUTTON" = "Settings"; "call_outgoing" = "Outgoing Call"; "call_incoming" = "Incoming Call"; +"call_missing" = "Missing Call"; +"call_rejected" = "Rejected Call"; +"call_cancelled" = "Cancelled Call"; "voice_call" = "Voice Call"; "video_call" = "Video Call"; "APN_Message" = "You've got a new message"; diff --git a/SessionMessagingKit/Calls/WebRTCSession+UI.swift b/SessionMessagingKit/Calls/WebRTCSession+UI.swift index 7ed0a54e8..305d1bfb7 100644 --- a/SessionMessagingKit/Calls/WebRTCSession+UI.swift +++ b/SessionMessagingKit/Calls/WebRTCSession+UI.swift @@ -10,6 +10,10 @@ extension WebRTCSession { remoteVideoTrack?.add(renderer) } + public func removeRemoteRenderer(_ renderer: RTCVideoRenderer) { + remoteVideoTrack?.remove(renderer) + } + public func handleLocalFrameCaptured(_ videoFrame: RTCVideoFrame) { guard let videoCapturer = delegate?.videoCapturer else { return } localVideoSource.capturer(videoCapturer, didCapture: videoFrame) diff --git a/SessionMessagingKit/Calls/WebRTCSession.swift b/SessionMessagingKit/Calls/WebRTCSession.swift index 95d617a67..79da8db65 100644 --- a/SessionMessagingKit/Calls/WebRTCSession.swift +++ b/SessionMessagingKit/Calls/WebRTCSession.swift @@ -128,10 +128,10 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate { return promise } - public func sendOffer(to sessionID: String, using transaction: YapDatabaseReadWriteTransaction) -> Promise { + public func sendOffer(to sessionID: String, using transaction: YapDatabaseReadWriteTransaction) -> Promise { print("[Calls] Sending offer message.") guard let thread = TSContactThread.fetch(for: sessionID, using: transaction) else { return Promise(error: Error.noThread) } - let (promise, seal) = Promise.pending() + let (promise, seal) = Promise.pending() peerConnection.offer(for: mediaConstraints) { [weak self] sdp, error in if let error = error { seal.reject(error) @@ -152,7 +152,7 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate { let tsMessage = TSOutgoingMessage.from(message, associatedWith: thread) tsMessage.save(with: transaction) MessageSender.sendNonDurably(message, in: thread, using: transaction).done2 { - seal.fulfill(()) + seal.fulfill(tsMessage.timestamp) }.catch2 { error in seal.reject(error) } diff --git a/SessionMessagingKit/Messages/Signal/TSMessage.h b/SessionMessagingKit/Messages/Signal/TSMessage.h index b6096bd43..8739181c0 100644 --- a/SessionMessagingKit/Messages/Signal/TSMessage.h +++ b/SessionMessagingKit/Messages/Signal/TSMessage.h @@ -86,6 +86,8 @@ extern const NSUInteger kOversizeTextMessageSizeThreshold; - (void)updateForDeletionWithTransaction:(YapDatabaseReadWriteTransaction *)transaction; +- (void)updateCallMessageWithNewBody:(NSString *)newBody transaction:(YapDatabaseReadWriteTransaction *)transaction; + @end NS_ASSUME_NONNULL_END diff --git a/SessionMessagingKit/Messages/Signal/TSMessage.m b/SessionMessagingKit/Messages/Signal/TSMessage.m index 39ff5530c..0584aaf74 100644 --- a/SessionMessagingKit/Messages/Signal/TSMessage.m +++ b/SessionMessagingKit/Messages/Signal/TSMessage.m @@ -442,6 +442,15 @@ const NSUInteger kOversizeTextMessageSizeThreshold = 2 * 1024; }]; } +- (void)updateCallMessageWithNewBody:(NSString *)newBody transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + if (!_isCallMessage) { return; } + [self applyChangeToSelfAndLatestCopy:transaction + changeBlock:^(TSMessage *message) { + [message setBody:newBody]; + }]; +} + @end NS_ASSUME_NONNULL_END diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift index cede76d96..b33962655 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift @@ -281,13 +281,6 @@ extension MessageReceiver { // TODO: Call in progress, put the new call on hold/reject return } - handlePreOfferCallMessage?(message) - case .offer: - print("[Calls] Received offer message.") - if getWebRTCSession().uuid != message.uuid! { - // TODO: Call in progress, put the new call on hold/reject - return - } let storage = SNMessagingKitConfiguration.shared.storage let transaction = transaction as! YapDatabaseReadWriteTransaction if let threadID = storage.getOrCreateThread(for: message.sender!, groupPublicKey: message.groupPublicKey, openGroupID: nil, using: transaction), @@ -295,9 +288,13 @@ extension MessageReceiver { let tsMessage = TSIncomingMessage.from(message, associatedWith: thread) tsMessage.save(with: transaction) } - // Delegate to the main app, which is expected to show a dialog confirming - // that the user wants to pick up the call. When they do, the SDP contained - // in the offer message will be passed to WebRTCSession.handleRemoteSDP(_:from:). + handlePreOfferCallMessage?(message) + case .offer: + print("[Calls] Received offer message.") + if getWebRTCSession().uuid != message.uuid! { + // TODO: Call in progress, put the new call on hold/reject + return + } handleOfferCallMessage?(message) case .answer: print("[Calls] Received answer message.")