From cfbbeca7acc0b70edb6d0657c1c956b53c559190 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Tue, 3 Apr 2018 21:22:02 -0400 Subject: [PATCH] WIP: QuotedMessagePreviewView MVP - [] populate from menu - [] send quoted message TODO - [] thumbnail - [] paperclip icon showing for text message - [] cancel button asset - [] fonts - [] colors - [] adjust content inset/offset when showing quote edit NICE TO HAVE - [] animate presentation - [] animate dismiss - [] non-paperclip icon for generic attachments // FREEBIE --- .../ConversationInputToolbar.m | 195 ++++++++++++------ .../ConversationViewController.m | 16 ++ .../ViewControllers/DebugUI/DebugUIMessages.m | 8 +- .../src/Messages/OWSMessageUtils.h | 12 +- .../src/Messages/OWSMessageUtils.m | 31 ++- 5 files changed, 174 insertions(+), 88 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m index f0fa8c664..d23297bc5 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m @@ -21,22 +21,34 @@ NS_ASSUME_NONNULL_BEGIN static void *kConversationInputTextViewObservingContext = &kConversationInputTextViewObservingContext; static const CGFloat ConversationInputToolbarBorderViewHeight = 0.5; +@class QuotedMessagePreviewView; + +@protocol QuotedMessagePreviewViewDelegate + +- (void)quoteMessagePreviewViewDidPressCancel:(QuotedMessagePreviewView *)view; + +@end + @interface QuotedMessagePreviewView : UIView -@property (nonatomic, readonly) UILabel *titleLabel; -@property (nonatomic, readonly) UILabel *bodyLabel; -@property (nonatomic, readonly) UIImageView *iconView; -@property (nonatomic, readonly) UIButton *cancelButton; -@property (nonatomic, readonly) UIView *quoteStripe; +@property (nonatomic, weak) id delegate; @end @implementation QuotedMessagePreviewView -- (nullable UIImageView *)iconForMessage:(TSQuotedMessage *)message ++ (nullable UIView *)iconViewForMessage:(TSQuotedMessage *)message { - // FIXME TODO - return nil; + NSString *iconText = [TSAttachmentStream emojiForMimeType:message.contentType]; + if (!iconText) { + return nil; + } + + UILabel *iconLabel = [UILabel new]; + [iconLabel setContentHuggingHigh]; + iconLabel.text = iconText; + + return iconLabel; } - (instancetype)initWithQuotedMessage:(TSQuotedMessage *)message @@ -46,76 +58,112 @@ static const CGFloat ConversationInputToolbarBorderViewHeight = 0.5; return self; } - _titleLabel = [UILabel new]; - _titleLabel.text = [[Environment current].contactsManager displayNameForPhoneIdentifier:message.authorId]; + BOOL isQuotingSelf = [message.authorId isEqualToString:[TSAccountManager localNumber]]; + + // used for stripe and author + // FIXME actual colors TBD + UIColor *authorColor = isQuotingSelf ? [UIColor ows_materialBlueColor] : [UIColor blackColor]; - _bodyLabel = [UILabel new]; - _bodyLabel.text = message.body; + // used for text and cancel + UIColor *foregroundColor = UIColor.darkGrayColor; - _iconView = [self iconForMessage:message]; - if (_iconView) { - [self addSubview:_iconView]; - } + UILabel *authorLabel = [UILabel new]; + authorLabel.textColor = authorColor; + authorLabel.text = [[Environment current].contactsManager displayNameForPhoneIdentifier:message.authorId]; + authorLabel.font = UIFont.ows_dynamicTypeHeadlineFont; + + UILabel *bodyLabel = [UILabel new]; + bodyLabel.textColor = foregroundColor; + bodyLabel.font = UIFont.ows_footnoteFont; + bodyLabel.text = message.body; - _cancelButton = [UIButton buttonWithType:UIButtonTypeCustom]; + UIView *iconView = [self.class iconViewForMessage:message]; + + UIButton *cancelButton = [UIButton buttonWithType:UIButtonTypeCustom]; + // FIXME proper image asset/size UIImage *buttonImage = [[UIImage imageNamed:@"quoted-message-cancel"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; - [_cancelButton setImage:buttonImage forState:UIControlStateNormal]; - _cancelButton.imageView.tintColor = [UIColor ows_blackColor]; + [cancelButton setImage:buttonImage forState:UIControlStateNormal]; + cancelButton.imageView.tintColor = foregroundColor; + [cancelButton addTarget:self action:@selector(didTapCancel:) forControlEvents:UIControlEventTouchUpInside]; - _quoteStripe = [UIView new]; - BOOL isQuotingSelf = [message.authorId isEqualToString:[TSAccountManager localNumber]]; + UIView *quoteStripe = [UIView new]; - // FIXME actual colors TBD - _quoteStripe.backgroundColor = isQuotingSelf ? [UIColor orangeColor] : [UIColor blackColor]; + quoteStripe.backgroundColor = authorColor; - UIView *contentContainer = [UIView containerView]; + NSArray<__kindof UIView *> *contentViews = iconView ? @[ iconView, bodyLabel ] : @[ bodyLabel ]; + UIStackView *contentContainer = [[UIStackView alloc] initWithArrangedSubviews:contentViews]; + contentContainer.axis = UILayoutConstraintAxisHorizontal; + contentContainer.spacing = 4.0; - [self addSubview:_titleLabel]; + [self addSubview:authorLabel]; [self addSubview:contentContainer]; - [contentContainer addSubview:_bodyLabel]; - [self addSubview:_cancelButton]; - [self addSubview:_quoteStripe]; + [self addSubview:cancelButton]; + [self addSubview:quoteStripe]; // Layout - CGFloat kLeadingMargin = 4; + CGFloat kCancelButtonMargin = 4; + CGFloat kQuoteStripeWidth = 4; + CGFloat leadingMargin = kQuoteStripeWidth + 8; + CGFloat vMargin = 6; + CGFloat trailingMargin = 8; - [_quoteStripe autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsZero excludingEdge:ALEdgeRight]; - [_titleLabel autoPinEdgeToSuperviewEdge:ALEdgeTop]; - [_titleLabel autoPinEdge:ALEdgeLeading toEdge:ALEdgeTrailing ofView:_quoteStripe withOffset:kLeadingMargin]; - [_titleLabel autoPinEdge:ALEdgeTrailing toEdge:ALEdgeLeading ofView:_cancelButton]; + self.layoutMargins = UIEdgeInsetsMake(vMargin, leadingMargin, vMargin, trailingMargin); - if (_iconView) { - [contentContainer addSubview:_iconView]; - [_iconView autoPinEdgeToSuperviewEdge:ALEdgeLeading]; - [_iconView autoPinEdge:ALEdgeTrailing toEdge:ALEdgeTrailing ofView:_bodyLabel]; - [_iconView autoPinHeightToSuperview]; - } else { - [_bodyLabel autoPinEdge:ALEdgeLeading toEdge:ALEdgeTrailing ofView:_quoteStripe withOffset:kLeadingMargin]; - } + [quoteStripe autoPinEdgeToSuperviewEdge:ALEdgeLeading]; + [quoteStripe autoPinHeightToSuperview]; + [quoteStripe autoSetDimension:ALDimensionWidth toSize:kQuoteStripeWidth]; + + [authorLabel autoPinTopToSuperviewMargin]; + [authorLabel autoPinLeadingToSuperviewMargin]; + + [authorLabel autoPinEdge:ALEdgeTrailing toEdge:ALEdgeLeading ofView:cancelButton withOffset:-kCancelButtonMargin]; + [authorLabel setCompressionResistanceHigh]; - [_bodyLabel autoPinHeightToSuperview]; - [_bodyLabel autoPinEdgeToSuperviewEdge:ALEdgeTrailing]; + [contentContainer autoPinLeadingToSuperviewMargin]; + [contentContainer autoPinBottomToSuperviewMargin]; + [contentContainer autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:authorLabel]; + [contentContainer autoPinEdge:ALEdgeTrailing + toEdge:ALEdgeLeading + ofView:cancelButton + withOffset:-kCancelButtonMargin]; - [contentContainer autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:_titleLabel]; - [contentContainer autoPinEdge:ALEdgeTrailing toEdge:ALEdgeLeading ofView:_cancelButton]; - [contentContainer autoPinEdgeToSuperviewEdge:ALEdgeBottom]; + [cancelButton autoPinTrailingToSuperviewMargin]; + [cancelButton autoVCenterInSuperview]; + [cancelButton setContentHuggingHigh]; - [_cancelButton autoPinEdgeToSuperviewEdge:ALEdgeTop]; - [_cancelButton autoVCenterInSuperview]; + [cancelButton autoSetDimensionsToSize:CGSizeMake(40, 40)]; return self; } +// MARK: UIViewOverrides + +// Used by stack view to determin size. +- (CGSize)intrinsicContentSize +{ + return CGSizeMake(0, 30); +} + +// MARK: Actions + +- (void)didTapCancel:(id)sender +{ + [self.delegate quoteMessagePreviewViewDidPressCancel:self]; +} + @end #pragma mark - -@interface ConversationInputToolbar () +@interface ConversationInputToolbar () -@property (nonatomic, readonly) UIView *contentView; +@property (nonatomic, readonly) UIView *composeContainer; @property (nonatomic, readonly) ConversationInputTextView *inputTextView; +@property (nonatomic, readonly) UIStackView *contentStackView; @property (nonatomic, readonly) UIButton *attachmentButton; @property (nonatomic, readonly) UIButton *sendButton; @property (nonatomic, readonly) UIButton *voiceMemoButton; @@ -171,7 +219,12 @@ static const CGFloat ConversationInputToolbarBorderViewHeight = 0.5; { // Since we have `self.autoresizingMask = UIViewAutoresizingFlexibleHeight`, the intrinsicContentSize is used // to determine the height of the rendered inputAccessoryView. - CGSize newSize = CGSizeMake(self.bounds.size.width, self.toolbarHeight + ConversationInputToolbarBorderViewHeight); + CGFloat height = self.toolbarHeight + ConversationInputToolbarBorderViewHeight; + if (self.quotedMessageView) { + height += self.quotedMessageView.intrinsicContentSize.height; + } + CGSize newSize = CGSizeMake(self.bounds.size.width, height); + return newSize; } @@ -189,14 +242,17 @@ static const CGFloat ConversationInputToolbarBorderViewHeight = 0.5; [borderView autoPinEdgeToSuperviewEdge:ALEdgeTop]; [borderView autoSetDimension:ALDimensionHeight toSize:ConversationInputToolbarBorderViewHeight]; - _contentView = [UIView containerView]; - [self addSubview:self.contentView]; - [self.contentView autoPinEdgesToSuperviewEdges]; + _composeContainer = [UIView containerView]; + _contentStackView = [[UIStackView alloc] initWithArrangedSubviews:@[ _composeContainer ]]; + _contentStackView.axis = UILayoutConstraintAxisVertical; + + [self addSubview:_contentStackView]; + [_contentStackView autoPinEdgesToSuperviewEdges]; _inputTextView = [ConversationInputTextView new]; self.inputTextView.textViewToolbarDelegate = self; self.inputTextView.font = [UIFont ows_dynamicTypeBodyFont]; - [self.contentView addSubview:self.inputTextView]; + [self.composeContainer 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 @@ -206,11 +262,11 @@ static const CGFloat ConversationInputToolbarBorderViewHeight = 0.5; _leftButtonWrapper = [UIView containerView]; [self.leftButtonWrapper addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(leftButtonTapped:)]]; - [self.contentView addSubview:self.leftButtonWrapper]; + [self.composeContainer addSubview:self.leftButtonWrapper]; _rightButtonWrapper = [UIView containerView]; [self.rightButtonWrapper addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(rightButtonTapped:)]]; - [self.contentView addSubview:self.rightButtonWrapper]; + [self.composeContainer addSubview:self.rightButtonWrapper]; _attachmentButton = [[UIButton alloc] init]; self.attachmentButton.accessibilityLabel @@ -330,14 +386,26 @@ static const CGFloat ConversationInputToolbarBorderViewHeight = 0.5; - (void)setQuotedMessage:(TSQuotedMessage *)quotedMessage { - QuotedMessagePreviewView *quotedMessageView = - [[QuotedMessagePreviewView alloc] initWithQuotedMessage:quotedMessage]; + OWSAssert(self.quotedMessageView == nil); - [self ensureContentConstraints]; + // TODO update preview view with message in case we switch which message we're quoting. + if (quotedMessage) { + self.quotedMessageView = [[QuotedMessagePreviewView alloc] initWithQuotedMessage:quotedMessage]; + self.quotedMessageView.delegate = self; + } + + // TODO animate + [self.contentStackView insertArrangedSubview:self.quotedMessageView atIndex:0]; } - (void)clearQuotedMessage { + // TODO animate + if (self.quotedMessageView) { + [self.contentStackView removeArrangedSubview:self.quotedMessageView]; + [self.quotedMessageView removeFromSuperview]; + self.quotedMessageView = nil; + } } - (void)beginEditingTextMessage @@ -810,6 +878,13 @@ static const CGFloat ConversationInputToolbarBorderViewHeight = 0.5; } } +#pragma mark QuotedMessagePreviewViewDelegate + +- (void)quoteMessagePreviewViewDidPressCancel:(QuotedMessagePreviewView *)view +{ + [self clearQuotedMessage]; +} + @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 59b40b8eb..8bba6ce45 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -87,6 +87,7 @@ #import #import #import +#import #import #import #import @@ -1051,6 +1052,21 @@ typedef enum : NSUInteger { [self becomeFirstResponder]; } } + + // FIXME DO NOT COMMIT. Just for developing. + TSInteraction *lastInteraction = self.viewItems.lastObject.interaction; + if ([lastInteraction isKindOfClass:[TSMessage class]]) { + TSMessage *lastMessage = (TSMessage *)lastInteraction; + [self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) { + DDLogDebug(@"%@ setting quoted message: %llu", self.logTag, (unsigned long long)lastMessage.timestamp); + TSQuotedMessage *_Nullable quotedMessage = + [OWSMessageUtils quotedMessageForMessage:lastMessage transaction:transaction]; + [self.inputToolbar setQuotedMessage:quotedMessage]; + }]; + [self reloadInputViews]; + } else { + DDLogDebug(@"%@ not setting quoted message for message: %@", self.logTag, lastInteraction.class); + } } // `viewWillDisappear` is called whenever the view *starts* to disappear, diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m index 846d4b9dd..6b3c8aa68 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m @@ -1947,8 +1947,8 @@ isQuotedMessageAttachmentDownloaded:(BOOL)isQuotedMessageAttachmentDownloaded isAttachmentDownloaded:isQuotedMessageAttachmentDownloaded quotedMessage:nil transaction:transaction]; - quotedMessage = - [OWSMessageUtils quotedMessageForIncomingMessage:messageToQuote transaction:transaction]; + OWSAssert(messageToQuote); + quotedMessage = [OWSMessageUtils quotedMessageForMessage:messageToQuote transaction:transaction]; } else { TSOutgoingMessage *_Nullable messageToQuote = [self createFakeOutgoingMessage:thread messageBody:quotedMessageBody @@ -1958,8 +1958,8 @@ isQuotedMessageAttachmentDownloaded:(BOOL)isQuotedMessageAttachmentDownloaded isRead:quotedMessageIsRead quotedMessage:nil transaction:transaction]; - quotedMessage = [OWSMessageUtils quotedMessageForOutgoingMessage:(TSOutgoingMessage *)messageToQuote - transaction:transaction]; + OWSAssert(messageToQuote); + quotedMessage = [OWSMessageUtils quotedMessageForMessage:messageToQuote transaction:transaction]; } OWSAssert(quotedMessage); diff --git a/SignalServiceKit/src/Messages/OWSMessageUtils.h b/SignalServiceKit/src/Messages/OWSMessageUtils.h index bca37b88e..1150e3a70 100644 --- a/SignalServiceKit/src/Messages/OWSMessageUtils.h +++ b/SignalServiceKit/src/Messages/OWSMessageUtils.h @@ -4,11 +4,10 @@ NS_ASSUME_NONNULL_BEGIN -@class TSIncomingMessage; -@class TSOutgoingMessage; +@class TSMessage; @class TSQuotedMessage; @class TSThread; -@class YapDatabaseReadWriteTransaction; +@class YapDatabaseReadTransaction; @interface OWSMessageUtils : NSObject @@ -21,11 +20,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)updateApplicationBadgeCount; -+ (nullable TSQuotedMessage *)quotedMessageForIncomingMessage:(TSIncomingMessage *)message - transaction:(YapDatabaseReadWriteTransaction *)transaction; - -+ (nullable TSQuotedMessage *)quotedMessageForOutgoingMessage:(TSOutgoingMessage *)message - transaction:(YapDatabaseReadWriteTransaction *)transaction; ++ (nullable TSQuotedMessage *)quotedMessageForMessage:(TSMessage *)message + transaction:(YapDatabaseReadTransaction *)transaction; @end diff --git a/SignalServiceKit/src/Messages/OWSMessageUtils.m b/SignalServiceKit/src/Messages/OWSMessageUtils.m index 9ad041534..db91ddeb6 100644 --- a/SignalServiceKit/src/Messages/OWSMessageUtils.m +++ b/SignalServiceKit/src/Messages/OWSMessageUtils.m @@ -102,28 +102,27 @@ NS_ASSUME_NONNULL_BEGIN return numberOfItems; } -+ (nullable TSQuotedMessage *)quotedMessageForIncomingMessage:(TSIncomingMessage *)message - transaction:(YapDatabaseReadWriteTransaction *)transaction ++ (nullable TSQuotedMessage *)quotedMessageForMessage:(TSMessage *)message + transaction:(YapDatabaseReadTransaction *)transaction; { OWSAssert(message); OWSAssert(transaction); - return [self quotedMessageForMessage:message - authorId:message.authorId - thread:[message threadWithTransaction:transaction] - transaction:transaction]; -} + TSThread *thread = [message threadWithTransaction:transaction]; -+ (nullable TSQuotedMessage *)quotedMessageForOutgoingMessage:(TSOutgoingMessage *)message - transaction:(YapDatabaseReadWriteTransaction *)transaction -{ - OWSAssert(message); - OWSAssert(transaction); + NSString *_Nullable authorId = ^{ + if ([message isKindOfClass:[TSOutgoingMessage class]]) { + return [TSAccountManager localNumber]; + } else if ([message isKindOfClass:[TSIncomingMessage class]]) { + return [(TSIncomingMessage *)message authorId]; + } else { + OWSFail(@"%@ Unexpected message type: %@", self.logTag, message.class); + return (NSString * _Nullable) nil; + } + }(); + OWSAssert(authorId.length > 0); - return [self quotedMessageForMessage:message - authorId:TSAccountManager.localNumber - thread:[message threadWithTransaction:transaction] - transaction:transaction]; + return [self quotedMessageForMessage:message authorId:authorId thread:thread transaction:transaction]; } + (nullable TSQuotedMessage *)quotedMessageForMessage:(TSMessage *)message