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:
parent
609e68e8bc
commit
cfbbeca7ac
|
@ -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<QuotedMessagePreviewViewDelegate> 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];
|
||||
|
||||
_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]];
|
||||
|
||||
// used for stripe and author
|
||||
// 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];
|
||||
[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];
|
||||
|
||||
[_bodyLabel autoPinHeightToSuperview];
|
||||
[_bodyLabel autoPinEdgeToSuperviewEdge:ALEdgeTrailing];
|
||||
[authorLabel autoPinTopToSuperviewMargin];
|
||||
[authorLabel autoPinLeadingToSuperviewMargin];
|
||||
|
||||
[contentContainer autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:_titleLabel];
|
||||
[contentContainer autoPinEdge:ALEdgeTrailing toEdge:ALEdgeLeading ofView:_cancelButton];
|
||||
[contentContainer autoPinEdgeToSuperviewEdge:ALEdgeBottom];
|
||||
[authorLabel autoPinEdge:ALEdgeTrailing toEdge:ALEdgeLeading ofView:cancelButton withOffset:-kCancelButtonMargin];
|
||||
[authorLabel setCompressionResistanceHigh];
|
||||
|
||||
[_cancelButton autoPinEdgeToSuperviewEdge:ALEdgeTop];
|
||||
[_cancelButton autoVCenterInSuperview];
|
||||
[contentContainer autoPinLeadingToSuperviewMargin];
|
||||
[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;
|
||||
}
|
||||
|
||||
// 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 () <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) 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
|
||||
|
|
|
@ -87,6 +87,7 @@
|
|||
#import <SignalServiceKit/TSGroupModel.h>
|
||||
#import <SignalServiceKit/TSInvalidIdentityKeyReceivingErrorMessage.h>
|
||||
#import <SignalServiceKit/TSNetworkManager.h>
|
||||
#import <SignalServiceKit/TSQuotedMessage.h>
|
||||
#import <SignalServiceKit/Threading.h>
|
||||
#import <YapDatabase/YapDatabase.h>
|
||||
#import <YapDatabase/YapDatabaseViewChange.h>
|
||||
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue