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
This commit is contained in:
Michael Kirk 2018-04-03 21:22:02 -04:00
parent 609e68e8bc
commit cfbbeca7ac
5 changed files with 178 additions and 92 deletions

View File

@ -21,22 +21,34 @@ NS_ASSUME_NONNULL_BEGIN
static void *kConversationInputTextViewObservingContext = &kConversationInputTextViewObservingContext; static void *kConversationInputTextViewObservingContext = &kConversationInputTextViewObservingContext;
static const CGFloat ConversationInputToolbarBorderViewHeight = 0.5; static const CGFloat ConversationInputToolbarBorderViewHeight = 0.5;
@class QuotedMessagePreviewView;
@protocol QuotedMessagePreviewViewDelegate
- (void)quoteMessagePreviewViewDidPressCancel:(QuotedMessagePreviewView *)view;
@end
@interface QuotedMessagePreviewView : UIView @interface QuotedMessagePreviewView : UIView
@property (nonatomic, readonly) UILabel *titleLabel; @property (nonatomic, weak) id<QuotedMessagePreviewViewDelegate> delegate;
@property (nonatomic, readonly) UILabel *bodyLabel;
@property (nonatomic, readonly) UIImageView *iconView;
@property (nonatomic, readonly) UIButton *cancelButton;
@property (nonatomic, readonly) UIView *quoteStripe;
@end @end
@implementation QuotedMessagePreviewView @implementation QuotedMessagePreviewView
- (nullable UIImageView *)iconForMessage:(TSQuotedMessage *)message + (nullable UIView *)iconViewForMessage:(TSQuotedMessage *)message
{ {
// FIXME TODO NSString *iconText = [TSAttachmentStream emojiForMimeType:message.contentType];
return nil; if (!iconText) {
return nil;
}
UILabel *iconLabel = [UILabel new];
[iconLabel setContentHuggingHigh];
iconLabel.text = iconText;
return iconLabel;
} }
- (instancetype)initWithQuotedMessage:(TSQuotedMessage *)message - (instancetype)initWithQuotedMessage:(TSQuotedMessage *)message
@ -46,76 +58,112 @@ static const CGFloat ConversationInputToolbarBorderViewHeight = 0.5;
return self; return self;
} }
_titleLabel = [UILabel new];
_titleLabel.text = [[Environment current].contactsManager displayNameForPhoneIdentifier:message.authorId];
_bodyLabel = [UILabel new];
_bodyLabel.text = message.body;
_iconView = [self iconForMessage:message];
if (_iconView) {
[self addSubview:_iconView];
}
_cancelButton = [UIButton buttonWithType:UIButtonTypeCustom];
UIImage *buttonImage =
[[UIImage imageNamed:@"quoted-message-cancel"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
[_cancelButton setImage:buttonImage forState:UIControlStateNormal];
_cancelButton.imageView.tintColor = [UIColor ows_blackColor];
_quoteStripe = [UIView new];
BOOL isQuotingSelf = [message.authorId isEqualToString:[TSAccountManager localNumber]]; BOOL isQuotingSelf = [message.authorId isEqualToString:[TSAccountManager localNumber]];
// used for stripe and author
// FIXME actual colors TBD // FIXME actual colors TBD
_quoteStripe.backgroundColor = isQuotingSelf ? [UIColor orangeColor] : [UIColor blackColor]; UIColor *authorColor = isQuotingSelf ? [UIColor ows_materialBlueColor] : [UIColor blackColor];
UIView *contentContainer = [UIView containerView]; // used for text and cancel
UIColor *foregroundColor = UIColor.darkGrayColor;
[self addSubview:_titleLabel]; 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;
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 = foregroundColor;
[cancelButton addTarget:self action:@selector(didTapCancel:) forControlEvents:UIControlEventTouchUpInside];
UIView *quoteStripe = [UIView new];
quoteStripe.backgroundColor = authorColor;
NSArray<__kindof UIView *> *contentViews = iconView ? @[ iconView, bodyLabel ] : @[ bodyLabel ];
UIStackView *contentContainer = [[UIStackView alloc] initWithArrangedSubviews:contentViews];
contentContainer.axis = UILayoutConstraintAxisHorizontal;
contentContainer.spacing = 4.0;
[self addSubview:authorLabel];
[self addSubview:contentContainer]; [self addSubview:contentContainer];
[contentContainer addSubview:_bodyLabel]; [self addSubview:cancelButton];
[self addSubview:_cancelButton]; [self addSubview:quoteStripe];
[self addSubview:_quoteStripe];
// Layout // 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]; self.layoutMargins = UIEdgeInsetsMake(vMargin, leadingMargin, vMargin, trailingMargin);
[_titleLabel autoPinEdgeToSuperviewEdge:ALEdgeTop];
[_titleLabel autoPinEdge:ALEdgeLeading toEdge:ALEdgeTrailing ofView:_quoteStripe withOffset:kLeadingMargin];
[_titleLabel autoPinEdge:ALEdgeTrailing toEdge:ALEdgeLeading ofView:_cancelButton];
if (_iconView) { [quoteStripe autoPinEdgeToSuperviewEdge:ALEdgeLeading];
[contentContainer addSubview:_iconView]; [quoteStripe autoPinHeightToSuperview];
[_iconView autoPinEdgeToSuperviewEdge:ALEdgeLeading]; [quoteStripe autoSetDimension:ALDimensionWidth toSize:kQuoteStripeWidth];
[_iconView autoPinEdge:ALEdgeTrailing toEdge:ALEdgeTrailing ofView:_bodyLabel];
[_iconView autoPinHeightToSuperview];
} else {
[_bodyLabel autoPinEdge:ALEdgeLeading toEdge:ALEdgeTrailing ofView:_quoteStripe withOffset:kLeadingMargin];
}
[_bodyLabel autoPinHeightToSuperview]; [authorLabel autoPinTopToSuperviewMargin];
[_bodyLabel autoPinEdgeToSuperviewEdge:ALEdgeTrailing]; [authorLabel autoPinLeadingToSuperviewMargin];
[contentContainer autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:_titleLabel]; [authorLabel autoPinEdge:ALEdgeTrailing toEdge:ALEdgeLeading ofView:cancelButton withOffset:-kCancelButtonMargin];
[contentContainer autoPinEdge:ALEdgeTrailing toEdge:ALEdgeLeading ofView:_cancelButton]; [authorLabel setCompressionResistanceHigh];
[contentContainer autoPinEdgeToSuperviewEdge:ALEdgeBottom];
[_cancelButton autoPinEdgeToSuperviewEdge:ALEdgeTop]; [contentContainer autoPinLeadingToSuperviewMargin];
[_cancelButton autoVCenterInSuperview]; [contentContainer autoPinBottomToSuperviewMargin];
[contentContainer autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:authorLabel];
[contentContainer autoPinEdge:ALEdgeTrailing
toEdge:ALEdgeLeading
ofView:cancelButton
withOffset:-kCancelButtonMargin];
[cancelButton autoPinTrailingToSuperviewMargin];
[cancelButton autoVCenterInSuperview];
[cancelButton setContentHuggingHigh];
[cancelButton autoSetDimensionsToSize:CGSizeMake(40, 40)];
return self; 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 @end
#pragma mark - #pragma mark -
@interface ConversationInputToolbar () <UIGestureRecognizerDelegate, ConversationTextViewToolbarDelegate> @interface ConversationInputToolbar () <UIGestureRecognizerDelegate,
ConversationTextViewToolbarDelegate,
QuotedMessagePreviewViewDelegate>
@property (nonatomic, readonly) UIView *contentView; @property (nonatomic, readonly) UIView *composeContainer;
@property (nonatomic, readonly) ConversationInputTextView *inputTextView; @property (nonatomic, readonly) ConversationInputTextView *inputTextView;
@property (nonatomic, readonly) UIStackView *contentStackView;
@property (nonatomic, readonly) UIButton *attachmentButton; @property (nonatomic, readonly) UIButton *attachmentButton;
@property (nonatomic, readonly) UIButton *sendButton; @property (nonatomic, readonly) UIButton *sendButton;
@property (nonatomic, readonly) UIButton *voiceMemoButton; @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 // Since we have `self.autoresizingMask = UIViewAutoresizingFlexibleHeight`, the intrinsicContentSize is used
// to determine the height of the rendered inputAccessoryView. // 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; return newSize;
} }
@ -189,14 +242,17 @@ static const CGFloat ConversationInputToolbarBorderViewHeight = 0.5;
[borderView autoPinEdgeToSuperviewEdge:ALEdgeTop]; [borderView autoPinEdgeToSuperviewEdge:ALEdgeTop];
[borderView autoSetDimension:ALDimensionHeight toSize:ConversationInputToolbarBorderViewHeight]; [borderView autoSetDimension:ALDimensionHeight toSize:ConversationInputToolbarBorderViewHeight];
_contentView = [UIView containerView]; _composeContainer = [UIView containerView];
[self addSubview:self.contentView]; _contentStackView = [[UIStackView alloc] initWithArrangedSubviews:@[ _composeContainer ]];
[self.contentView autoPinEdgesToSuperviewEdges]; _contentStackView.axis = UILayoutConstraintAxisVertical;
[self addSubview:_contentStackView];
[_contentStackView autoPinEdgesToSuperviewEdges];
_inputTextView = [ConversationInputTextView new]; _inputTextView = [ConversationInputTextView new];
self.inputTextView.textViewToolbarDelegate = self; self.inputTextView.textViewToolbarDelegate = self;
self.inputTextView.font = [UIFont ows_dynamicTypeBodyFont]; 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, // 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 // 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]; _leftButtonWrapper = [UIView containerView];
[self.leftButtonWrapper [self.leftButtonWrapper
addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(leftButtonTapped:)]]; addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(leftButtonTapped:)]];
[self.contentView addSubview:self.leftButtonWrapper]; [self.composeContainer addSubview:self.leftButtonWrapper];
_rightButtonWrapper = [UIView containerView]; _rightButtonWrapper = [UIView containerView];
[self.rightButtonWrapper [self.rightButtonWrapper
addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(rightButtonTapped:)]]; addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(rightButtonTapped:)]];
[self.contentView addSubview:self.rightButtonWrapper]; [self.composeContainer addSubview:self.rightButtonWrapper];
_attachmentButton = [[UIButton alloc] init]; _attachmentButton = [[UIButton alloc] init];
self.attachmentButton.accessibilityLabel self.attachmentButton.accessibilityLabel
@ -330,14 +386,26 @@ static const CGFloat ConversationInputToolbarBorderViewHeight = 0.5;
- (void)setQuotedMessage:(TSQuotedMessage *)quotedMessage - (void)setQuotedMessage:(TSQuotedMessage *)quotedMessage
{ {
QuotedMessagePreviewView *quotedMessageView = OWSAssert(self.quotedMessageView == nil);
[[QuotedMessagePreviewView alloc] initWithQuotedMessage:quotedMessage];
[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 - (void)clearQuotedMessage
{ {
// TODO animate
if (self.quotedMessageView) {
[self.contentStackView removeArrangedSubview:self.quotedMessageView];
[self.quotedMessageView removeFromSuperview];
self.quotedMessageView = nil;
}
} }
- (void)beginEditingTextMessage - (void)beginEditingTextMessage
@ -810,6 +878,13 @@ static const CGFloat ConversationInputToolbarBorderViewHeight = 0.5;
} }
} }
#pragma mark QuotedMessagePreviewViewDelegate
- (void)quoteMessagePreviewViewDidPressCancel:(QuotedMessagePreviewView *)view
{
[self clearQuotedMessage];
}
@end @end
NS_ASSUME_NONNULL_END NS_ASSUME_NONNULL_END

