From fbe5b12c9d008da4b74f358fb273309e9b1f4bee Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Wed, 6 Oct 2021 17:00:12 +1100 Subject: [PATCH] WIP: incoming call banner --- Session.xcodeproj/project.pbxproj | 12 ++ Session/Calls/CallVC.swift | 10 +- .../Views & Modals/IncomingCallBanner.swift | 146 ++++++++++++++++-- .../ConversationVC+Interaction.swift | 15 +- Session/Conversations/ConversationVC.swift | 1 - Session/Meta/AppDelegate.swift | 13 +- SessionMessagingKit/Calls/WebRTCSession.swift | 8 +- .../MessageReceiver+Handling.swift | 5 +- 8 files changed, 179 insertions(+), 31 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 19035d934..b4e6571f4 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -140,6 +140,7 @@ 7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */; }; 7B7CB189270430D20079FF93 /* CallMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB188270430D20079FF93 /* CallMessageView.swift */; }; 7B7CB18B270591630079FF93 /* ShareLogsModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18A270591630079FF93 /* ShareLogsModal.swift */; }; + 7B7CB18E270D066F0079FF93 /* IncomingCallBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18D270D066F0079FF93 /* IncomingCallBanner.swift */; }; 7BC01A3E241F40AB00BC7C55 /* NotificationServiceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC01A3D241F40AB00BC7C55 /* NotificationServiceExtension.swift */; }; 7BC01A42241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 7BC01A3B241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 7BCD116C27016062006330F1 /* WebRTCSession+DataChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BCD116B27016062006330F1 /* WebRTCSession+DataChannel.swift */; }; @@ -1116,6 +1117,7 @@ 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedMessageView.swift; sourceTree = ""; }; 7B7CB188270430D20079FF93 /* CallMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallMessageView.swift; sourceTree = ""; }; 7B7CB18A270591630079FF93 /* ShareLogsModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareLogsModal.swift; sourceTree = ""; }; + 7B7CB18D270D066F0079FF93 /* IncomingCallBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallBanner.swift; sourceTree = ""; }; 7BC01A3B241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SessionNotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 7BC01A3D241F40AB00BC7C55 /* NotificationServiceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationServiceExtension.swift; sourceTree = ""; }; 7BC01A3F241F40AB00BC7C55 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -2040,6 +2042,14 @@ path = Utilities; sourceTree = ""; }; + 7B7CB18C270D06350079FF93 /* Views & Modals */ = { + isa = PBXGroup; + children = ( + 7B7CB18D270D066F0079FF93 /* IncomingCallBanner.swift */, + ); + path = "Views & Modals"; + sourceTree = ""; + }; 7BC01A3C241F40AB00BC7C55 /* SessionNotificationServiceExtension */ = { isa = PBXGroup; children = ( @@ -2334,6 +2344,7 @@ B877E24126CA12910007970A /* CallVC.swift */, B877E24526CA13BA0007970A /* CallVC+Camera.swift */, B8B558F026C4BB0600693325 /* CameraManager.swift */, + 7B7CB18C270D06350079FF93 /* Views & Modals */, ); path = Calls; sourceTree = ""; @@ -4928,6 +4939,7 @@ C31D1DE9252172D4005D4DA8 /* ContactUtilities.swift in Sources */, 4521C3C01F59F3BA00B4C582 /* TextFieldHelper.swift in Sources */, 340FC8AC204DAC8D007AEB0F /* PrivacySettingsTableViewController.m in Sources */, + 7B7CB18E270D066F0079FF93 /* IncomingCallBanner.swift in Sources */, B8569AE325CBB19A00DBA3DB /* DocumentView.swift in Sources */, B85357BF23A1AE0800AAF6CD /* SeedReminderView.swift in Sources */, B821494F25D4E163009C0F2A /* BodyTextView.swift in Sources */, diff --git a/Session/Calls/CallVC.swift b/Session/Calls/CallVC.swift index f3d0f3923..be1cb7252 100644 --- a/Session/Calls/CallVC.swift +++ b/Session/Calls/CallVC.swift @@ -6,8 +6,10 @@ import UIKit final class CallVC : UIViewController, WebRTCSessionDelegate { let sessionID: String + let uuid: String let mode: Mode let webRTCSession: WebRTCSession + var shouldAnswer = false var isMuted = false var isVideoEnabled = false var conversationVC: ConversationVC? = nil @@ -162,10 +164,11 @@ final class CallVC : UIViewController, WebRTCSessionDelegate { } // MARK: Lifecycle - init(for sessionID: String, mode: Mode) { + init(for sessionID: String, uuid: String, mode: Mode) { self.sessionID = sessionID + self.uuid = uuid self.mode = mode - self.webRTCSession = WebRTCSession.current ?? WebRTCSession(for: sessionID, with: UUID().uuidString) + self.webRTCSession = WebRTCSession.current ?? WebRTCSession(for: sessionID, with: uuid) super.init(nibName: nil, bundle: nil) self.webRTCSession.delegate = self } @@ -193,6 +196,7 @@ final class CallVC : UIViewController, WebRTCSessionDelegate { } answerButton.isHidden = true } + if shouldAnswer { answerCall() } } func setUpViewHierarchy() { @@ -299,8 +303,6 @@ final class CallVC : UIViewController, WebRTCSessionDelegate { print("[Calls] Ending call.") callInfoLabel.isHidden = false callInfoLabel.text = "Call Ended" - WebRTCSession.current?.dropConnection() - WebRTCSession.current = nil UIView.animate(withDuration: 0.25) { self.remoteVideoView.alpha = 0 } diff --git a/Session/Calls/Views & Modals/IncomingCallBanner.swift b/Session/Calls/Views & Modals/IncomingCallBanner.swift index cc71c7b5e..ca63e881d 100644 --- a/Session/Calls/Views & Modals/IncomingCallBanner.swift +++ b/Session/Calls/Views & Modals/IncomingCallBanner.swift @@ -1,15 +1,141 @@ -// Copyright © 2021 Rangeproof Pty Ltd. All rights reserved. - import UIKit +import WebRTC +import SessionMessagingKit -class IncomingCallBanner: UIView { - - /* - // Only override draw() if you perform custom drawing. - // An empty implementation adversely affects performance during animation. - override func draw(_ rect: CGRect) { - // Drawing code +final class IncomingCallBanner: UIView { + let sessionID: String + let uuid: String + let sdp: RTCSessionDescription + + // MARK: UI Components + private lazy var profilePictureView: ProfilePictureView = { + let result = ProfilePictureView() + let size = CGFloat(60) + result.size = size + result.set(.width, to: size) + result.set(.height, to: size) + return result + }() + + private lazy var displayNameLabel: UILabel = { + let result = UILabel() + result.textColor = UIColor.white + result.font = .boldSystemFont(ofSize: Values.largeFontSize) + result.lineBreakMode = .byTruncatingTail + result.textAlignment = .center + return result + }() + + private lazy var answerButton: UIButton = { + let result = UIButton(type: .custom) + let image = UIImage(named: "AnswerCall")!.withTint(.white)?.resizedImage(to: CGSize(width: 24.8, height: 24.8)) + result.setImage(image, for: UIControl.State.normal) + result.set(.width, to: 48) + result.set(.height, to: 48) + result.backgroundColor = Colors.accent + result.layer.cornerRadius = 24 + result.addTarget(self, action: #selector(answerCall), for: UIControl.Event.touchUpInside) + return result + }() + + private lazy var hangUpButton: UIButton = { + let result = UIButton(type: .custom) + let image = UIImage(named: "EndCall")!.withTint(.white)?.resizedImage(to: CGSize(width: 29.6, height: 11.2)) + result.setImage(image, for: UIControl.State.normal) + result.set(.width, to: 48) + result.set(.height, to: 48) + result.backgroundColor = Colors.destructive + result.layer.cornerRadius = 24 + result.addTarget(self, action: #selector(endCall), for: UIControl.Event.touchUpInside) + return result + }() + + // MARK: Initialization + public static var current: IncomingCallBanner? + + init(for sessionID: String, uuid: String, sdp: RTCSessionDescription) { + self.uuid = uuid + self.sessionID = sessionID + self.sdp = sdp + super.init(frame: CGRect.zero) + setUpViewHierarchy() + if let incomingCallBanner = IncomingCallBanner.current { + incomingCallBanner.dismiss() + } + IncomingCallBanner.current = self + } + + override init(frame: CGRect) { + preconditionFailure("Use init(message:) instead.") + } + + required init?(coder: NSCoder) { + preconditionFailure("Use init(coder:) instead.") + } + + private func setUpViewHierarchy() { + self.backgroundColor = UIColor(hex: 0x000000).withAlphaComponent(0.9) + self.layer.cornerRadius = Values.veryLargeSpacing + self.layer.masksToBounds = true + self.set(.height, to: 100) + profilePictureView.publicKey = self.sessionID + profilePictureView.update() + displayNameLabel.text = Storage.shared.getContact(with: sessionID)?.name + let stackView = UIStackView(arrangedSubviews: [profilePictureView, displayNameLabel, UIView.hStretchingSpacer(), hangUpButton, answerButton]) + stackView.axis = .horizontal + stackView.alignment = .center + stackView.spacing = Values.largeSpacing + self.addSubview(stackView) + stackView.center(.vertical, in: self) + stackView.autoPinWidthToSuperview(withMargin: Values.mediumSpacing) + } + + @objc private func answerCall() { + showCallVC(answer: true) + } + + @objc private func endCall() { + Storage.write { transaction in + WebRTCSession.current?.endCall(with: self.sessionID, using: transaction) + } + dismiss() + } + + public func showCallVC(answer: Bool) { + dismiss() + guard let presentingVC = CurrentAppContext().frontmostViewController() else { preconditionFailure() } // TODO: Handle more gracefully + let callVC = CallVC(for: sessionID, uuid: uuid, mode: .answer(sdp: sdp)) + callVC.shouldAnswer = answer + callVC.modalPresentationStyle = .overFullScreen + callVC.modalTransitionStyle = .crossDissolve + if let conversationVC = presentingVC as? ConversationVC { + callVC.conversationVC = conversationVC + conversationVC.inputAccessoryView?.isHidden = true + conversationVC.inputAccessoryView?.alpha = 0 + } + presentingVC.present(callVC, animated: true, completion: nil) + } + + public func show() { + self.alpha = 0.0 + let window = CurrentAppContext().mainWindow! + window.addSubview(self) + let topMargin = UIApplication.shared.keyWindow!.safeAreaInsets.top - Values.smallSpacing + self.autoPinWidthToSuperview(withMargin: Values.smallSpacing) + self.autoPinEdge(toSuperviewEdge: .top, withInset: topMargin) + UIView.animate(withDuration: 0.5, delay: 0, options: [], animations: { + self.alpha = 1.0 + }, completion: nil) + AudioServicesPlaySystemSound(kSystemSoundID_Vibrate) + } + + public func dismiss() { + UIView.animate(withDuration: 0.5, delay: 0, options: [], animations: { + self.alpha = 0.0 + }, completion: { _ in + IncomingCallBanner.current = nil + self.removeFromSuperview() + }) } - */ } diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 3ebabf9c8..faad7c3be 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -29,7 +29,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc // MARK: Call @objc func startCall(_ sender: Any) { guard let contactSessionID = (thread as? TSContactThread)?.contactSessionID() else { return } - let callVC = CallVC(for: contactSessionID, mode: .offer) + let callVC = CallVC(for: contactSessionID, uuid: UUID().uuidString, mode: .offer) callVC.conversationVC = self callVC.modalPresentationStyle = .overFullScreen callVC.modalTransitionStyle = .crossDissolve @@ -39,15 +39,10 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc } internal func showCallVCIfNeeded() { - guard hasIncomingCall, let contactSessionID = (thread as? TSContactThread)?.contactSessionID() else { return } - hasIncomingCall = false - let callVC = CallVC(for: contactSessionID, mode: .offer) // TODO: change to answer - callVC.conversationVC = self - callVC.modalPresentationStyle = .overFullScreen - callVC.modalTransitionStyle = .crossDissolve - self.inputAccessoryView?.isHidden = true - self.inputAccessoryView?.alpha = 0 - present(callVC, animated: true, completion: nil) + guard let contactSessionID = (thread as? TSContactThread)?.contactSessionID(), + let incomingCallBanner = IncomingCallBanner.current, incomingCallBanner.sessionID == contactSessionID + else { return } + incomingCallBanner.showCallVC(answer: false) } // MARK: Blocking diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index 9181aeb90..fcaf3f5c6 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -10,7 +10,6 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat let focusedMessageID: String? // This isn't actually used ATM var unreadViewItems: [ConversationViewItem] = [] var scrollButtonConstraint: NSLayoutConstraint? - var hasIncomingCall = false // Search var isShowingSearchUI = false var lastSearchedText: String? diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index fb15fc6c9..764e36fdd 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -1,5 +1,7 @@ import PromiseKit import WebRTC +import SessionUIKit +import UIKit extension AppDelegate { @@ -11,7 +13,7 @@ extension AppDelegate { let sdp = RTCSessionDescription(type: .offer, sdp: message.sdps![0]) 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() == message.sender! { - let callVC = CallVC(for: message.sender!, mode: .answer(sdp: sdp)) + let callVC = CallVC(for: message.sender!, uuid: message.uuid!, mode: .answer(sdp: sdp)) callVC.modalPresentationStyle = .overFullScreen callVC.modalTransitionStyle = .crossDissolve callVC.conversationVC = conversationVC @@ -19,19 +21,24 @@ extension AppDelegate { conversationVC.inputAccessoryView?.alpha = 0 presentingVC.present(callVC, animated: true, completion: nil) } else { + let incomingCallBanner = IncomingCallBanner(for: message.sender!, uuid: message.uuid!, sdp: sdp) + incomingCallBanner.show() } } } // Answer messages MessageReceiver.handleAnswerCallMessage = { message in DispatchQueue.main.async { - guard let callVC = CurrentAppContext().frontmostViewController() as? CallVC else { return } - callVC.handleAnswerMessage(message) + if let incomingCallBanner = IncomingCallBanner.current, incomingCallBanner.uuid == message.uuid! { incomingCallBanner.dismiss() } + if let callVC = CurrentAppContext().frontmostViewController() as? CallVC { callVC.handleAnswerMessage(message) } + WebRTCSession.current?.dropConnection() + WebRTCSession.current = nil } } // End call messages MessageReceiver.handleEndCallMessage = { message in DispatchQueue.main.async { + if let currentBanner = IncomingCallBanner.current { currentBanner.dismiss() } guard let callVC = CurrentAppContext().frontmostViewController() as? CallVC else { return } callVC.handleEndCallMessage(message) } diff --git a/SessionMessagingKit/Calls/WebRTCSession.swift b/SessionMessagingKit/Calls/WebRTCSession.swift index e0e00ab30..d01e8a47c 100644 --- a/SessionMessagingKit/Calls/WebRTCSession.swift +++ b/SessionMessagingKit/Calls/WebRTCSession.swift @@ -11,8 +11,8 @@ public protocol WebRTCSessionDelegate : AnyObject { /// See https://webrtc.org/getting-started/overview for more information. public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate { public weak var delegate: WebRTCSessionDelegate? + public let uuid: String private let contactSessionID: String - private let uuid: String private var queuedICECandidates: [RTCIceCandidate] = [] private var iceCandidateSendTimer: Timer? @@ -138,6 +138,7 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate { } DispatchQueue.main.async { let message = CallMessage() + message.sentTimestamp = NSDate.millisecondTimestamp() message.uuid = self.uuid message.kind = .offer message.sdps = [ sdp.sdp ] @@ -171,6 +172,7 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate { } DispatchQueue.main.async { let message = CallMessage() + message.uuid = self.uuid message.kind = .answer message.sdps = [ sdp.sdp ] MessageSender.sendNonDurably(message, in: thread, using: transaction).done2 { @@ -203,6 +205,7 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate { let sdps = candidates.map { $0.sdp } let sdpMLineIndexes = candidates.map { UInt32($0.sdpMLineIndex) } let sdpMids = candidates.map { $0.sdpMid! } + message.uuid = self.uuid message.kind = .iceCandidates(sdpMLineIndexes: sdpMLineIndexes, sdpMids: sdpMids) message.sdps = sdps self.queuedICECandidates.removeAll() @@ -213,6 +216,7 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate { public func endCall(with sessionID: String, using transaction: YapDatabaseReadWriteTransaction) { guard let thread = TSContactThread.fetch(for: sessionID, using: transaction) else { return } let message = CallMessage() + message.uuid = self.uuid message.kind = .endCall print("[Calls] Sending end call message.") MessageSender.sendNonDurably(message, in: thread, using: transaction).retainUntilComplete() @@ -294,7 +298,7 @@ extension WebRTCSession { sendJSON(["video": false]) } - public func turnOnVideo() { + public func turnOnVideo() { localVideoTrack.isEnabled = true sendJSON(["video": true]) } diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift index bf5263d50..6dc8ff062 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift @@ -277,7 +277,10 @@ extension MessageReceiver { switch message.kind! { case .preOffer: print("[Calls] Received pre-offer message.") - // TODO: Notify incoming call + let currentSession = getWebRTCSession() + if currentSession.uuid != message.uuid! { + // TODO: Call in progress, put the new call on hold/reject + } case .offer: print("[Calls] Received offer message.") let storage = SNMessagingKitConfiguration.shared.storage