765 lines
29 KiB
Objective-C
765 lines
29 KiB
Objective-C
/*
|
|
Copyright 2018-2024 New Vector Ltd.
|
|
Copyright 2017 Vector Creations Ltd
|
|
Copyright 2016 OpenMarket Ltd
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only
|
|
Please see LICENSE in the repository root for full details.
|
|
*/
|
|
|
|
#import "CallViewController.h"
|
|
|
|
#import "GeneratedInterface-Swift.h"
|
|
|
|
#import "AvatarGenerator.h"
|
|
|
|
#import "UsersDevicesViewController.h"
|
|
|
|
#import "RiotNavigationController.h"
|
|
|
|
#import "IncomingCallView.h"
|
|
|
|
@interface CallViewController () <
|
|
PictureInPicturable,
|
|
DialpadViewControllerDelegate,
|
|
CallTransferMainViewControllerDelegate,
|
|
CallAudioRouteMenuViewDelegate>
|
|
{
|
|
// Current alert (if any).
|
|
UIAlertController *currentAlert;
|
|
|
|
// Flag to compute self.shouldPromptForStunServerFallback
|
|
BOOL promptForStunServerFallback;
|
|
}
|
|
|
|
@property (nonatomic, weak) IBOutlet UIView *pipViewContainer;
|
|
|
|
@property (nonatomic, strong) id<Theme> overriddenTheme;
|
|
@property (nonatomic, assign) BOOL inPiP;
|
|
@property (nonatomic, strong) CallPiPView *pipView;
|
|
|
|
@property (nonatomic, strong) CustomSizedPresentationController *customSizedPresentationController;
|
|
@property (nonatomic, strong) SlidingModalPresenter *slidingModalPresenter;
|
|
@property (nonatomic, strong) CallAudioRouteMenuView *audioRoutesMenuView;
|
|
|
|
@end
|
|
|
|
@implementation CallViewController
|
|
|
|
- (void)finalizeInit
|
|
{
|
|
[super finalizeInit];
|
|
|
|
// Setup `MXKViewControllerHandling` properties
|
|
self.enableBarTintColorStatusChange = NO;
|
|
self.rageShakeManager = [RageShakeManager sharedManager];
|
|
|
|
promptForStunServerFallback = NO;
|
|
_shouldPromptForStunServerFallback = NO;
|
|
}
|
|
|
|
- (void)viewDidLoad
|
|
{
|
|
[super viewDidLoad];
|
|
|
|
// Back button
|
|
|
|
UIImage *backButtonImage = AssetImages.backIcon.image;
|
|
[self.backToAppButton setImage:backButtonImage forState:UIControlStateNormal];
|
|
[self.backToAppButton setImage:backButtonImage forState:UIControlStateHighlighted];
|
|
|
|
// Camera switch
|
|
|
|
UIImage *cameraSwitchButtonImage = AssetImages.cameraSwitch.image;
|
|
[self.cameraSwitchButton setImage:cameraSwitchButtonImage forState:UIControlStateNormal];
|
|
[self.cameraSwitchButton setImage:cameraSwitchButtonImage forState:UIControlStateHighlighted];
|
|
|
|
// Audio mute
|
|
|
|
UIImage *audioMuteOffButtonImage = AssetImages.callAudioMuteOffIcon.image;
|
|
UIImage *audioMuteOnButtonImage = AssetImages.callAudioMuteOnIcon.image;
|
|
|
|
[self.audioMuteButton setImage:audioMuteOffButtonImage forState:UIControlStateNormal];
|
|
[self.audioMuteButton setImage:audioMuteOffButtonImage forState:UIControlStateHighlighted];
|
|
[self.audioMuteButton setImage:audioMuteOnButtonImage forState:UIControlStateSelected];
|
|
|
|
// Video mute
|
|
|
|
UIImage *videoOffButtonImage = AssetImages.callVideoMuteOffIcon.image;
|
|
UIImage *videoOnButtonImage = AssetImages.callVideoMuteOnIcon.image;
|
|
|
|
[self.videoMuteButton setImage:videoOffButtonImage forState:UIControlStateNormal];
|
|
[self.videoMuteButton setImage:videoOffButtonImage forState:UIControlStateHighlighted];
|
|
[self.videoMuteButton setImage:videoOnButtonImage forState:UIControlStateSelected];
|
|
|
|
// More
|
|
|
|
UIImage *moreButtonImage = AssetImages.callMoreIcon.image;
|
|
|
|
[self.moreButtonForVoice setImage:moreButtonImage forState:UIControlStateNormal];
|
|
[self.moreButtonForVideo setImage:moreButtonImage forState:UIControlStateNormal];
|
|
|
|
// Hang up
|
|
|
|
UIImage *hangUpButtonImage = AssetImages.callHangupLarge.image;
|
|
|
|
[self.endCallButton setTitle:nil forState:UIControlStateNormal];
|
|
[self.endCallButton setTitle:nil forState:UIControlStateHighlighted];
|
|
[self.endCallButton setImage:hangUpButtonImage forState:UIControlStateNormal];
|
|
[self.endCallButton setImage:hangUpButtonImage forState:UIControlStateHighlighted];
|
|
|
|
// force orientation to portrait if phone
|
|
if ([UIDevice currentDevice].isPhone)
|
|
{
|
|
[[UIDevice currentDevice] setValue:[NSNumber numberWithInteger: UIInterfaceOrientationPortrait] forKey:@"orientation"];
|
|
}
|
|
|
|
[self updateLocalPreviewLayout];
|
|
|
|
[self configureUserInterface];
|
|
}
|
|
|
|
- (UIStatusBarStyle)preferredStatusBarStyle
|
|
{
|
|
return self.overriddenTheme.statusBarStyle;
|
|
}
|
|
|
|
- (void)configureUserInterface
|
|
{
|
|
if (@available(iOS 13.0, *)) {
|
|
self.overrideUserInterfaceStyle = self.overriddenTheme.userInterfaceStyle;
|
|
}
|
|
|
|
[self.overriddenTheme applyStyleOnNavigationBar:self.navigationController.navigationBar];
|
|
|
|
self.barTitleColor = self.overriddenTheme.textPrimaryColor;
|
|
self.activityIndicator.backgroundColor = self.overriddenTheme.overlayBackgroundColor;
|
|
|
|
self.backToAppButton.tintColor = self.overriddenTheme.callScreenButtonTintColor;
|
|
self.cameraSwitchButton.tintColor = self.overriddenTheme.callScreenButtonTintColor;
|
|
self.callerNameLabel.textColor = self.overriddenTheme.baseTextPrimaryColor;
|
|
self.callStatusLabel.textColor = self.overriddenTheme.baseTextPrimaryColor;
|
|
[self.resumeButton setTitleColor:self.overriddenTheme.tintColor
|
|
forState:UIControlStateNormal];
|
|
[self.transferButton setTitleColor:self.overriddenTheme.tintColor
|
|
forState:UIControlStateNormal];
|
|
|
|
self.localPreviewContainerView.layer.borderColor = self.overriddenTheme.tintColor.CGColor;
|
|
self.localPreviewContainerView.layer.borderWidth = 2;
|
|
self.localPreviewContainerView.layer.cornerRadius = 5;
|
|
self.localPreviewContainerView.clipsToBounds = YES;
|
|
}
|
|
|
|
- (void)viewWillDisappear:(BOOL)animated
|
|
{
|
|
if (currentAlert)
|
|
{
|
|
[currentAlert dismissViewControllerAnimated:NO completion:nil];
|
|
currentAlert = nil;
|
|
}
|
|
|
|
[super viewWillDisappear:animated];
|
|
}
|
|
|
|
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
|
|
{
|
|
// limit orientation to portrait only for phone
|
|
if ([UIDevice currentDevice].isPhone)
|
|
{
|
|
return UIInterfaceOrientationMaskPortrait;
|
|
}
|
|
return [super supportedInterfaceOrientations];
|
|
}
|
|
|
|
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation
|
|
{
|
|
if ([UIDevice currentDevice].isPhone)
|
|
{
|
|
return UIInterfaceOrientationPortrait;
|
|
}
|
|
return [super preferredInterfaceOrientationForPresentation];
|
|
}
|
|
|
|
- (BOOL)shouldAutorotate
|
|
{
|
|
return NO;
|
|
}
|
|
|
|
#pragma mark - override MXKViewController
|
|
|
|
- (UIView *)createIncomingCallView
|
|
{
|
|
if ([MXCallKitAdapter callKitAvailable])
|
|
{
|
|
return nil;
|
|
}
|
|
|
|
NSString *callInfo;
|
|
if (self.mxCall.isVideoCall)
|
|
callInfo = [VectorL10n callIncomingVideo];
|
|
else
|
|
callInfo = [VectorL10n callIncomingVoice];
|
|
|
|
IncomingCallView *incomingCallView = [[IncomingCallView alloc] initWithCallerAvatar:self.peer.avatarUrl
|
|
mediaManager:self.mainSession.mediaManager
|
|
placeholderImage:self.picturePlaceholder
|
|
callerName:self.peer.displayname
|
|
callInfo:callInfo];
|
|
|
|
// Incoming call is retained by call vc so use weak to avoid retain cycle
|
|
__weak typeof(self) weakSelf = self;
|
|
|
|
incomingCallView.onAnswer = ^{
|
|
[weakSelf onButtonPressed:weakSelf.answerCallButton];
|
|
};
|
|
|
|
incomingCallView.onReject = ^{
|
|
[weakSelf onButtonPressed:weakSelf.rejectCallButton];
|
|
};
|
|
|
|
return incomingCallView;
|
|
}
|
|
|
|
- (void)showAudioDeviceOptions
|
|
{
|
|
MXiOSAudioOutputRouter *router = self.mxCall.audioOutputRouter;
|
|
if (router.isAnyExternalDeviceConnected)
|
|
{
|
|
self.slidingModalPresenter = [SlidingModalPresenter new];
|
|
|
|
_audioRoutesMenuView = [[CallAudioRouteMenuView alloc] initWithRoutes:router.availableOutputRoutes
|
|
currentRoute:router.currentRoute];
|
|
_audioRoutesMenuView.delegate = self;
|
|
|
|
[self.slidingModalPresenter presentView:_audioRoutesMenuView
|
|
from:self
|
|
animated:true
|
|
options:SlidingModalPresenter.CenterInScreenOption
|
|
completion:nil];
|
|
}
|
|
else
|
|
{
|
|
// toggle between built-in and loud speakers
|
|
switch (router.currentRoute.routeType)
|
|
{
|
|
case MXiOSAudioOutputRouteTypeBuiltIn:
|
|
[router changeCurrentRouteTo:router.loudSpeakersRoute];
|
|
break;
|
|
case MXiOSAudioOutputRouteTypeLoudSpeakers:
|
|
[router changeCurrentRouteTo:router.builtInRoute];
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
- (void)configureSpeakerButton
|
|
{
|
|
switch (self.mxCall.audioOutputRouter.currentRoute.routeType)
|
|
{
|
|
case MXiOSAudioOutputRouteTypeBuiltIn:
|
|
[self.speakerButton setImage:AssetImages.callSpeakerOffIcon.image
|
|
forState:UIControlStateNormal];
|
|
break;
|
|
case MXiOSAudioOutputRouteTypeLoudSpeakers:
|
|
[self.speakerButton setImage:AssetImages.callSpeakerOnIcon.image
|
|
forState:UIControlStateNormal];
|
|
break;
|
|
case MXiOSAudioOutputRouteTypeExternalWired:
|
|
case MXiOSAudioOutputRouteTypeExternalBluetooth:
|
|
case MXiOSAudioOutputRouteTypeExternalCar:
|
|
[self.speakerButton setImage:AssetImages.callSpeakerExternalIcon.image
|
|
forState:UIControlStateNormal];
|
|
break;
|
|
}
|
|
}
|
|
|
|
#pragma mark - MXCallDelegate
|
|
|
|
- (void)call:(MXCall *)call stateDidChange:(MXCallState)state reason:(MXEvent *)event
|
|
{
|
|
[super call:call stateDidChange:state reason:event];
|
|
|
|
[self configurePiPView];
|
|
|
|
[self checkStunServerFallbackWithCallState:state];
|
|
}
|
|
|
|
- (void)call:(MXCall *)call didEncounterError:(NSError *)error reason:(MXCallHangupReason)reason
|
|
{
|
|
if ([error.domain isEqualToString:MXEncryptingErrorDomain]
|
|
&& error.code == MXEncryptingErrorUnknownDeviceCode)
|
|
{
|
|
// There are unknown devices, check what the user wants to do
|
|
__weak __typeof(self) weakSelf = self;
|
|
|
|
MXUsersDevicesMap<MXDeviceInfo*> *unknownDevices = error.userInfo[MXEncryptingErrorUnknownDeviceDevicesKey];
|
|
|
|
[currentAlert dismissViewControllerAnimated:NO completion:nil];
|
|
|
|
currentAlert = [UIAlertController alertControllerWithTitle:[VectorL10n unknownDevicesAlertTitle]
|
|
message:[VectorL10n unknownDevicesAlert]
|
|
preferredStyle:UIAlertControllerStyleAlert];
|
|
|
|
[currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n unknownDevicesVerify]
|
|
style:UIAlertActionStyleDefault
|
|
handler:^(UIAlertAction * action) {
|
|
|
|
if (weakSelf)
|
|
{
|
|
typeof(self) self = weakSelf;
|
|
self->currentAlert = nil;
|
|
|
|
// Get the UsersDevicesViewController from the storyboard
|
|
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:[NSBundle mainBundle]];
|
|
UsersDevicesViewController *usersDevicesViewController = [storyboard instantiateViewControllerWithIdentifier:@"UsersDevicesViewControllerStoryboardId"];
|
|
|
|
[usersDevicesViewController displayUsersDevices:unknownDevices andMatrixSession:self.mainSession onComplete:^(BOOL doneButtonPressed) {
|
|
|
|
if (doneButtonPressed)
|
|
{
|
|
// Retry the call
|
|
if (call.isIncoming)
|
|
{
|
|
[call answer];
|
|
}
|
|
else
|
|
{
|
|
[call callWithVideo:call.isVideoCall];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Ignore the call
|
|
[call hangupWithReason:reason];
|
|
}
|
|
}];
|
|
|
|
// Show this screen within a navigation controller
|
|
UINavigationController *usersDevicesNavigationController = [[RiotNavigationController alloc] init];
|
|
|
|
// Set Riot navigation bar colors
|
|
[ThemeService.shared.theme applyStyleOnNavigationBar:usersDevicesNavigationController.navigationBar];
|
|
usersDevicesNavigationController.navigationBar.barTintColor = ThemeService.shared.theme.backgroundColor;
|
|
|
|
[usersDevicesNavigationController pushViewController:usersDevicesViewController animated:NO];
|
|
|
|
[self presentViewController:usersDevicesNavigationController animated:YES completion:nil];
|
|
|
|
}
|
|
|
|
}]];
|
|
|
|
|
|
[currentAlert addAction:[UIAlertAction actionWithTitle:(call.isIncoming ? [VectorL10n unknownDevicesAnswerAnyway] : [VectorL10n unknownDevicesCallAnyway])
|
|
style:UIAlertActionStyleDefault
|
|
handler:^(UIAlertAction * action) {
|
|
|
|
if (weakSelf)
|
|
{
|
|
typeof(self) self = weakSelf;
|
|
self->currentAlert = nil;
|
|
|
|
// Retry the call
|
|
if (call.isIncoming)
|
|
{
|
|
[call answer];
|
|
}
|
|
else
|
|
{
|
|
[call callWithVideo:call.isVideoCall];
|
|
}
|
|
}
|
|
|
|
}]];
|
|
|
|
[currentAlert mxk_setAccessibilityIdentifier:@"CallVCUnknownDevicesAlert"];
|
|
[self presentViewController:currentAlert animated:YES completion:nil];
|
|
}
|
|
else
|
|
{
|
|
[super call:call didEncounterError:error reason:reason];
|
|
}
|
|
}
|
|
|
|
|
|
#pragma mark - Fallback STUN server
|
|
|
|
- (void)checkStunServerFallbackWithCallState:(MXCallState)callState
|
|
{
|
|
// Detect if we should display the prompt to fallback to the STUN server defined
|
|
// in the app plist if the homeserver does not provide STUN or TURN servers.
|
|
// We should display it if the call ends while we were in connecting state
|
|
if (!self.mainSession.callManager.turnServers
|
|
&& !self.mainSession.callManager.fallbackSTUNServer
|
|
&& !RiotSettings.shared.isAllowStunServerFallbackHasBeenSetOnce)
|
|
{
|
|
switch (callState)
|
|
{
|
|
case MXCallStateConnecting:
|
|
promptForStunServerFallback = YES;
|
|
break;
|
|
|
|
case MXCallStateConnected:
|
|
promptForStunServerFallback = NO;
|
|
break;
|
|
|
|
case MXCallStateEnded:
|
|
if (promptForStunServerFallback)
|
|
{
|
|
_shouldPromptForStunServerFallback = YES;
|
|
}
|
|
|
|
default:
|
|
// There is nothing to do for other states
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
#pragma mark - Properties
|
|
|
|
- (id<Theme>)overriddenTheme
|
|
{
|
|
if (_overriddenTheme == nil)
|
|
{
|
|
_overriddenTheme = [DarkTheme new];
|
|
}
|
|
return _overriddenTheme;
|
|
}
|
|
|
|
- (CallPiPView *)pipView
|
|
{
|
|
if (_pipView == nil)
|
|
{
|
|
_pipView = [CallPiPView instantiateWithSession:self.mainSession];
|
|
[_pipView updateWithTheme:self.overriddenTheme];
|
|
}
|
|
return _pipView;
|
|
}
|
|
|
|
- (void)setMxCallOnHold:(MXCall *)mxCallOnHold
|
|
{
|
|
[super setMxCallOnHold:mxCallOnHold];
|
|
|
|
[self configurePiPView];
|
|
}
|
|
|
|
- (UIImage*)picturePlaceholder
|
|
{
|
|
CGFloat fontSize = floor(self.callerImageViewWidthConstraint.constant * 0.7);
|
|
|
|
if (self.peer)
|
|
{
|
|
// Use the vector style placeholder
|
|
return [AvatarGenerator generateAvatarForMatrixItem:self.peer.userId
|
|
withDisplayName:self.peer.displayname
|
|
size:self.callerImageViewWidthConstraint.constant
|
|
andFontSize:fontSize];
|
|
}
|
|
else if (self.mxCall.room)
|
|
{
|
|
return [AvatarGenerator generateAvatarForMatrixItem:self.mxCall.room.roomId
|
|
withDisplayName:self.mxCall.room.summary.displayName
|
|
size:self.callerImageViewWidthConstraint.constant
|
|
andFontSize:fontSize];
|
|
}
|
|
|
|
return [MXKTools paintImage:AssetImages.placeholder.image
|
|
withColor:self.overriddenTheme.tintColor];
|
|
}
|
|
|
|
- (void)updatePeerInfoDisplay
|
|
{
|
|
[super updatePeerInfoDisplay];
|
|
|
|
NSString *peerAvatarURL;
|
|
|
|
if (self.peer)
|
|
{
|
|
peerAvatarURL = self.peer.avatarUrl;
|
|
}
|
|
else if (self.mxCall.isConferenceCall)
|
|
{
|
|
peerAvatarURL = self.mxCall.room.summary.avatar;
|
|
}
|
|
|
|
self.blurredCallerImageView.contentMode = UIViewContentModeScaleAspectFill;
|
|
self.callerImageView.contentMode = UIViewContentModeScaleAspectFill;
|
|
if (peerAvatarURL)
|
|
{
|
|
// Retrieve the avatar in full resolution
|
|
[self.blurredCallerImageView setImageURI:peerAvatarURL
|
|
withType:nil
|
|
andImageOrientation:UIImageOrientationUp
|
|
previewImage:self.picturePlaceholder
|
|
mediaManager:self.mainSession.mediaManager];
|
|
|
|
// Retrieve the avatar in full resolution
|
|
[self.callerImageView setImageURI:peerAvatarURL
|
|
withType:nil
|
|
andImageOrientation:UIImageOrientationUp
|
|
previewImage:self.picturePlaceholder
|
|
mediaManager:self.mainSession.mediaManager];
|
|
}
|
|
else
|
|
{
|
|
self.blurredCallerImageView.image = self.picturePlaceholder;
|
|
self.callerImageView.image = self.picturePlaceholder;
|
|
}
|
|
}
|
|
|
|
#pragma mark - Sounds
|
|
|
|
- (NSURL*)audioURLWithName:(NSString*)soundName
|
|
{
|
|
NSURL *audioUrl;
|
|
|
|
NSString *path = [[NSBundle mainBundle] pathForResource:soundName ofType:@"mp3"];
|
|
if (path)
|
|
{
|
|
audioUrl = [NSURL fileURLWithPath:path];
|
|
}
|
|
|
|
// Use by default the matrix kit sounds.
|
|
if (!audioUrl)
|
|
{
|
|
audioUrl = [super audioURLWithName:soundName];
|
|
}
|
|
|
|
return audioUrl;
|
|
}
|
|
|
|
#pragma mark - Actions
|
|
|
|
- (IBAction)onButtonPressed:(id)sender
|
|
{
|
|
if (sender == _chatButton)
|
|
{
|
|
if (self.delegate)
|
|
{
|
|
// Dismiss the view controller whereas the call is still running
|
|
[self.delegate dismissCallViewController:self completion:^{
|
|
|
|
if (self.mxCall.room)
|
|
{
|
|
// Open the room page
|
|
Analytics.shared.viewRoomTrigger = AnalyticsViewRoomTriggerInCall;
|
|
[[AppDelegate theDelegate] showRoom:self.mxCall.room.roomId andEventId:nil withMatrixSession:self.mxCall.room.mxSession];
|
|
}
|
|
|
|
}];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
[super onButtonPressed:sender];
|
|
}
|
|
}
|
|
|
|
- (void)setInPiP:(BOOL)inPiP
|
|
{
|
|
_inPiP = inPiP;
|
|
|
|
if (_inPiP)
|
|
{
|
|
self.overlayContainerView.hidden = YES;
|
|
self.callerImageView.hidden = YES;
|
|
self.callerNameLabel.hidden = YES;
|
|
self.callStatusLabel.hidden = YES;
|
|
self.localPreviewContainerView.hidden = YES;
|
|
self.localPreviewActivityView.hidden = YES;
|
|
|
|
if (self.pipViewContainer.subviews.count == 0)
|
|
{
|
|
[self.pipViewContainer vc_addSubViewMatchingParent:self.pipView];
|
|
}
|
|
[self configurePiPView];
|
|
self.pipViewContainer.hidden = NO;
|
|
}
|
|
else
|
|
{
|
|
self.pipViewContainer.hidden = YES;
|
|
self.localPreviewContainerView.hidden = !self.mxCall.isVideoCall;
|
|
self.callerImageView.hidden = self.mxCall.isVideoCall && self.mxCall.state == MXCallStateConnected;
|
|
self.callerNameLabel.hidden = NO;
|
|
self.callStatusLabel.hidden = NO;
|
|
|
|
// show controls when coming back from PiP mode
|
|
[self showOverlayContainer:YES];
|
|
}
|
|
}
|
|
|
|
- (void)showOverlayContainer:(BOOL)isShown
|
|
{
|
|
if (self.inPiP)
|
|
{
|
|
return;
|
|
}
|
|
|
|
[super showOverlayContainer:isShown];
|
|
}
|
|
|
|
#pragma mark - DTMF
|
|
|
|
- (void)openDialpad
|
|
{
|
|
DialpadConfiguration *config = [[DialpadConfiguration alloc] initWithShowsTitle:YES
|
|
showsCloseButton:YES
|
|
showsBackspaceButton:NO
|
|
showsCallButton:NO
|
|
formattingEnabled:NO
|
|
editingEnabled:NO
|
|
playTones:YES];
|
|
DialpadViewController *controller = [DialpadViewController instantiateWithConfiguration:config];
|
|
controller.delegate = self;
|
|
self.customSizedPresentationController = [[CustomSizedPresentationController alloc] initWithPresentedViewController:controller presentingViewController:self];
|
|
self.customSizedPresentationController.dismissOnBackgroundTap = NO;
|
|
self.customSizedPresentationController.cornerRadius = 16;
|
|
|
|
controller.transitioningDelegate = self.customSizedPresentationController;
|
|
[self presentViewController:controller animated:YES completion:nil];
|
|
}
|
|
|
|
#pragma mark - Call Transfer
|
|
|
|
- (void)openCallTransfer
|
|
{
|
|
CallTransferMainViewController *controller = [CallTransferMainViewController instantiateWithSession:self.mainSession ignoredUserIds:@[self.peer.userId]];
|
|
controller.delegate = self;
|
|
UINavigationController *navController = [[RiotNavigationController alloc] initWithRootViewController:controller];
|
|
[self.mxCall hold:YES];
|
|
[self presentViewController:navController animated:YES completion:nil];
|
|
}
|
|
|
|
#pragma mark - DialpadViewControllerDelegate
|
|
|
|
- (void)dialpadViewControllerDidTapClose:(DialpadViewController *)viewController
|
|
{
|
|
[viewController dismissViewControllerAnimated:YES completion:nil];
|
|
self.customSizedPresentationController = nil;
|
|
}
|
|
|
|
- (void)dialpadViewControllerDidTapDigit:(DialpadViewController *)viewController digit:(NSString *)digit
|
|
{
|
|
if (digit.length == 0)
|
|
{
|
|
return;
|
|
}
|
|
BOOL result = [self.mxCall sendDTMF:digit];
|
|
|
|
MXLogDebug(@"[CallViewController] Sending DTMF tones %@", result ? @"succeeded": @"failed");
|
|
}
|
|
|
|
#pragma mark - CallTransferMainViewControllerDelegate
|
|
|
|
- (void)callTransferMainViewControllerDidComplete:(CallTransferMainViewController *)viewController consult:(BOOL)consult contact:(MXKContact *)contact phoneNumber:(NSString *)phoneNumber
|
|
{
|
|
[viewController dismissViewControllerAnimated:YES completion:nil];
|
|
|
|
void(^failureBlock)(NSError *_Nullable) = ^(NSError *error) {
|
|
[self->currentAlert dismissViewControllerAnimated:NO completion:nil];
|
|
|
|
MXWeakify(self);
|
|
|
|
self->currentAlert = [UIAlertController alertControllerWithTitle:[VectorL10n callTransferErrorTitle]
|
|
message:[VectorL10n callTransferErrorMessage]
|
|
preferredStyle:UIAlertControllerStyleAlert];
|
|
|
|
[self->currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n ok]
|
|
style:UIAlertActionStyleDefault
|
|
handler:^(UIAlertAction * action) {
|
|
|
|
MXStrongifyAndReturnIfNil(self);
|
|
self->currentAlert = nil;
|
|
}]];
|
|
|
|
[self presentViewController:self->currentAlert animated:YES completion:nil];
|
|
};
|
|
|
|
void(^continueBlock)(NSString *_Nonnull) = ^(NSString *targetUserId) {
|
|
MXUserModel *targetUser = [[MXUserModel alloc] initWithUserId:targetUserId
|
|
displayname:contact.displayName
|
|
avatarUrl:contact.matrixAvatarURL];
|
|
MXUserModel *transfereeUser = [[MXUserModel alloc] initWithUser:self.peer];
|
|
|
|
[self.mainSession.callManager transferCall:self.mxCall
|
|
to:targetUser
|
|
withTransferee:transfereeUser
|
|
consultFirst:consult
|
|
success:^(NSString * _Nonnull newCallId){
|
|
MXLogDebug(@"Call transfer succeeded with new call ID: %@", newCallId);
|
|
} failure:^(NSError * _Nullable error) {
|
|
MXLogDebug(@"Call transfer failed with error: %@", error);
|
|
failureBlock(error);
|
|
}];
|
|
};
|
|
|
|
if (contact)
|
|
{
|
|
continueBlock(contact.matrixIdentifiers.firstObject);
|
|
}
|
|
else if (phoneNumber)
|
|
{
|
|
MXWeakify(self);
|
|
[self.mainSession.callManager getThirdPartyUserFrom:phoneNumber success:^(MXThirdPartyUserInstance * _Nonnull user) {
|
|
if (weakself == nil) {
|
|
return;
|
|
}
|
|
|
|
continueBlock(user.userId);
|
|
} failure:^(NSError * _Nullable error) {
|
|
failureBlock(error);
|
|
}];
|
|
}
|
|
}
|
|
|
|
- (void)callTransferMainViewControllerDidCancel:(CallTransferMainViewController *)viewController
|
|
{
|
|
[self.mxCall hold:NO];
|
|
[viewController dismissViewControllerAnimated:YES completion:nil];
|
|
}
|
|
|
|
#pragma mark - PiP
|
|
|
|
- (void)configurePiPView
|
|
{
|
|
if (self.inPiP)
|
|
{
|
|
[self.pipView configureWithCall:self.mxCall
|
|
peer:self.peer
|
|
onHoldCall:self.mxCallOnHold
|
|
onHoldPeer:self.peerOnHold];
|
|
}
|
|
}
|
|
|
|
#pragma mark - PictureInPicturable
|
|
|
|
- (void)didEnterPiP
|
|
{
|
|
self.inPiP = YES;
|
|
}
|
|
|
|
- (void)willExitPiP
|
|
{
|
|
self.pipViewContainer.hidden = YES;
|
|
}
|
|
|
|
- (void)didExitPiP
|
|
{
|
|
self.inPiP = NO;
|
|
}
|
|
|
|
#pragma mark - CallAudioRouteMenuViewDelegate
|
|
|
|
- (void)callAudioRouteMenuView:(CallAudioRouteMenuView *)view didSelectRoute:(MXiOSAudioOutputRoute *)route
|
|
{
|
|
[self.mxCall.audioOutputRouter changeCurrentRouteTo:route];
|
|
[self.slidingModalPresenter dismissWithAnimated:YES completion:nil];
|
|
}
|
|
|
|
@end
|