From a1f8e16eb3e1e948d3b0d0c9c294e9fcb4f42672 Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Mon, 11 Oct 2021 13:14:39 +1100 Subject: [PATCH] WIP: add mini call floating view --- Session.xcodeproj/project.pbxproj | 4 + Session/Calls/CallVC.swift | 11 +- .../Calls/Views & Modals/MiniCallView.swift | 138 ++++++++++++++++++ Session/Meta/AppDelegate.swift | 1 + SessionMessagingKit/Calls/WebRTCSession.swift | 4 +- 5 files changed, 154 insertions(+), 4 deletions(-) create mode 100644 Session/Calls/Views & Modals/MiniCallView.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index b4e6571f4..b314e561d 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -141,6 +141,7 @@ 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 */; }; + 7B7CB190270FB2150079FF93 /* MiniCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18F270FB2150079FF93 /* MiniCallView.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 */; }; @@ -1118,6 +1119,7 @@ 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 = ""; }; + 7B7CB18F270FB2150079FF93 /* MiniCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MiniCallView.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 = ""; }; @@ -2046,6 +2048,7 @@ isa = PBXGroup; children = ( 7B7CB18D270D066F0079FF93 /* IncomingCallBanner.swift */, + 7B7CB18F270FB2150079FF93 /* MiniCallView.swift */, ); path = "Views & Modals"; sourceTree = ""; @@ -4902,6 +4905,7 @@ B85357C323A1BD1200AAF6CD /* SeedVC.swift in Sources */, 45B5360E206DD8BB00D61655 /* UIResponder+OWS.swift in Sources */, B8D84ECF25E3108A005A043E /* ExpandingAttachmentsButton.swift in Sources */, + 7B7CB190270FB2150079FF93 /* MiniCallView.swift in Sources */, B875885A264503A6000E60D0 /* JoinOpenGroupModal.swift in Sources */, B8CCF6432397711F0091D419 /* SettingsVC.swift in Sources */, C354E75A23FE2A7600CE22E3 /* BaseVC.swift in Sources */, diff --git a/Session/Calls/CallVC.swift b/Session/Calls/CallVC.swift index be1cb7252..569b9cc1e 100644 --- a/Session/Calls/CallVC.swift +++ b/Session/Calls/CallVC.swift @@ -191,7 +191,9 @@ final class CallVC : UIViewController, WebRTCSessionDelegate { callInfoLabel.text = "Ringing..." Storage.write { transaction in self.webRTCSession.sendPreOffer(to: self.sessionID, using: transaction).done { - self.webRTCSession.sendOffer(to: self.sessionID, using: transaction).retainUntilComplete() + self.webRTCSession.sendOffer(to: self.sessionID, using: transaction).done { + self.minimizeButton.isHidden = false + }.retainUntilComplete() }.retainUntilComplete() } answerButton.isHidden = true @@ -203,7 +205,7 @@ final class CallVC : UIViewController, WebRTCSessionDelegate { // Background let background = getBackgroudView() view.addSubview(background) - background.autoPinEdgesToSuperviewEdges() + background.pin(to: view) // Call info label view.addSubview(callInfoLabel) callInfoLabel.translatesAutoresizingMaskIntoConstraints = false @@ -332,7 +334,10 @@ final class CallVC : UIViewController, WebRTCSessionDelegate { } @objc private func minimize() { - + let miniCallView = MiniCallView(from: self) + miniCallView.show() + self.conversationVC?.showInputAccessoryView() + presentingViewController?.dismiss(animated: true, completion: nil) } @objc private func operateCamera() { diff --git a/Session/Calls/Views & Modals/MiniCallView.swift b/Session/Calls/Views & Modals/MiniCallView.swift new file mode 100644 index 000000000..41b261c42 --- /dev/null +++ b/Session/Calls/Views & Modals/MiniCallView.swift @@ -0,0 +1,138 @@ +import UIKit +import WebRTC + +final class MiniCallView: UIView { + var callVC: CallVC + + private lazy var remoteVideoView: RTCMTLVideoView = { + let result = RTCMTLVideoView() + result.contentMode = .scaleAspectFill + return result + }() + + // MARK: Initialization + public static var current: MiniCallView? + + init(from callVC: CallVC) { + self.callVC = callVC + super.init(frame: CGRect.zero) + self.backgroundColor = .black + setUpViewHierarchy() + setUpGestureRecognizers() + MiniCallView.current = self + } + + override init(frame: CGRect) { + preconditionFailure("Use init(message:) instead.") + } + + required init?(coder: NSCoder) { + preconditionFailure("Use init(coder:) instead.") + } + + private func setUpViewHierarchy() { + self.set(.width, to: 80) + self.set(.height, to: 173) + // Background + let background = getBackgroudView() + self.addSubview(background) + background.pin(to: self) + // Remote video view + callVC.webRTCSession.attachRemoteRenderer(remoteVideoView) + self.addSubview(remoteVideoView) + remoteVideoView.translatesAutoresizingMaskIntoConstraints = false + remoteVideoView.pin(to: self) + } + + private func getBackgroudView() -> UIView { + let background = UIView() + let imageView = UIImageView() + imageView.layer.cornerRadius = 32 + imageView.layer.masksToBounds = true + imageView.contentMode = .scaleAspectFill + if let profilePicture = OWSProfileManager.shared().profileAvatar(forRecipientId: callVC.sessionID) { + imageView.image = profilePicture + } else { + let displayName = Storage.shared.getContact(with: callVC.sessionID)?.name ?? callVC.sessionID + imageView.image = Identicon.generatePlaceholderIcon(seed: callVC.sessionID, text: displayName, size: 64) + } + background.addSubview(imageView) + imageView.set(.width, to: 64) + imageView.set(.height, to: 64) + imageView.center(in: background) + let blurView = UIView() + blurView.alpha = 0.5 + blurView.backgroundColor = .black + background.addSubview(blurView) + blurView.autoPinEdgesToSuperviewEdges() + return background + } + + private func setUpGestureRecognizers() { + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap)) + tapGestureRecognizer.numberOfTapsRequired = 1 + addGestureRecognizer(tapGestureRecognizer) + let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan)) + addGestureRecognizer(panGestureRecognizer) + } + + // MARK: Interaction + @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { + dismiss() + guard let presentingVC = CurrentAppContext().frontmostViewController() else { preconditionFailure() } // TODO: Handle more gracefully + presentingVC.present(callVC, animated: true, completion: nil) + } + + @objc private func handlePan(_ gesture: UIPanGestureRecognizer) { + let location = gesture.location(in: self.superview!) + if let draggedView = gesture.view { + draggedView.center = location + if gesture.state == .ended { + let sideMargin = 40 + Values.verySmallSpacing + if draggedView.frame.midX >= self.superview!.layer.frame.width / 2 { + UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseIn, animations: { + draggedView.center.x = self.superview!.layer.frame.width - sideMargin + }, completion: nil) + }else{ + UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseIn, animations: { + draggedView.center.x = sideMargin + }, completion: nil) + } + let topMargin = UIApplication.shared.keyWindow!.safeAreaInsets.top + Values.veryLargeSpacing + if draggedView.frame.minY <= topMargin { + UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseIn, animations: { + draggedView.center.y = topMargin + draggedView.frame.size.height / 2 + }, completion: nil) + } + let bottomMargin = UIApplication.shared.keyWindow!.safeAreaInsets.bottom + if draggedView.frame.maxY >= self.superview!.layer.frame.height { + UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseIn, animations: { + draggedView.center.y = self.layer.frame.height - draggedView.frame.size.height / 2 - bottomMargin + }, completion: nil) + } + } + } + } + + public func show() { + self.alpha = 0.0 + let window = CurrentAppContext().mainWindow! + window.addSubview(self) + self.autoPinEdge(toSuperviewEdge: .right, withInset: Values.smallSpacing) + let topMargin = UIApplication.shared.keyWindow!.safeAreaInsets.top + Values.veryLargeSpacing + self.autoPinEdge(toSuperviewEdge: .top, withInset: topMargin) + UIView.animate(withDuration: 0.5, delay: 0, options: [], animations: { + self.alpha = 1.0 + }, completion: nil) + } + + public func dismiss() { + UIView.animate(withDuration: 0.5, delay: 0, options: [], animations: { + self.alpha = 0.0 + }, completion: { _ in + MiniCallView.current = nil + self.removeFromSuperview() + }) + } + +} diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index 3d1b56ba5..b744061c5 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -38,6 +38,7 @@ extension AppDelegate { DispatchQueue.main.async { if let currentBanner = IncomingCallBanner.current { currentBanner.dismiss() } if let callVC = CurrentAppContext().frontmostViewController() as? CallVC { callVC.handleEndCallMessage(message) } + if let miniCallView = MiniCallView.current { miniCallView.dismiss() } WebRTCSession.current?.dropConnection() WebRTCSession.current = nil } diff --git a/SessionMessagingKit/Calls/WebRTCSession.swift b/SessionMessagingKit/Calls/WebRTCSession.swift index d01e8a47c..a9a4b00a9 100644 --- a/SessionMessagingKit/Calls/WebRTCSession.swift +++ b/SessionMessagingKit/Calls/WebRTCSession.swift @@ -95,7 +95,6 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate { self.uuid = uuid super.init() let mediaStreamTrackIDS = ["ARDAMS"] - createDataChannel() peerConnection.add(audioTrack, streamIds: mediaStreamTrackIDS) peerConnection.add(localVideoTrack, streamIds: mediaStreamTrackIDS) // Configure audio session @@ -105,6 +104,7 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate { // MARK: Signaling public func sendPreOffer(to sessionID: String, using transaction: YapDatabaseReadWriteTransaction) -> Promise { print("[Calls] Sending pre-offer message.") + createDataChannel() guard let thread = TSContactThread.fetch(for: sessionID, using: transaction) else { return Promise(error: Error.noThread) } let (promise, seal) = Promise.pending() DispatchQueue.main.async { @@ -267,6 +267,8 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate { public func peerConnection(_ peerConnection: RTCPeerConnection, didOpen dataChannel: RTCDataChannel) { print("[Calls] Data channel opened.") + self.dataChannel = dataChannel + self.dataChannel?.delegate = self } }