WIP: incoming call banner

pull/560/head
ryanzhao 4 years ago
parent 98268ebf73
commit fbe5b12c9d

@ -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 = "<group>"; };
7B7CB188270430D20079FF93 /* CallMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallMessageView.swift; sourceTree = "<group>"; };
7B7CB18A270591630079FF93 /* ShareLogsModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareLogsModal.swift; sourceTree = "<group>"; };
7B7CB18D270D066F0079FF93 /* IncomingCallBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallBanner.swift; sourceTree = "<group>"; };
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 = "<group>"; };
7BC01A3F241F40AB00BC7C55 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -2040,6 +2042,14 @@
path = Utilities;
sourceTree = "<group>";
};
7B7CB18C270D06350079FF93 /* Views & Modals */ = {
isa = PBXGroup;
children = (
7B7CB18D270D066F0079FF93 /* IncomingCallBanner.swift */,
);
path = "Views & Modals";
sourceTree = "<group>";
};
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 = "<group>";
@ -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 */,

@ -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
}

@ -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()
})
}
*/
}

@ -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

@ -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?

@ -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)
}

@ -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])
}

@ -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

Loading…
Cancel
Save