diff --git a/Riot.xcodeproj/project.pbxproj b/Riot.xcodeproj/project.pbxproj index 6d8ab14c7..7f4d1ddbc 100644 --- a/Riot.xcodeproj/project.pbxproj +++ b/Riot.xcodeproj/project.pbxproj @@ -76,6 +76,8 @@ 32D392191EB9B7AB009A2BAF /* DirectoryServerDetailTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 32D392171EB9B7AB009A2BAF /* DirectoryServerDetailTableViewCell.xib */; }; 32FD0A3D1EB0CD9B0072B066 /* BugReportViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 32FD0A3B1EB0CD9B0072B066 /* BugReportViewController.m */; }; 32FD0A3E1EB0CD9B0072B066 /* BugReportViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 32FD0A3C1EB0CD9B0072B066 /* BugReportViewController.xib */; }; + 92324BE31F4F66D3009DE194 /* IncomingCallView.m in Sources */ = {isa = PBXBuildFile; fileRef = 92324BE21F4F66D3009DE194 /* IncomingCallView.m */; }; + 92324BE61F4F6A60009DE194 /* CircleButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 92324BE51F4F6A60009DE194 /* CircleButton.m */; }; A27ECCE3FC4971745D2CB78D /* libPods-RiotShareExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7246451C668D6782166E22EC /* libPods-RiotShareExtension.a */; }; F0131DE51F2200D600CBF707 /* RiotSplitViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F0131DE41F2200D600CBF707 /* RiotSplitViewController.m */; }; F02C1A861E8EB04C0045A404 /* PeopleViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F02C1A841E8EB04C0045A404 /* PeopleViewController.m */; }; @@ -646,6 +648,10 @@ 7246451C668D6782166E22EC /* libPods-RiotShareExtension.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RiotShareExtension.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 765F5104DB3EC39713DEB3A4 /* Pods-RiotShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RiotShareExtension.release.xcconfig"; path = "Pods/Target Support Files/Pods-RiotShareExtension/Pods-RiotShareExtension.release.xcconfig"; sourceTree = ""; }; 839BB91240D350D5607D55BA /* Pods-Riot.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Riot.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Riot/Pods-Riot.debug.xcconfig"; sourceTree = ""; }; + 92324BE11F4F66D3009DE194 /* IncomingCallView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IncomingCallView.h; sourceTree = ""; }; + 92324BE21F4F66D3009DE194 /* IncomingCallView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IncomingCallView.m; sourceTree = ""; }; + 92324BE41F4F6A60009DE194 /* CircleButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CircleButton.h; sourceTree = ""; }; + 92324BE51F4F6A60009DE194 /* CircleButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CircleButton.m; sourceTree = ""; }; C195C53961EA28E6900AEB68 /* Pods-Riot.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Riot.release.xcconfig"; path = "Pods/Target Support Files/Pods-Riot/Pods-Riot.release.xcconfig"; sourceTree = ""; }; F0131DE31F2200D600CBF707 /* RiotSplitViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RiotSplitViewController.h; sourceTree = ""; }; F0131DE41F2200D600CBF707 /* RiotSplitViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RiotSplitViewController.m; sourceTree = ""; }; @@ -1412,6 +1418,17 @@ name = Pods; sourceTree = ""; }; + 92324BE01F4F668F009DE194 /* Calls */ = { + isa = PBXGroup; + children = ( + 92324BE11F4F66D3009DE194 /* IncomingCallView.h */, + 92324BE21F4F66D3009DE194 /* IncomingCallView.m */, + 92324BE41F4F6A60009DE194 /* CircleButton.h */, + 92324BE51F4F6A60009DE194 /* CircleButton.m */, + ); + path = Calls; + sourceTree = ""; + }; F083BB021E7005FD00A9B29C /* RiotTests */ = { isa = PBXGroup; children = ( @@ -1920,6 +1937,7 @@ F083BC571E7009EC00A9B29C /* Views */ = { isa = PBXGroup; children = ( + 92324BE01F4F668F009DE194 /* Calls */, F0B4CBAD1F4215E3008E99C5 /* Event */, F083BC581E7009EC00A9B29C /* Authentication */, F083BC5F1E7009EC00A9B29C /* Contact */, @@ -2993,6 +3011,7 @@ F083BE9C1E7009ED00A9B29C /* TableViewCellWithCheckBoxAndLabel.m in Sources */, 32471CE11F13AC1500BDF50A /* RoomMembershipExpandedWithPaginationTitleBubbleCell.m in Sources */, F083BDFE1E7009ED00A9B29C /* RecentCellData.m in Sources */, + 92324BE61F4F6A60009DE194 /* CircleButton.m in Sources */, F083BE3A1E7009ED00A9B29C /* RoomIncomingEncryptedAttachmentBubbleCell.m in Sources */, 3205ED841E97725E003D65FA /* DirectoryServerTableViewCell.m in Sources */, F083BEA21E7009ED00A9B29C /* TableViewCellWithPhoneNumberTextField.m in Sources */, @@ -3026,6 +3045,7 @@ F083BE481E7009ED00A9B29C /* RoomIncomingEncryptedTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.m in Sources */, F06CDD691EF01E3900870B75 /* RoomEmptyBubbleCell.m in Sources */, F083BE041E7009ED00A9B29C /* Tools.m in Sources */, + 92324BE31F4F66D3009DE194 /* IncomingCallView.m in Sources */, F083BE6A1E7009ED00A9B29C /* RoomOutgoingAttachmentBubbleCell.m in Sources */, F083BDEF1E7009ED00A9B29C /* UINavigationController+Riot.m in Sources */, F083BE761E7009ED00A9B29C /* RoomOutgoingTextMsgWithPaginationTitleBubbleCell.m in Sources */, diff --git a/Riot/AppDelegate.m b/Riot/AppDelegate.m index 37989a831..3804f4757 100644 --- a/Riot/AppDelegate.m +++ b/Riot/AppDelegate.m @@ -1812,81 +1812,8 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN currentCallViewController = [CallViewController callViewController:mxCall]; currentCallViewController.delegate = self; - if (mxCall.isIncoming) - { - // Prompt user before presenting the call view controller - NSString *callPromptFormat = mxCall.isVideoCall ? NSLocalizedStringFromTable(@"call_incoming_video_prompt", @"Vector", nil) : NSLocalizedStringFromTable(@"call_incoming_voice_prompt", @"Vector", nil); - NSString *callerName = currentCallViewController.peer.displayname; - if (!callerName.length) - { - callerName = currentCallViewController.peer.userId; - } - NSString *callPrompt = [NSString stringWithFormat:callPromptFormat, callerName]; - - __weak typeof(self) weakSelf = self; - - // Removing existing notification (if any) - [_incomingCallNotification dismissViewControllerAnimated:NO completion:nil]; - - - - _incomingCallNotification = [UIAlertController alertControllerWithTitle:callPrompt - message:nil - preferredStyle:UIAlertControllerStyleAlert]; - - [_incomingCallNotification addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"decline", @"Vector", nil) - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - - // Reject the call. - // Note: Do not reset the incoming call notification before this operation, because it is used to release properly the dismissed call view controller. - if (self->currentCallViewController) - { - [self->currentCallViewController onButtonPressed:self->currentCallViewController.rejectCallButton]; - - currentCallViewController = nil; - } - - self.incomingCallNotification = nil; - - mxCall.delegate = nil; - } - - }]]; - - [_incomingCallNotification addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"accept", @"Vector", nil) - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - - self.incomingCallNotification = nil; - - if (self->currentCallViewController) - { - [self->currentCallViewController onButtonPressed:self->currentCallViewController.answerCallButton]; - - [self presentCallViewController:nil]; - } - } - - }]]; - - [_incomingCallNotification mxk_setAccessibilityIdentifier:@"AppDelegateIncomingCallAlert"]; - [self showNotificationAlert:_incomingCallNotification]; - } - else - { - [self presentCallViewController:nil]; - } + [self presentCallViewController:nil]; } - }]; } diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 6c73d198d..c246df64d 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -476,6 +476,8 @@ // Call "call_incoming_voice_prompt" = "Incoming voice call from %@"; "call_incoming_video_prompt" = "Incoming video call from %@"; +"call_incoming_voice" = "Incoming call..."; +"call_incoming_video" = "Incoming video call..."; "call_already_displayed" = "There is already a call in progress."; "call_jitsi_error" = "Failed to join the conference call."; diff --git a/Riot/ViewController/CallViewController.m b/Riot/ViewController/CallViewController.m index 830fa7281..558039dfc 100644 --- a/Riot/ViewController/CallViewController.m +++ b/Riot/ViewController/CallViewController.m @@ -27,6 +27,8 @@ #import "RiotNavigationController.h" +#import "IncomingCallView.h" + @interface CallViewController () { // Display a gradient view above the screen @@ -196,6 +198,37 @@ gradientMaskLayer = nil; } +- (UIView *)createIncomingCallView +{ + NSString *avatarThumbURL = [self.mainSession.matrixRestClient urlOfContentThumbnail:self.peer.avatarUrl + toFitViewSize:IncomingCallView.callerAvatarSize + withMethod:MXThumbnailingMethodCrop]; + + NSString *callInfo; + if (self.mxCall.isVideoCall) + callInfo = NSLocalizedStringFromTable(@"call_incoming_video", @"Vector", nil); + else + callInfo = NSLocalizedStringFromTable(@"call_incoming_voice", @"Vector", nil); + + IncomingCallView *incomingCallView = [[IncomingCallView alloc] initWithCallerAvatarURL:avatarThumbURL + 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; +} + #pragma mark - MXCallDelegate - (void)call:(MXCall *)call didEncounterError:(NSError *)error diff --git a/Riot/Views/Calls/CircleButton.h b/Riot/Views/Calls/CircleButton.h new file mode 100644 index 000000000..61780be30 --- /dev/null +++ b/Riot/Views/Calls/CircleButton.h @@ -0,0 +1,55 @@ +/* + Copyright 2017 Vector Creations Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface CircleButton : UIButton + +/** + Background color that is used for highlighted state + + By default the same as borderColor + */ +@property (nonatomic) UIColor *highlightBackgroundColor; + +/** + Background color that is used for normal state + + By default white + */ +@property (nonatomic) UIColor *defaultBackgroundColor; + +/** + Tint color that is used for highlighted state + + By default is white + */ +@property (nonatomic) UIColor *highlightTintColor; + +/** + Tint color that is used for normal state + + By default is the same as borderColor + */ +@property (nonatomic) UIColor *defaultTintColor; + +- (instancetype)initWithImage:(UIImage *)image borderColor:(UIColor *)borderColor; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Riot/Views/Calls/CircleButton.m b/Riot/Views/Calls/CircleButton.m new file mode 100644 index 000000000..b49b108dc --- /dev/null +++ b/Riot/Views/Calls/CircleButton.m @@ -0,0 +1,80 @@ +/* + Copyright 2017 Vector Creations Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import "CircleButton.h" + +@implementation CircleButton + +- (instancetype)initWithImage:(UIImage *)image borderColor:(UIColor *)borderColor +{ + self = [[super class] buttonWithType:UIButtonTypeCustom]; + + self.adjustsImageWhenDisabled = NO; + self.adjustsImageWhenHighlighted = NO; + + self.layer.borderWidth = 1.0; + self.layer.borderColor = borderColor.CGColor; + + self.defaultBackgroundColor = [UIColor whiteColor]; + self.highlightTintColor = [UIColor whiteColor]; + + self.highlightBackgroundColor = borderColor; + self.defaultTintColor = borderColor; + self.tintColor = borderColor; + + [self setImage:[image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] forState:UIControlStateNormal]; + + return self; +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + + self.layer.cornerRadius = CGRectGetWidth(self.bounds) / 2.0; +} + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +{ + [super touchesBegan:touches withEvent:event]; + [self animateBackgroundColor:self.highlightBackgroundColor tintColor:self.highlightTintColor]; +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +{ + [super touchesEnded:touches withEvent:event]; + [self animateBackgroundColor:self.defaultBackgroundColor tintColor:self.defaultTintColor]; +} + +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event +{ + [super touchesCancelled:touches withEvent:event]; + [self animateBackgroundColor:self.defaultBackgroundColor tintColor:self.defaultTintColor]; +} + +- (void)animateBackgroundColor:(UIColor *)color tintColor:(UIColor *)tintColor +{ + [UIView animateWithDuration:0.25 + delay:0.0 + options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction + animations:^{ + self.backgroundColor = color; + self.tintColor = tintColor; + } + completion:nil]; +} + +@end diff --git a/Riot/Views/Calls/IncomingCallView.h b/Riot/Views/Calls/IncomingCallView.h new file mode 100644 index 000000000..9b437c1ee --- /dev/null +++ b/Riot/Views/Calls/IncomingCallView.h @@ -0,0 +1,47 @@ +/* + Copyright 2017 Vector Creations Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef void (^IncomingCallViewAction)(); + +@interface IncomingCallView : UIView + +/** + Size that is applied to displayed user avatar + */ +@property (class, readonly) CGSize callerAvatarSize; + +/** + Block which is performed on call answer action + */ +@property (nonatomic, nullable, copy) IncomingCallViewAction onAnswer; + +/** + Block which is performed on call reject + */ +@property (nonatomic, nullable, copy) IncomingCallViewAction onReject; + +- (instancetype)initWithCallerAvatarURL:(NSString *)callerAvatarURL + placeholderImage:(UIImage *)placeholderImage + callerName:(NSString *)callerName + callInfo:(NSString *)callInfo; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Riot/Views/Calls/IncomingCallView.m b/Riot/Views/Calls/IncomingCallView.m new file mode 100644 index 000000000..88857cb2c --- /dev/null +++ b/Riot/Views/Calls/IncomingCallView.m @@ -0,0 +1,305 @@ +/* + Copyright 2017 Vector Creations Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import "IncomingCallView.h" + +#import +#import + +#import "CircleButton.h" + +static const CGFloat kAvatarSize = 100.0; +static const CGFloat kButtonSize = 80.0; + +@interface IncomingCallView () + +@property (nonatomic) MXKImageView *callerImageView; +@property (nonatomic) UILabel *callerNameLabel; +@property (nonatomic) UILabel *callInfoLabel; + +@property (nonatomic) CircleButton *answerButton; +@property (nonatomic) UILabel *answerTitleLabel; + +@property (nonatomic) CircleButton *rejectButton; +@property (nonatomic) UILabel *rejectTitleLabel; + +@end + +@implementation IncomingCallView + ++ (CGSize)callerAvatarSize +{ + return CGSizeMake(kAvatarSize, kAvatarSize); +} + +- (instancetype)initWithCallerAvatarURL:(NSString *)callerAvatarURL placeholderImage:(UIImage *)placeholderImage callerName:(NSString *)callerName callInfo:(NSString *)callInfo +{ + self = [super initWithFrame:CGRectZero]; + if (self) + { + self.backgroundColor = [UIColor whiteColor]; + self.opaque = YES; + + self.callerImageView = [[MXKImageView alloc] init]; + self.callerImageView.backgroundColor = [UIColor whiteColor]; + self.callerImageView.clipsToBounds = YES; + self.callerImageView.mediaFolder = kMXMediaManagerAvatarThumbnailFolder; + self.callerImageView.enableInMemoryCache = YES; + [self.callerImageView setImageURL:callerAvatarURL + withType:nil + andImageOrientation:UIImageOrientationUp + previewImage:placeholderImage]; + + self.callerNameLabel = [[UILabel alloc] init]; + self.callerNameLabel.backgroundColor = [UIColor whiteColor]; + self.callerNameLabel.textColor = [UIColor colorWithRed:60.0/255.0 green:60.0/255.0 blue:60.0/255.0 alpha:1.0]; + self.callerNameLabel.font = [UIFont systemFontOfSize:24.0 weight:UIFontWeightMedium]; + self.callerNameLabel.text = callerName; + + self.callInfoLabel = [[UILabel alloc] init]; + self.callInfoLabel.backgroundColor = [UIColor whiteColor]; + self.callInfoLabel.textColor = [UIColor colorWithRed:142.0/255.0 green:142.0/255.0 blue:147.0/255.0 alpha:1.0]; + self.callInfoLabel.font = [UIFont systemFontOfSize:18.0 weight:UIFontWeightRegular]; + self.callInfoLabel.text = callInfo; + + UIColor *answerButtonBorderColor = [UIColor colorWithRed:98.0/255.0 green:206.0/255.0 blue:156.0/255.0 alpha:1.0]; + + self.answerButton = [[CircleButton alloc] initWithImage:[UIImage imageNamed:@"voice_call_icon"] + borderColor:answerButtonBorderColor]; + [self.answerButton addTarget:self action:@selector(didTapAnswerButton) forControlEvents:UIControlEventTouchUpInside]; + + self.answerTitleLabel = [[UILabel alloc] init]; + self.answerTitleLabel.backgroundColor = [UIColor whiteColor]; + self.answerTitleLabel.textColor = answerButtonBorderColor; + self.answerTitleLabel.font = [UIFont systemFontOfSize:18.0 weight:UIFontWeightRegular]; + self.answerTitleLabel.text = NSLocalizedStringFromTable(@"accept", @"Vector", nil); + + UIColor *rejectButtonBorderColor = [UIColor colorWithRed:1.0 green:0.0 blue:100.0/255.0 alpha:1.0]; + + self.rejectButton = [[CircleButton alloc] initWithImage:[UIImage imageNamed:@"call_hangup_icon"] + borderColor:rejectButtonBorderColor]; + [self.rejectButton addTarget:self action:@selector(didTapRejectButton) forControlEvents:UIControlEventTouchUpInside]; + + self.rejectTitleLabel = [[UILabel alloc] init]; + self.rejectTitleLabel.backgroundColor = [UIColor whiteColor]; + self.rejectTitleLabel.textColor = rejectButtonBorderColor; + self.rejectTitleLabel.font = [UIFont systemFontOfSize:18.0 weight:UIFontWeightRegular]; + self.rejectTitleLabel.text = NSLocalizedStringFromTable(@"decline", @"Vector", nil); + + [self setupLayout]; + } + + return self; +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + + self.callerImageView.layer.cornerRadius = CGRectGetWidth(self.callerImageView.bounds) / 2.0; +} + +- (void)setupLayout +{ + NSArray *views = @[self.callerImageView, self.callerNameLabel, self.callInfoLabel, self.answerButton, self.answerTitleLabel, self.rejectButton, self.rejectTitleLabel]; + for (UIView *view in views) + { + view.translatesAutoresizingMaskIntoConstraints = NO; + [self addSubview:view]; + } + + [NSLayoutConstraint activateConstraints:@[ + [NSLayoutConstraint constraintWithItem:self.callerImageView + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:self + attribute:NSLayoutAttributeTop + multiplier:1.0 + constant:62.0], + + [NSLayoutConstraint constraintWithItem:self.callerImageView + attribute:NSLayoutAttributeCenterX + relatedBy:NSLayoutRelationEqual + toItem:self + attribute:NSLayoutAttributeCenterX + multiplier:1.0 + constant:0.0], + + [NSLayoutConstraint constraintWithItem:self.callerImageView + attribute:NSLayoutAttributeWidth + relatedBy:NSLayoutRelationEqual + toItem:nil + attribute:NSLayoutAttributeNotAnAttribute + multiplier:1.0 + constant:kAvatarSize], + + [NSLayoutConstraint constraintWithItem:self.callerImageView + attribute:NSLayoutAttributeHeight + relatedBy:NSLayoutRelationEqual + toItem:self.callerImageView + attribute:NSLayoutAttributeWidth + multiplier:1.0 + constant:0.0], + + [NSLayoutConstraint constraintWithItem:self.callerNameLabel + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:self.callerImageView + attribute:NSLayoutAttributeBottom + multiplier:1.0 + constant:18.0], + + [NSLayoutConstraint constraintWithItem:self.callerNameLabel + attribute:NSLayoutAttributeCenterX + relatedBy:NSLayoutRelationEqual + toItem:self + attribute:NSLayoutAttributeCenterX + multiplier:1.0 + constant:0.0], + + [NSLayoutConstraint constraintWithItem:self.callInfoLabel + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:self.callerNameLabel + attribute:NSLayoutAttributeBottom + multiplier:1.0 + constant:7.0], + + [NSLayoutConstraint constraintWithItem:self.callInfoLabel + attribute:NSLayoutAttributeCenterX + relatedBy:NSLayoutRelationEqual + toItem:self + attribute:NSLayoutAttributeCenterX + multiplier:1.0 + constant:0.0], + + [NSLayoutConstraint constraintWithItem:self.rejectButton + attribute:NSLayoutAttributeWidth + relatedBy:NSLayoutRelationEqual + toItem:nil + attribute:NSLayoutAttributeNotAnAttribute + multiplier:1.0 + constant:kButtonSize], + + [NSLayoutConstraint constraintWithItem:self.rejectButton + attribute:NSLayoutAttributeHeight + relatedBy:NSLayoutRelationEqual + toItem:self.rejectButton + attribute:NSLayoutAttributeWidth + multiplier:1.0 + constant:0.0], + + [NSLayoutConstraint constraintWithItem:self.rejectButton + attribute:NSLayoutAttributeTrailing + relatedBy:NSLayoutRelationEqual + toItem:self + attribute:NSLayoutAttributeCenterX + multiplier:1.0 + constant:-22.5], + + [NSLayoutConstraint constraintWithItem:self.rejectTitleLabel + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:self.rejectButton + attribute:NSLayoutAttributeBottom + multiplier:1.0 + constant:8.0], + + [NSLayoutConstraint constraintWithItem:self.rejectTitleLabel + attribute:NSLayoutAttributeCenterX + relatedBy:NSLayoutRelationEqual + toItem:self.rejectButton + attribute:NSLayoutAttributeCenterX + multiplier:1.0 + constant:0.0], + + [NSLayoutConstraint constraintWithItem:self.rejectTitleLabel + attribute:NSLayoutAttributeBottom + relatedBy:NSLayoutRelationEqual + toItem:self + attribute:NSLayoutAttributeBottom + multiplier:1.0 + constant:-16.0], + + [NSLayoutConstraint constraintWithItem:self.answerButton + attribute:NSLayoutAttributeWidth + relatedBy:NSLayoutRelationEqual + toItem:nil + attribute:NSLayoutAttributeNotAnAttribute + multiplier:1.0 + constant:kButtonSize], + + [NSLayoutConstraint constraintWithItem:self.answerButton + attribute:NSLayoutAttributeHeight + relatedBy:NSLayoutRelationEqual + toItem:self.answerButton + attribute:NSLayoutAttributeWidth + multiplier:1.0 + constant:0.0], + + [NSLayoutConstraint constraintWithItem:self.answerButton + attribute:NSLayoutAttributeLeading + relatedBy:NSLayoutRelationEqual + toItem:self + attribute:NSLayoutAttributeCenterX + multiplier:1.0 + constant:22.5], + + [NSLayoutConstraint constraintWithItem:self.answerTitleLabel + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:self.answerButton + attribute:NSLayoutAttributeBottom + multiplier:1.0 + constant:8.0], + + [NSLayoutConstraint constraintWithItem:self.answerTitleLabel + attribute:NSLayoutAttributeCenterX + relatedBy:NSLayoutRelationEqual + toItem:self.answerButton + attribute:NSLayoutAttributeCenterX + multiplier:1.0 + constant:0.0], + + [NSLayoutConstraint constraintWithItem:self.answerTitleLabel + attribute:NSLayoutAttributeBottom + relatedBy:NSLayoutRelationEqual + toItem:self + attribute:NSLayoutAttributeBottom + multiplier:1.0 + constant:-16.0] + ]]; +} + +// MARK: - Actions + +- (void)didTapAnswerButton +{ + if (self.onAnswer) + { + self.onAnswer(); + } +} + +- (void)didTapRejectButton +{ + if (self.onReject) + { + self.onReject(); + } +} + +@end