diff --git a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h index cdf49a0c2..5688f98d5 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h +++ b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h @@ -4,6 +4,8 @@ NS_ASSUME_NONNULL_BEGIN +@class SignalAttachment; + @protocol ConversationInputToolbarDelegate - (void)sendButtonPressed; @@ -20,6 +22,10 @@ NS_ASSUME_NONNULL_BEGIN - (void)textViewDidChange; +#pragma mark - Attachment Approval + +- (void)didApproveAttachment:(SignalAttachment *)attachment; + @end #pragma mark - @@ -51,6 +57,12 @@ NS_ASSUME_NONNULL_BEGIN - (void)cancelVoiceMemoIfNecessary; +#pragma mark - Attachment Approval + +- (void)showApprovalUIForAttachment:(SignalAttachment *)attachment; +- (void)viewWillAppear:(BOOL)animated; +- (void)viewWillDisappear:(BOOL)animated; + @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m index 821aa8e81..0edb477c7 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m @@ -4,6 +4,7 @@ #import "ConversationInputToolbar.h" #import "ConversationInputTextView.h" +#import "Signal-Swift.h" #import "UIColor+OWS.h" #import "UIFont+OWS.h" #import "UIView+OWS.h" @@ -16,6 +17,7 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex @interface ConversationInputToolbar () +@property (nonatomic, readonly) UIView *contentView; @property (nonatomic, readonly) ConversationInputTextView *inputTextView; @property (nonatomic, readonly) UIButton *attachmentButton; @property (nonatomic, readonly) UIButton *sendButton; @@ -37,6 +39,12 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex @property (nonatomic) BOOL isRecordingVoiceMemo; @property (nonatomic) CGPoint voiceMemoGestureStartLocation; +#pragma mark - Attachment Approval + +@property (nonatomic) UIView *attachmentApprovalView; +@property (nonatomic, nullable) MediaMessageView *attachmentView; +@property (nonatomic, nullable) SignalAttachment *attachmentToApprove; + @end #pragma mark - @@ -69,9 +77,13 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex [self addSubview:backgroundView]; [backgroundView autoPinEdgesToSuperviewEdges]; + _contentView = [UIView containerView]; + [self addSubview:self.contentView]; + [self.contentView autoPinEdgesToSuperviewEdges]; + _inputTextView = [ConversationInputTextView new]; self.inputTextView.textViewToolbarDelegate = self; - [self addSubview:self.inputTextView]; + [self.contentView addSubview:self.inputTextView]; // We want to be permissive about taps on the send and attachment buttons, // so we use wrapper views that capture nearby taps. This is a lot easier @@ -81,11 +93,11 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex _leftButtonWrapper = [UIView containerView]; [self.leftButtonWrapper addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(leftButtonTapped:)]]; - [self addSubview:self.leftButtonWrapper]; + [self.contentView addSubview:self.leftButtonWrapper]; _rightButtonWrapper = [UIView containerView]; [self.rightButtonWrapper addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(rightButtonTapped:)]]; - [self addSubview:self.rightButtonWrapper]; + [self.contentView addSubview:self.rightButtonWrapper]; _attachmentButton = [[UIButton alloc] init]; self.attachmentButton.accessibilityLabel @@ -118,6 +130,10 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex self.voiceMemoButton.imageView.tintColor = [UIColor ows_materialBlueColor]; [self.rightButtonWrapper addSubview:self.voiceMemoButton]; + _attachmentApprovalView = [UIView containerView]; + [self addSubview:self.attachmentApprovalView]; + [self.attachmentApprovalView autoPinToSuperviewEdges]; + // We want to be permissive about the voice message gesture, so we hang // the long press GR on the button's wrapper, not the button itself. UILongPressGestureRecognizer *longPressGestureRecognizer = @@ -192,6 +208,33 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex { [NSLayoutConstraint deactivateConstraints:self.contentContraints]; + if (self.attachmentToApprove) { + self.contentView.hidden = YES; + self.attachmentApprovalView.hidden = NO; + + self.contentContraints = @[ + [self.attachmentApprovalView autoSetDimension:ALDimensionHeight toSize:300.f], + ]; + + [self setNeedsLayout]; + [self layoutIfNeeded]; + + // Ensure the keyboard is dismissed. + // + // NOTE: We need to do this _last_ or the layout changes in the input toolbar + // will be inadvertently animated. + [self.inputTextView resignFirstResponder]; + + return; + } + + self.contentView.hidden = NO; + self.attachmentApprovalView.hidden = YES; + self.attachmentView = nil; + for (UIView *subview in self.attachmentApprovalView.subviews) { + [subview removeFromSuperview]; + } + const int textViewVInset = 5; const int contentHInset = 6; const int contentHSpacing = 6; @@ -626,6 +669,99 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex } } +#pragma mark - Attachment Approval + +- (void)showApprovalUIForAttachment:(SignalAttachment *)attachment +{ + OWSAssert(attachment); + + self.attachmentToApprove = attachment; + + MediaMessageView *attachmentView = [[MediaMessageView alloc] initWithAttachment:attachment]; + self.attachmentView = attachmentView; + [self.attachmentApprovalView addSubview:attachmentView]; + [attachmentView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:10]; + [attachmentView autoPinWidthToSuperviewWithMargin:20]; + + UIView *buttonRow = [UIView containerView]; + [self.attachmentApprovalView addSubview:buttonRow]; + [buttonRow autoPinWidthToSuperviewWithMargin:20]; + [buttonRow autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:attachmentView withOffset:10]; + [buttonRow autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:10]; + + // We use this invisible subview to ensure that the buttons are centered + // horizontally. + UIView *buttonSpacer = [UIView new]; + [buttonRow addSubview:buttonSpacer]; + // Vertical positioning of this view doesn't matter. + [buttonSpacer autoPinEdgeToSuperviewEdge:ALEdgeTop]; + [buttonSpacer autoSetDimension:ALDimensionWidth toSize:ScaleFromIPhone5To7Plus(20, 30)]; + [buttonSpacer autoSetDimension:ALDimensionHeight toSize:0]; + [buttonSpacer autoHCenterInSuperview]; + + UIView *cancelButton = [self createAttachmentApprovalButton:[CommonStrings cancelButton] + color:[UIColor ows_destructiveRedColor] + selector:@selector(attachmentApprovalCancelPressed)]; + [buttonRow addSubview:cancelButton]; + [cancelButton autoPinHeightToSuperview]; + [cancelButton autoPinEdge:ALEdgeRight toEdge:ALEdgeLeft ofView:buttonSpacer]; + + UIView *sendButton = + [self createAttachmentApprovalButton:NSLocalizedString( + @"ATTACHMENT_APPROVAL_SEND_BUTTON", comment + : @"Label for 'send' button in the 'attachment approval' dialog.") + color:[UIColor colorWithRGBHex:0x2ecc71] + selector:@selector(attachmentApprovalSendPressed)]; + [buttonRow addSubview:sendButton]; + [sendButton autoPinHeightToSuperview]; + [sendButton autoPinEdge:ALEdgeLeft toEdge:ALEdgeRight ofView:buttonSpacer]; + + [self ensureContentConstraints]; +} + +- (UIView *)createAttachmentApprovalButton:(NSString *)title color:(UIColor *)color selector:(SEL)selector +{ + const CGFloat buttonWidth = ScaleFromIPhone5To7Plus(110, 140); + const CGFloat buttonHeight = ScaleFromIPhone5To7Plus(35, 45); + + return [OWSFlatButton buttonWithTitle:title + titleColor:[UIColor whiteColor] + backgroundColor:color + width:buttonWidth + height:buttonHeight + target:self + selector:selector]; +} + +- (void)attachmentApprovalCancelPressed +{ + self.attachmentToApprove = nil; + + [self ensureContentConstraints]; +} + +- (void)attachmentApprovalSendPressed +{ + SignalAttachment *attachment = self.attachmentToApprove; + self.attachmentToApprove = nil; + + if (attachment) { + [self.inputToolbarDelegate didApproveAttachment:attachment]; + } + + [self ensureContentConstraints]; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [self.attachmentView viewWillAppear:animated]; +} + +- (void)viewWillDisappear:(BOOL)animated +{ + [self.attachmentView viewWillDisappear:animated]; +} + #pragma mark - Logging + (NSString *)logTag diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index e7d7f7c1a..2846bc765 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -555,6 +555,8 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { // or on another device. [self hideInputIfNeeded]; + [self.inputToolbar viewWillAppear:animated]; + self.isViewVisible = YES; // We should have already requested contact access at this point, so this should be a no-op @@ -986,6 +988,8 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { self.isViewVisible = NO; + [self.inputToolbar viewWillDisappear:animated]; + [self.audioAttachmentPlayer stop]; self.audioAttachmentPlayer = nil; @@ -3448,18 +3452,18 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { } else if (skipApprovalDialog) { [self sendMessageAttachment:attachment]; } else { - UIViewController *viewController = - [[AttachmentApprovalViewController alloc] initWithAttachment:attachment - successCompletion:^{ - [weakSelf sendMessageAttachment:attachment]; - }]; - UINavigationController *navigationController = - [[UINavigationController alloc] initWithRootViewController:viewController]; - [self.navigationController presentViewController:navigationController animated:YES completion:nil]; + [self.inputToolbar showApprovalUIForAttachment:attachment]; } }); } +- (void)didApproveAttachment:(SignalAttachment *)attachment +{ + OWSAssert(attachment); + + [self sendMessageAttachment:attachment]; +} + - (void)showErrorAlertForAttachment:(SignalAttachment *_Nullable)attachment { OWSAssert(attachment == nil || [attachment hasError]); diff --git a/Signal/src/ViewControllers/MediaMessageView.swift b/Signal/src/ViewControllers/MediaMessageView.swift index b0dffe597..4cc2fdc40 100644 --- a/Signal/src/ViewControllers/MediaMessageView.swift +++ b/Signal/src/ViewControllers/MediaMessageView.swift @@ -62,8 +62,6 @@ class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate { // MARK: - Create Views private func createViews() { - self.backgroundColor = UIColor.white - if attachment.isAnimatedImage { createAnimatedPreview() } else if attachment.isImage { @@ -89,15 +87,15 @@ class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate { subview.autoHCenterInSuperview() if lastView == nil { - subview.autoPinEdge(toSuperviewEdge:.top) + subview.autoPinEdge(toSuperviewEdge: .top) } else { - subview.autoPinEdge(.top, to:.bottom, of:lastView!, withOffset:10) + subview.autoPinEdge(.top, to: .bottom, of: lastView!, withOffset: 10) } lastView = subview } - lastView?.autoPinEdge(toSuperviewEdge:.bottom) + lastView?.autoPinEdge(toSuperviewEdge: .bottom) return stackView } @@ -117,10 +115,10 @@ class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate { setAudioIconToPlay() audioPlayButton.imageView?.layer.minificationFilter = kCAFilterTrilinear audioPlayButton.imageView?.layer.magnificationFilter = kCAFilterTrilinear - audioPlayButton.addTarget(self, action:#selector(audioPlayButtonPressed), for:.touchUpInside) + audioPlayButton.addTarget(self, action: #selector(audioPlayButtonPressed), for: .touchUpInside) let buttonSize = createHeroViewSize() - audioPlayButton.autoSetDimension(.width, toSize:buttonSize) - audioPlayButton.autoSetDimension(.height, toSize:buttonSize) + audioPlayButton.autoSetDimension(.width, toSize: buttonSize) + audioPlayButton.autoSetDimension(.height, toSize: buttonSize) subviews.append(audioPlayButton) let fileNameLabel = createFileNameLabel() @@ -136,7 +134,7 @@ class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate { updateAudioStatusLabel() subviews.append(audioStatusLabel) - let stackView = wrapViewsInVerticalStack(subviews:subviews) + let stackView = wrapViewsInVerticalStack(subviews: subviews) self.addSubview(stackView) fileNameLabel?.autoPinWidthToSuperview(withMargin: 32) stackView.autoPinWidthToSuperview() @@ -152,7 +150,7 @@ class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate { createGenericPreview() return } - guard let image = YYImage(contentsOfFile:dataUrl.path) else { + guard let image = YYImage(contentsOfFile: dataUrl.path) else { createGenericPreview() return } @@ -166,14 +164,14 @@ class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate { private func createImagePreview() { var image = attachment.image if image == nil { - image = UIImage(data:attachment.data) + image = UIImage(data: attachment.data) } guard image != nil else { createGenericPreview() return } - let imageView = UIImageView(image:image) + let imageView = UIImageView(image: image) imageView.layer.minificationFilter = kCAFilterTrilinear imageView.layer.magnificationFilter = kCAFilterTrilinear imageView.contentMode = .scaleAspectFit @@ -186,7 +184,7 @@ class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate { createGenericPreview() return } - guard let videoPlayer = MPMoviePlayerController(contentURL:dataUrl) else { + guard let videoPlayer = MPMoviePlayerController(contentURL: dataUrl) else { createGenericPreview() return } @@ -214,7 +212,7 @@ class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate { let fileSizeLabel = createFileSizeLabel() subviews.append(fileSizeLabel) - let stackView = wrapViewsInVerticalStack(subviews:subviews) + let stackView = wrapViewsInVerticalStack(subviews: subviews) self.addSubview(stackView) fileNameLabel?.autoPinWidthToSuperview(withMargin: 32) stackView.autoPinWidthToSuperview() @@ -227,9 +225,9 @@ class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate { private func createHeroImageView(imageName: String) -> UIView { let imageSize = createHeroViewSize() - let image = UIImage(named:imageName) + let image = UIImage(named: imageName) assert(image != nil) - let imageView = UIImageView(image:image) + let imageView = UIImageView(image: image) imageView.layer.minificationFilter = kCAFilterTrilinear imageView.layer.magnificationFilter = kCAFilterTrilinear imageView.layer.shadowColor = UIColor.black.cgColor @@ -237,14 +235,14 @@ class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate { imageView.layer.shadowRadius = CGFloat(2.0 * shadowScaling) imageView.layer.shadowOpacity = 0.25 imageView.layer.shadowOffset = CGSize(width: 0.75 * shadowScaling, height: 0.75 * shadowScaling) - imageView.autoSetDimension(.width, toSize:imageSize) - imageView.autoSetDimension(.height, toSize:imageSize) + imageView.autoSetDimension(.width, toSize: imageSize) + imageView.autoSetDimension(.height, toSize: imageSize) return imageView } private func labelFont() -> UIFont { - return UIFont.ows_regularFont(withSize:ScaleFromIPhone5To7Plus(18, 24)) + return UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(18, 24)) } private func formattedFileExtension() -> String? { @@ -252,7 +250,7 @@ class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate { return nil } - return String(format:NSLocalizedString("ATTACHMENT_APPROVAL_FILE_EXTENSION_FORMAT", + return String(format: NSLocalizedString("ATTACHMENT_APPROVAL_FILE_EXTENSION_FORMAT", comment: "Format string for file extension label in call interstitial view"), fileExtension.uppercased()) } @@ -287,7 +285,7 @@ class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate { private func createFileSizeLabel() -> UIView { let label = UILabel() let fileSize = attachment.dataLength - label.text = String(format:NSLocalizedString("ATTACHMENT_APPROVAL_FILE_SIZE_FORMAT", + label.text = String(format: NSLocalizedString("ATTACHMENT_APPROVAL_FILE_SIZE_FORMAT", comment: "Format string for file size label in call interstitial view. Embeds: {{file size as 'N mb' or 'N kb'}}."), ViewControllerUtils.formatFileSize(UInt(fileSize))) @@ -346,7 +344,7 @@ class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate { let isAudioPlaying = playbackState == .playing if isAudioPlaying && audioProgressSeconds > 0 && audioDurationSeconds > 0 { - audioStatusLabel.text = String(format:"%@ / %@", + audioStatusLabel.text = String(format: "%@ / %@", ViewControllerUtils.formatDurationSeconds(Int(round(self.audioProgressSeconds))), ViewControllerUtils.formatDurationSeconds(Int(round(self.audioDurationSeconds)))) } else { @@ -355,16 +353,16 @@ class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate { } private func setAudioIconToPlay() { - let image = UIImage(named:"audio_play_black_large")?.withRenderingMode(.alwaysTemplate) + let image = UIImage(named: "audio_play_black_large")?.withRenderingMode(.alwaysTemplate) assert(image != nil) - audioPlayButton?.setImage(image, for:.normal) + audioPlayButton?.setImage(image, for: .normal) audioPlayButton?.imageView?.tintColor = UIColor.ows_materialBlue() } private func setAudioIconToPause() { - let image = UIImage(named:"audio_pause_black_large")?.withRenderingMode(.alwaysTemplate) + let image = UIImage(named: "audio_pause_black_large")?.withRenderingMode(.alwaysTemplate) assert(image != nil) - audioPlayButton?.setImage(image, for:.normal) + audioPlayButton?.setImage(image, for: .normal) audioPlayButton?.imageView?.tintColor = UIColor.ows_materialBlue() } } diff --git a/Signal/src/ViewControllers/MessageMetadataViewController.swift b/Signal/src/ViewControllers/MessageMetadataViewController.swift index 617406292..1becce99b 100644 --- a/Signal/src/ViewControllers/MessageMetadataViewController.swift +++ b/Signal/src/ViewControllers/MessageMetadataViewController.swift @@ -383,6 +383,7 @@ class MessageMetadataViewController: OWSViewController { if let dataUTI = MIMETypeUtil.utiType(forMIMEType: contentType) { let attachment = SignalAttachment(dataSource: dataSource, dataUTI: dataUTI) let mediaMessageView = MediaMessageView(attachment: attachment) + mediaMessageView.backgroundColor = UIColor.white self.mediaMessageView = mediaMessageView rows.append(mediaMessageView) }