mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
Add “interstitial call view” that is shown during lengthy “webrtc supported” check.
// FREEBIE
This commit is contained in:
parent
ea57b48490
commit
c43063e1d6
6 changed files with 316 additions and 21 deletions
|
@ -9,6 +9,7 @@
|
|||
/* Begin PBXBuildFile section */
|
||||
341BB7491DB727EE001E2975 /* JSQMediaItem+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = 341BB7481DB727EE001E2975 /* JSQMediaItem+OWS.m */; };
|
||||
34535D821E256BE9008A4747 /* UIView+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = 34535D811E256BE9008A4747 /* UIView+OWS.m */; };
|
||||
348F3A4F1E4A533900750D44 /* CallInterstitialViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 348F3A4E1E4A533900750D44 /* CallInterstitialViewController.swift */; };
|
||||
34FD93701E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 34FD936F1E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.m */; };
|
||||
450873C31D9D5149006B54F2 /* OWSExpirationTimerView.m in Sources */ = {isa = PBXBuildFile; fileRef = 450873C21D9D5149006B54F2 /* OWSExpirationTimerView.m */; };
|
||||
450873C41D9D5149006B54F2 /* OWSExpirationTimerView.m in Sources */ = {isa = PBXBuildFile; fileRef = 450873C21D9D5149006B54F2 /* OWSExpirationTimerView.m */; };
|
||||
|
@ -602,6 +603,7 @@
|
|||
341BB7481DB727EE001E2975 /* JSQMediaItem+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "JSQMediaItem+OWS.m"; sourceTree = "<group>"; };
|
||||
34535D801E256BE9008A4747 /* UIView+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+OWS.h"; sourceTree = "<group>"; };
|
||||
34535D811E256BE9008A4747 /* UIView+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+OWS.m"; sourceTree = "<group>"; };
|
||||
348F3A4E1E4A533900750D44 /* CallInterstitialViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallInterstitialViewController.swift; sourceTree = "<group>"; };
|
||||
34FD936E1E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWSAnyTouchGestureRecognizer.h; path = views/OWSAnyTouchGestureRecognizer.h; sourceTree = "<group>"; };
|
||||
34FD936F1E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWSAnyTouchGestureRecognizer.m; path = views/OWSAnyTouchGestureRecognizer.m; sourceTree = "<group>"; };
|
||||
450873C11D9D5149006B54F2 /* OWSExpirationTimerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSExpirationTimerView.h; sourceTree = "<group>"; };
|
||||
|
@ -2607,25 +2609,26 @@
|
|||
FC3196321A08142D0094C78E /* Signals */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
348F3A4E1E4A533900750D44 /* CallInterstitialViewController.swift */,
|
||||
4509E79B1DD6545B0025A59F /* CallViewController.swift */,
|
||||
FC3196281A067D8F0094C78E /* MessageComposeTableViewController.h */,
|
||||
FC3196291A067D8F0094C78E /* MessageComposeTableViewController.m */,
|
||||
FCAC963A19FEF9280046DFC5 /* SignalsViewController.h */,
|
||||
FCAC963B19FEF9280046DFC5 /* SignalsViewController.m */,
|
||||
FCAC964F19FF0A6E0046DFC5 /* MessagesViewController.h */,
|
||||
FCAC965019FF0A6E0046DFC5 /* MessagesViewController.m */,
|
||||
FC31962B1A06A2190094C78E /* FingerprintViewController.h */,
|
||||
FC31962C1A06A2190094C78E /* FingerprintViewController.m */,
|
||||
FCB11D911A12A4AA002F93FB /* FullImageViewController.h */,
|
||||
FCB11D921A12A4AA002F93FB /* FullImageViewController.m */,
|
||||
A5D0699A1A50E9CB004CB540 /* ShowGroupMembersViewController.h */,
|
||||
A5D069991A50E9CB004CB540 /* ShowGroupMembersViewController.m */,
|
||||
FC3196281A067D8F0094C78E /* MessageComposeTableViewController.h */,
|
||||
FC3196291A067D8F0094C78E /* MessageComposeTableViewController.m */,
|
||||
FCAC964F19FF0A6E0046DFC5 /* MessagesViewController.h */,
|
||||
FCAC965019FF0A6E0046DFC5 /* MessagesViewController.m */,
|
||||
FCFD256D1A151BCB00F4C644 /* NewGroupViewController.h */,
|
||||
FCFD256E1A151BCB00F4C644 /* NewGroupViewController.m */,
|
||||
FC4FA0241A1B9DC600DA100A /* SignalsNavigationController.h */,
|
||||
FC4FA0251A1B9DC600DA100A /* SignalsNavigationController.m */,
|
||||
452E3C8C1D935C77002A45B0 /* OWSConversationSettingsTableViewController.h */,
|
||||
452E3C8D1D935C77002A45B0 /* OWSConversationSettingsTableViewController.m */,
|
||||
A5D0699A1A50E9CB004CB540 /* ShowGroupMembersViewController.h */,
|
||||
A5D069991A50E9CB004CB540 /* ShowGroupMembersViewController.m */,
|
||||
FC4FA0241A1B9DC600DA100A /* SignalsNavigationController.h */,
|
||||
FC4FA0251A1B9DC600DA100A /* SignalsNavigationController.m */,
|
||||
FCAC963A19FEF9280046DFC5 /* SignalsViewController.h */,
|
||||
FCAC963B19FEF9280046DFC5 /* SignalsViewController.m */,
|
||||
);
|
||||
name = Signals;
|
||||
sourceTree = "<group>";
|
||||
|
@ -3081,6 +3084,7 @@
|
|||
45387B041E36D650005D00B3 /* OWS102MoveLoggingPreferenceToUserDefaults.m in Sources */,
|
||||
E197B61818BBEC1A00F073E5 /* RemoteIOAudio.m in Sources */,
|
||||
B67ADDC41989FF8700E1A773 /* RPServerRequestsManager.m in Sources */,
|
||||
348F3A4F1E4A533900750D44 /* CallInterstitialViewController.swift in Sources */,
|
||||
EF764C351DB67CC5000D9A87 /* UIViewController+CameraPermissions.m in Sources */,
|
||||
76EB059418170B33006006FC /* HttpManager.m in Sources */,
|
||||
45CD81EF1DC030E7004C9430 /* AccountManager.swift in Sources */,
|
||||
|
|
|
@ -250,6 +250,18 @@ protocol CallServiceObserver: class {
|
|||
return "CallServiceActiveCallNotification"
|
||||
}
|
||||
|
||||
class func presentCallInterstitialNotificationName() -> String {
|
||||
return "PresentCallInterstitialNotification"
|
||||
}
|
||||
|
||||
class func dismissCallInterstitialNotificationName() -> String {
|
||||
return "DismissCallInterstitialNotification"
|
||||
}
|
||||
|
||||
class func callWasCancelledByInterstitialNotificationName() -> String {
|
||||
return "CallWasCancelledByInterstitialNotification"
|
||||
}
|
||||
|
||||
// MARK: - Service Actions
|
||||
|
||||
/**
|
||||
|
|
|
@ -14,11 +14,37 @@ import Foundation
|
|||
let contactsManager: OWSContactsManager
|
||||
let contactsUpdater: ContactsUpdater
|
||||
|
||||
var cancelledCallTokens: [String] = []
|
||||
|
||||
init(redphoneManager: PhoneManager, contactsManager: OWSContactsManager, contactsUpdater: ContactsUpdater) {
|
||||
self.redphoneManager = redphoneManager
|
||||
|
||||
self.contactsManager = contactsManager
|
||||
self.contactsUpdater = contactsUpdater
|
||||
|
||||
super.init()
|
||||
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector:#selector(callWasCancelledByInterstitial),
|
||||
name:Notification.Name(rawValue: CallService.callWasCancelledByInterstitialNotificationName()),
|
||||
object:nil)
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
func callWasCancelledByInterstitial(notification: NSNotification) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
let callToken = notification.object as! String
|
||||
cancelCallToken(callToken)
|
||||
}
|
||||
|
||||
func cancelCallToken(_ callToken: String) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
cancelledCallTokens.append(callToken)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -44,6 +70,10 @@ import Foundation
|
|||
return self.initiateRedphoneCall(recipientId: recipientId)
|
||||
}
|
||||
|
||||
// A temporary unique id used to identify this call during the
|
||||
let callToken = NSUUID().uuidString
|
||||
presentCallInterstitial(callToken)
|
||||
|
||||
// Since users can toggle this setting, which is only communicated during contact sync, it's easy to imagine the
|
||||
// preference getting stale. Especially as users are toggling the feature to test calls. So here, we opt for a
|
||||
// blocking network request *every* time we place a call to make sure we've got up to date preferences.
|
||||
|
@ -52,6 +82,10 @@ import Foundation
|
|||
// SignalRecipient *recipient = [SignalRecipient recipientWithTextSecureIdentifier:self.thread.contactIdentifier];
|
||||
self.contactsUpdater.lookupIdentifier(recipientId,
|
||||
success: { recipient in
|
||||
guard !self.cancelledCallTokens.contains(callToken) else {
|
||||
Logger.error("\(self.TAG) OutboundCallInitiator aborting due to cancelled call.")
|
||||
return
|
||||
}
|
||||
|
||||
guard !Environment.getCurrent().phoneManager.hasOngoingRedphoneCall() else {
|
||||
Logger.error("\(self.TAG) OutboundCallInitiator aborting due to ongoing RedPhone call.")
|
||||
|
@ -74,6 +108,9 @@ import Foundation
|
|||
failure: { error in
|
||||
Logger.warn("\(self.TAG) looking up recipientId: \(recipientId) failed with error \(error)")
|
||||
|
||||
self.cancelCallToken(callToken)
|
||||
self.dismissCallInterstitial(callToken)
|
||||
|
||||
let alertTitle = NSLocalizedString("UNABLE_TO_PLACE_CALL", comment:"Alert Title")
|
||||
let alertController = UIAlertController(title: alertTitle, message: error.localizedDescription, preferredStyle: .alert)
|
||||
|
||||
|
@ -113,4 +150,17 @@ import Foundation
|
|||
return true
|
||||
}
|
||||
|
||||
private func presentCallInterstitial(_ callToken: String) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
let notificationName = CallService.presentCallInterstitialNotificationName()
|
||||
NotificationCenter.default.post(name: NSNotification.Name(rawValue: notificationName), object: callToken)
|
||||
}
|
||||
|
||||
private func dismissCallInterstitial(_ callToken: String) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
let notificationName = CallService.dismissCallInterstitialNotificationName()
|
||||
NotificationCenter.default.post(name: NSNotification.Name(rawValue: notificationName), object: callToken)
|
||||
}
|
||||
}
|
||||
|
|
166
Signal/src/view controllers/CallInterstitialViewController.swift
Normal file
166
Signal/src/view controllers/CallInterstitialViewController.swift
Normal file
|
@ -0,0 +1,166 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@objc(OWSCallInterstitialViewController)
|
||||
class CallInterstitialViewController: UIViewController {
|
||||
|
||||
let TAG = "[CallInterstitialViewController]"
|
||||
|
||||
var wasCallCancelled = false
|
||||
var callToken: String?
|
||||
|
||||
// MARK: Views
|
||||
|
||||
var hasConstraints = false
|
||||
var blurView: UIVisualEffectView!
|
||||
var contentView: UIView!
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
assert(false)
|
||||
super.init(coder: aDecoder)
|
||||
}
|
||||
|
||||
required init() {
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
observeNotifications()
|
||||
}
|
||||
|
||||
func observeNotifications() {
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector:#selector(willResignActive),
|
||||
name:NSNotification.Name.UIApplicationWillResignActive,
|
||||
object:nil)
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
func willResignActive() {
|
||||
cancelCall()
|
||||
}
|
||||
|
||||
// MARK: View Lifecycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
createViews()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
blurView.layer.opacity = 0
|
||||
contentView.layer.opacity = 0
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
UIView.animate(withDuration: 0.3,
|
||||
delay: 1.0,
|
||||
options: UIViewAnimationOptions.curveLinear,
|
||||
animations: {
|
||||
self.blurView.layer.opacity = 1
|
||||
self.contentView.layer.opacity = 1
|
||||
},
|
||||
completion: nil)
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
blurView.layer.removeAllAnimations()
|
||||
contentView.layer.removeAllAnimations()
|
||||
}
|
||||
|
||||
// MARK: - Create Views
|
||||
|
||||
func createViews() {
|
||||
assert(self.view != nil)
|
||||
|
||||
// Dark blurred background.
|
||||
let blurEffect = UIBlurEffect(style: .dark)
|
||||
blurView = UIVisualEffectView(effect: blurEffect)
|
||||
blurView.isUserInteractionEnabled = false
|
||||
self.view.addSubview(blurView)
|
||||
|
||||
contentView = UIView()
|
||||
self.view.addSubview(contentView)
|
||||
|
||||
let dialingLabel = UILabel()
|
||||
dialingLabel.text = NSLocalizedString("CALL_INTERSTITIAL_CALLING_LABEL", comment: "Title for call interstitial view")
|
||||
dialingLabel.textColor = UIColor.white
|
||||
dialingLabel.font = UIFont.ows_lightFont(withSize:ScaleFromIPhone5To7Plus(32, 40))
|
||||
dialingLabel.textAlignment = .center
|
||||
contentView.addSubview(dialingLabel)
|
||||
|
||||
let cancelCallButton = UIButton()
|
||||
cancelCallButton.setTitle(NSLocalizedString("CALL_INTERSTITIAL_CANCEL_BUTTON", comment: "Label for cancel button on call interstitial view"),
|
||||
for:.normal)
|
||||
cancelCallButton.setTitleColor(UIColor.white, for:.normal)
|
||||
cancelCallButton.titleLabel?.font = UIFont.ows_lightFont(withSize:ScaleFromIPhone5To7Plus(26, 32))
|
||||
let buttonInset = ScaleFromIPhone5To7Plus(7, 9)
|
||||
cancelCallButton.titleEdgeInsets = UIEdgeInsets(top: buttonInset,
|
||||
left: buttonInset,
|
||||
bottom: buttonInset,
|
||||
right: buttonInset)
|
||||
cancelCallButton.addTarget(self, action:#selector(cancelCallButtonPressed), for:.touchUpInside)
|
||||
contentView.addSubview(cancelCallButton)
|
||||
|
||||
dialingLabel.autoPinWidthToSuperview()
|
||||
dialingLabel.autoVCenterInSuperview()
|
||||
|
||||
cancelCallButton.autoSetDimension(.height, toSize:ScaleFromIPhone5To7Plus(50, 60))
|
||||
cancelCallButton.autoPinWidthToSuperview()
|
||||
cancelCallButton.autoPinEdge(toSuperviewEdge:.bottom, withInset:ScaleFromIPhone5To7Plus(23, 41))
|
||||
}
|
||||
|
||||
func cancelCallButtonPressed(sender button: UIButton) {
|
||||
cancelCall()
|
||||
}
|
||||
|
||||
// MARK: - Layout
|
||||
|
||||
override func updateViewConstraints() {
|
||||
if !hasConstraints {
|
||||
// We only want to create our constraints once.
|
||||
//
|
||||
// Note that constraints are also created elsewhere.
|
||||
// This only creates the constraints for the top-level contents of the view.
|
||||
hasConstraints = true
|
||||
|
||||
// Force creation of the view.
|
||||
let view = self.view
|
||||
assert(view != nil)
|
||||
|
||||
// Dark blurred background.
|
||||
blurView.autoPinEdgesToSuperviewEdges()
|
||||
|
||||
contentView.autoPinEdgesToSuperviewEdges()
|
||||
}
|
||||
|
||||
super.updateViewConstraints()
|
||||
}
|
||||
|
||||
// MARK: - Methods
|
||||
|
||||
func cancelCall() {
|
||||
guard !wasCallCancelled else {
|
||||
return
|
||||
}
|
||||
wasCallCancelled = true
|
||||
|
||||
assert(callToken != nil)
|
||||
let notificationName = CallService.callWasCancelledByInterstitialNotificationName()
|
||||
NotificationCenter.default.post(name: NSNotification.Name(rawValue: notificationName), object: callToken)
|
||||
|
||||
self.dismiss(animated: false)
|
||||
}
|
||||
}
|
|
@ -125,11 +125,19 @@ NSString *const SignalsViewControllerSegueShowIncomingCall = @"ShowIncomingCallS
|
|||
(self.traitCollection.forceTouchCapability == UIForceTouchCapabilityAvailable)) {
|
||||
[self registerForPreviewingWithDelegate:self sourceView:self.tableView];
|
||||
}
|
||||
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(handleActiveCallNotification:)
|
||||
name:[CallService callServiceActiveCallNotificationName]
|
||||
object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(handlePresentCallInterstitialNotification:)
|
||||
name:[CallService presentCallInterstitialNotificationName]
|
||||
object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(handleDismissCallInterstitialNotification:)
|
||||
name:[CallService dismissCallInterstitialNotificationName]
|
||||
object:nil];
|
||||
}
|
||||
|
||||
- (UIViewController *)previewingContext:(id<UIViewControllerPreviewing>)previewingContext
|
||||
|
@ -150,22 +158,71 @@ NSString *const SignalsViewControllerSegueShowIncomingCall = @"ShowIncomingCallS
|
|||
}
|
||||
}
|
||||
|
||||
- (void)handleActiveCallNotification:(NSNotification *)notification
|
||||
- (void)handlePresentCallInterstitialNotification:(NSNotification *)notification
|
||||
{
|
||||
AssertIsOnMainThread();
|
||||
|
||||
if (![notification.object isKindOfClass:[SignalCall class]]) {
|
||||
DDLogError(@"%@ expected presentCall observer to be notified with a SignalCall, but found %@",
|
||||
self.tag,
|
||||
notification.object);
|
||||
return;
|
||||
}
|
||||
|
||||
SignalCall *call = (SignalCall *)notification.object;
|
||||
|
||||
NSString *callToken = notification.object;
|
||||
OWSAssert(callToken != nil);
|
||||
|
||||
OWSCallInterstitialViewController *viewController = [OWSCallInterstitialViewController new];
|
||||
viewController.callToken = callToken;
|
||||
|
||||
void(^presentInterstitial)() = ^{
|
||||
viewController.modalPresentationStyle = UIModalPresentationOverFullScreen;
|
||||
[self presentViewController:viewController
|
||||
animated:NO
|
||||
completion:nil];
|
||||
};
|
||||
|
||||
// Dismiss any other modals so we can present call modal.
|
||||
if (self.presentedViewController) {
|
||||
[self dismissViewControllerAnimated:YES completion:^{
|
||||
presentInterstitial();
|
||||
}];
|
||||
} else {
|
||||
presentInterstitial();
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleDismissCallInterstitialNotification:(NSNotification *)notification
|
||||
{
|
||||
AssertIsOnMainThread();
|
||||
|
||||
NSString *callToken = notification.object;
|
||||
OWSAssert(callToken != nil);
|
||||
|
||||
if (!self.presentedViewController ||
|
||||
![self.presentedViewController isKindOfClass:[OWSCallInterstitialViewController class]]) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
OWSCallInterstitialViewController *viewController = (OWSCallInterstitialViewController *)self.presentedViewController;
|
||||
if (![viewController.callToken isEqualToString:callToken]) {
|
||||
return;
|
||||
}
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)handleActiveCallNotification:(NSNotification *)notification
|
||||
{
|
||||
AssertIsOnMainThread();
|
||||
|
||||
if (![notification.object isKindOfClass:[SignalCall class]]) {
|
||||
DDLogError(@"%@ expected presentCall observer to be notified with a SignalCall, but found %@",
|
||||
self.tag,
|
||||
notification.object);
|
||||
return;
|
||||
}
|
||||
|
||||
SignalCall *call = (SignalCall *)notification.object;
|
||||
|
||||
// Dismiss any other modals so we can present call modal.
|
||||
if (self.presentedViewController) {
|
||||
BOOL shouldAnimate = ![self.presentedViewController isKindOfClass:[OWSCallInterstitialViewController class]];
|
||||
[self dismissViewControllerAnimated:shouldAnimate
|
||||
completion:^{
|
||||
[self performSegueWithIdentifier:SignalsViewControllerSegueShowIncomingCall sender:call];
|
||||
}];
|
||||
} else {
|
||||
|
|
|
@ -70,6 +70,12 @@
|
|||
/* No comment provided by engineer. */
|
||||
"AUDIO_PERMISSION_MESSAGE" = "Signal requires access to your microphone to work properly. You can grant this permission in the Settings app >> Privacy >> Microphone >> Signal";
|
||||
|
||||
/* Title for call interstitial view */
|
||||
"CALL_INTERSTITIAL_CALLING_LABEL" = "Calling...";
|
||||
|
||||
/* Label for cancel button on call interstitial view */
|
||||
"CALL_INTERSTITIAL_CANCEL_BUTTON" = "Cancel";
|
||||
|
||||
/* Accessibilty label for placing call button */
|
||||
"CALL_LABEL" = "Call";
|
||||
|
||||
|
|
Loading…
Reference in a new issue