View File

@ -87,6 +87,7 @@
#import <SignalServiceKit/TSGroupModel.h> #import <SignalServiceKit/TSGroupModel.h>
#import <SignalServiceKit/TSInvalidIdentityKeyReceivingErrorMessage.h> #import <SignalServiceKit/TSInvalidIdentityKeyReceivingErrorMessage.h>
#import <SignalServiceKit/TSNetworkManager.h> #import <SignalServiceKit/TSNetworkManager.h>
#import <SignalServiceKit/TSQuotedMessage.h>
#import <SignalServiceKit/Threading.h> #import <SignalServiceKit/Threading.h>
#import <YapDatabase/YapDatabase.h> #import <YapDatabase/YapDatabase.h>
#import <YapDatabase/YapDatabaseViewChange.h> #import <YapDatabase/YapDatabaseViewChange.h>
@ -1051,6 +1052,21 @@ typedef enum : NSUInteger {
[self becomeFirstResponder]; [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, // `viewWillDisappear` is called whenever the view *starts* to disappear,

View File

@ -1947,8 +1947,8 @@ isQuotedMessageAttachmentDownloaded:(BOOL)isQuotedMessageAttachmentDownloaded
isAttachmentDownloaded:isQuotedMessageAttachmentDownloaded isAttachmentDownloaded:isQuotedMessageAttachmentDownloaded
quotedMessage:nil quotedMessage:nil
transaction:transaction]; transaction:transaction];
quotedMessage = OWSAssert(messageToQuote);
[OWSMessageUtils quotedMessageForIncomingMessage:messageToQuote transaction:transaction]; quotedMessage = [OWSMessageUtils quotedMessageForMessage:messageToQuote transaction:transaction];
} else { } else {
TSOutgoingMessage *_Nullable messageToQuote = [self createFakeOutgoingMessage:thread TSOutgoingMessage *_Nullable messageToQuote = [self createFakeOutgoingMessage:thread
messageBody:quotedMessageBody messageBody:quotedMessageBody
@ -1958,8 +1958,8 @@ isQuotedMessageAttachmentDownloaded:(BOOL)isQuotedMessageAttachmentDownloaded
isRead:quotedMessageIsRead isRead:quotedMessageIsRead
quotedMessage:nil quotedMessage:nil
transaction:transaction]; transaction:transaction];
quotedMessage = [OWSMessageUtils quotedMessageForOutgoingMessage:(TSOutgoingMessage *)messageToQuote OWSAssert(messageToQuote);
transaction:transaction]; quotedMessage = [OWSMessageUtils quotedMessageForMessage:messageToQuote transaction:transaction];
} }
OWSAssert(quotedMessage); OWSAssert(quotedMessage);

View File

@ -4,11 +4,10 @@
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
@class TSIncomingMessage; @class TSMessage;
@class TSOutgoingMessage;
@class TSQuotedMessage; @class TSQuotedMessage;
@class TSThread; @class TSThread;
@class YapDatabaseReadWriteTransaction; @class YapDatabaseReadTransaction;
@interface OWSMessageUtils : NSObject @interface OWSMessageUtils : NSObject
@ -21,11 +20,8 @@ NS_ASSUME_NONNULL_BEGIN
- (void)updateApplicationBadgeCount; - (void)updateApplicationBadgeCount;
+ (nullable TSQuotedMessage *)quotedMessageForIncomingMessage:(TSIncomingMessage *)message + (nullable TSQuotedMessage *)quotedMessageForMessage:(TSMessage *)message
transaction:(YapDatabaseReadWriteTransaction *)transaction; transaction:(YapDatabaseReadTransaction *)transaction;
+ (nullable TSQuotedMessage *)quotedMessageForOutgoingMessage:(TSOutgoingMessage *)message
transaction:(YapDatabaseReadWriteTransaction *)transaction;
@end @end

View File

@ -102,28 +102,27 @@ NS_ASSUME_NONNULL_BEGIN
return numberOfItems; return numberOfItems;
} }
+ (nullable TSQuotedMessage *)quotedMessageForIncomingMessage:(TSIncomingMessage *)message + (nullable TSQuotedMessage *)quotedMessageForMessage:(TSMessage *)message
transaction:(YapDatabaseReadWriteTransaction *)transaction transaction:(YapDatabaseReadTransaction *)transaction;
{ {
OWSAssert(message); OWSAssert(message);
OWSAssert(transaction); OWSAssert(transaction);
return [self quotedMessageForMessage:message TSThread *thread = [message threadWithTransaction:transaction];
authorId:message.authorId
thread:[message threadWithTransaction:transaction]
transaction:transaction];
}
+ (nullable TSQuotedMessage *)quotedMessageForOutgoingMessage:(TSOutgoingMessage *)message NSString *_Nullable authorId = ^{
transaction:(YapDatabaseReadWriteTransaction *)transaction if ([message isKindOfClass:[TSOutgoingMessage class]]) {
{ return [TSAccountManager localNumber];
OWSAssert(message); } else if ([message isKindOfClass:[TSIncomingMessage class]]) {
OWSAssert(transaction); 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 return [self quotedMessageForMessage:message authorId:authorId thread:thread transaction:transaction];
authorId:TSAccountManager.localNumber
thread:[message threadWithTransaction:transaction]
transaction:transaction];
} }
+ (nullable TSQuotedMessage *)quotedMessageForMessage:(TSMessage *)message + (nullable TSQuotedMessage *)quotedMessageForMessage:(TSMessage *)message