Refine appearance of quoted reply message cells.

This commit is contained in:
Matthew Chen 2018-04-04 14:42:39 -04:00
parent 10b4ade55a
commit 6171505657
10 changed files with 265 additions and 73 deletions

View file

@ -2,19 +2,17 @@
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWSBubbleView.h"
NS_ASSUME_NONNULL_BEGIN
@class OWSBubbleView;
@interface OWSBubbleStrokeView : UIView
@property (nonatomic, weak) OWSBubbleView *bubbleView;
@interface OWSBubbleStrokeView : UIView <OWSBubbleViewPartner>
@property (nonatomic) UIColor *strokeColor;
@property (nonatomic) CGFloat strokeThickness;
- (void)updateLayers;
@end
NS_ASSUME_NONNULL_END

View file

@ -12,6 +12,8 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic) CAShapeLayer *shapeLayer;
@property (nonatomic, weak) OWSBubbleView *bubbleView;
@end
#pragma mark -
@ -31,8 +33,6 @@ NS_ASSUME_NONNULL_BEGIN
self.shapeLayer = [CAShapeLayer new];
[self.layer addSublayer:self.shapeLayer];
[self updateLayers];
return self;
}
@ -56,7 +56,7 @@ NS_ASSUME_NONNULL_BEGIN
[super setFrame:frame];
if (didChange || !self.shapeLayer) {
if (didChange) {
[self updateLayers];
}
}
@ -67,7 +67,7 @@ NS_ASSUME_NONNULL_BEGIN
[super setBounds:bounds];
if (didChange || !self.shapeLayer) {
if (didChange) {
[self updateLayers];
}
}

View file

@ -2,6 +2,8 @@
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWSBubbleView.h"
NS_ASSUME_NONNULL_BEGIN
extern const CGFloat kOWSMessageCellCornerRadius;
@ -13,12 +15,20 @@ extern const CGFloat kBubbleThornVInset;
extern const CGFloat kBubbleTextHInset;
extern const CGFloat kBubbleTextVInset;
@class OWSBubbleStrokeView;
@class OWSBubbleView;
@protocol OWSBubbleViewPartner <NSObject>
- (void)updateLayers;
- (void)setBubbleView:(OWSBubbleView *)bubbleView;
@end
#pragma mark -
@interface OWSBubbleView : UIView
@property (nonatomic, weak, nullable) OWSBubbleStrokeView *bubbleStrokeView;
@property (nonatomic) BOOL isOutgoing;
@property (nonatomic) BOOL hideTail;
@property (nonatomic) BOOL isTruncated;
@ -27,6 +37,14 @@ extern const CGFloat kBubbleTextVInset;
- (UIBezierPath *)maskPath;
#pragma mark - Coordination
- (void)addPartnerView:(id<OWSBubbleViewPartner>)view;
- (void)clearPartnerViews;
- (void)updatePartnerViews;
@end
NS_ASSUME_NONNULL_END

View file

@ -3,7 +3,6 @@
//
#import "OWSBubbleView.h"
#import "OWSBubbleStrokeView.h"
#import <SignalMessaging/UIView+OWS.h>
NS_ASSUME_NONNULL_BEGIN
@ -22,6 +21,8 @@ const CGFloat kBubbleTextVInset = 10.f;
@property (nonatomic) CAShapeLayer *maskLayer;
@property (nonatomic) CAShapeLayer *shapeLayer;
@property (nonatomic, readonly) NSMutableArray<id<OWSBubbleViewPartner>> *partnerViews;
@end
#pragma mark -
@ -41,7 +42,7 @@ const CGFloat kBubbleTextVInset = 10.f;
self.maskLayer = [CAShapeLayer new];
self.layer.mask = self.maskLayer;
[self updateLayers];
_partnerViews = [NSMutableArray new];
return self;
}
@ -52,7 +53,7 @@ const CGFloat kBubbleTextVInset = 10.f;
_isOutgoing = isOutgoing;
if (didChange || !self.shapeLayer) {
if (didChange) {
[self updateLayers];
}
}
@ -63,7 +64,7 @@ const CGFloat kBubbleTextVInset = 10.f;
_hideTail = hideTail;
if (didChange || !self.shapeLayer) {
if (didChange) {
[self updateLayers];
}
}
@ -74,7 +75,7 @@ const CGFloat kBubbleTextVInset = 10.f;
_isTruncated = isTruncated;
if (didChange || !self.shapeLayer) {
if (didChange) {
[self updateLayers];
}
}
@ -87,13 +88,13 @@ const CGFloat kBubbleTextVInset = 10.f;
[super setFrame:frame];
if (didChangeSize || !self.shapeLayer) {
if (didChangeSize) {
[self updateLayers];
}
// We always need to inform the "bubble stroke view" (if any) if our
// frame/bounds/center changes. Its contents are not in local coordinates.
[self.bubbleStrokeView updateLayers];
[self updatePartnerViews];
}
- (void)setBounds:(CGRect)bounds
@ -104,13 +105,13 @@ const CGFloat kBubbleTextVInset = 10.f;
[super setBounds:bounds];
if (didChangeSize || !self.shapeLayer) {
if (didChangeSize) {
[self updateLayers];
}
// We always need to inform the "bubble stroke view" (if any) if our
// frame/bounds/center changes. Its contents are not in local coordinates.
[self.bubbleStrokeView updateLayers];
[self updatePartnerViews];
}
- (void)setCenter:(CGPoint)center
@ -119,7 +120,7 @@ const CGFloat kBubbleTextVInset = 10.f;
// We always need to inform the "bubble stroke view" (if any) if our
// frame/bounds/center changes. Its contents are not in local coordinates.
[self.bubbleStrokeView updateLayers];
[self updatePartnerViews];
}
- (void)setBubbleColor:(nullable UIColor *)bubbleColor
@ -211,6 +212,33 @@ const CGFloat kBubbleTextVInset = 10.f;
return bezierPath;
}
#pragma mark - Coordination
- (void)addPartnerView:(id<OWSBubbleViewPartner>)partnerView
{
OWSAssert(self.partnerViews);
[partnerView setBubbleView:self];
[self.partnerViews addObject:partnerView];
}
- (void)clearPartnerViews
{
OWSAssert(self.partnerViews);
[self.partnerViews removeAllObjects];
}
- (void)updatePartnerViews
{
[self layoutIfNeeded];
for (id<OWSBubbleViewPartner> partnerView in self.partnerViews) {
[partnerView updateLayers];
}
}
@end
NS_ASSUME_NONNULL_END

View file

@ -10,6 +10,8 @@ NS_ASSUME_NONNULL_BEGIN
+ (NSString *)cellReuseIdentifier;
+ (UIFont *)defaultTextMessageFont;
@end
NS_ASSUME_NONNULL_END

View file

@ -147,11 +147,16 @@ NS_ASSUME_NONNULL_BEGIN
return NSStringFromClass([self class]);
}
+ (UIFont *)defaultTextMessageFont
{
return [UIFont ows_dynamicTypeBodyFont];
}
- (UIFont *)textMessageFont
{
OWSAssert(DisplayableText.kMaxJumbomojiCount == 5);
CGFloat basePointSize = [UIFont ows_dynamicTypeBodyFont].pointSize;
CGFloat basePointSize = self.class.defaultTextMessageFont.pointSize;
switch (self.displayableBodyText.jumbomojiCount) {
case 0:
break;
@ -361,8 +366,10 @@ NS_ASSUME_NONNULL_BEGIN
if (self.isQuotedReply) {
OWSAssert(!lastSubview);
TSMessage *message = (TSMessage *)self.viewItem.interaction;
OWSQuotedMessageView *quotedMessageView =
[[OWSQuotedMessageView alloc] initWithViewItem:self.viewItem textMessageFont:self.textMessageFont];
[OWSQuotedMessageView quotedMessageViewForConversation:message.quotedMessage
displayableQuotedText:self.viewItem.displayableQuotedText];
[quotedMessageView createContents];
[self.bubbleView addSubview:quotedMessageView];
@ -381,6 +388,8 @@ NS_ASSUME_NONNULL_BEGIN
}
lastSubview = quotedMessageView;
bottomMargin = 0;
[self.bubbleView addPartnerView:quotedMessageView];
}
UIView *_Nullable bodyMediaView = nil;
@ -471,8 +480,8 @@ NS_ASSUME_NONNULL_BEGIN
[bubbleStrokeView autoPinEdge:ALEdgeBottom toEdge:ALEdgeBottom ofView:bodyMediaView];
[bubbleStrokeView autoPinEdge:ALEdgeLeft toEdge:ALEdgeLeft ofView:bodyMediaView];
[bubbleStrokeView autoPinEdge:ALEdgeRight toEdge:ALEdgeRight ofView:bodyMediaView];
self.bubbleView.bubbleStrokeView = bubbleStrokeView;
OWSAssert(self.bubbleView.bubbleStrokeView);
[self.bubbleView addPartnerView:bubbleStrokeView];
}
}
@ -1174,8 +1183,10 @@ NS_ASSUME_NONNULL_BEGIN
return CGSizeZero;
}
TSMessage *message = (TSMessage *)self.viewItem.interaction;
OWSQuotedMessageView *quotedMessageView =
[[OWSQuotedMessageView alloc] initWithViewItem:self.viewItem textMessageFont:self.textMessageFont];
[OWSQuotedMessageView quotedMessageViewForConversation:message.quotedMessage
displayableQuotedText:self.viewItem.displayableQuotedText];
const int maxMessageWidth = [self maxMessageWidthForContentWidth:contentWidth];
CGSize result = [quotedMessageView sizeForMaxWidth:maxMessageWidth - kBubbleThornSideInset];
result.width += kBubbleThornSideInset;
@ -1324,7 +1335,7 @@ NS_ASSUME_NONNULL_BEGIN
self.bubbleView.hidden = YES;
self.bubbleView.bubbleColor = nil;
self.bubbleView.bubbleStrokeView = nil;
[self.bubbleView clearPartnerViews];
for (UIView *subview in self.bubbleView.subviews) {
[subview removeFromSuperview];

View file

@ -2,25 +2,28 @@
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWSBubbleView.h"
NS_ASSUME_NONNULL_BEGIN
@class ConversationViewItem;
@class DisplayableText;
@class TSQuotedMessage;
@interface OWSQuotedMessageView : UIView
@interface OWSQuotedMessageView : UIView <OWSBubbleViewPartner>
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithViewItem:(ConversationViewItem *)viewItem
// quotedMessage:(TSQuotedMessage *)quotedMessage
textMessageFont:(UIFont *)textMessageFont;
// Only needs to be called if we're going to render this instance.
- (void)createContents;
// Measurement
- (CGSize)sizeForMaxWidth:(CGFloat)maxWidth;
+ (OWSQuotedMessageView *)quotedMessageViewForConversation:(TSQuotedMessage *)quotedMessage
displayableQuotedText:(nullable DisplayableText *)displayableQuotedText;
+ (OWSQuotedMessageView *)quotedMessageViewForPreview:(TSQuotedMessage *)quotedMessage;
@end
NS_ASSUME_NONNULL_END

View file

@ -5,6 +5,7 @@
#import "OWSQuotedMessageView.h"
#import "ConversationViewItem.h"
#import "Environment.h"
#import "OWSMessageCell.h"
#import "Signal-Swift.h"
#import <SignalMessaging/OWSContactsManager.h>
#import <SignalMessaging/SignalMessaging-Swift.h>
@ -18,16 +19,47 @@ NS_ASSUME_NONNULL_BEGIN
@interface OWSQuotedMessageView ()
@property (nonatomic, readonly) ConversationViewItem *viewItem;
@property (nonatomic, readonly) TSQuotedMessage *quotedMessage;
@property (nonatomic, nullable, readonly) DisplayableText *displayableQuotedText;
@property (nonatomic, readonly) UIFont *textMessageFont;
@property (nonatomic, readonly) UIColor *strokeColor;
@property (nonatomic, readonly) CGFloat strokeThickness;
// TODO: Replace with a bubble stroke view.
@property (nonatomic) CAShapeLayer *shapeLayer;
@property (nonatomic, weak) OWSBubbleView *bubbleView;
@end
@implementation OWSQuotedMessageView
- (instancetype)initWithViewItem:(ConversationViewItem *)viewItem
// quotedMessage:(TSQuotedMessage *)quotedMessage
textMessageFont:(UIFont *)textMessageFont
+ (OWSQuotedMessageView *)quotedMessageViewForConversation:(TSQuotedMessage *)quotedMessage
displayableQuotedText:(nullable DisplayableText *)displayableQuotedText
{
OWSAssert(quotedMessage);
return
[[OWSQuotedMessageView alloc] initWithQuotedMessage:quotedMessage displayableQuotedText:displayableQuotedText];
}
+ (OWSQuotedMessageView *)quotedMessageViewForPreview:(TSQuotedMessage *)quotedMessage
{
OWSAssert(quotedMessage);
DisplayableText *_Nullable displayableQuotedText = nil;
if (quotedMessage.body.length > 0) {
displayableQuotedText = [DisplayableText displayableText:quotedMessage.body];
}
return
[[OWSQuotedMessageView alloc] initWithQuotedMessage:quotedMessage displayableQuotedText:displayableQuotedText];
}
- (instancetype)initWithQuotedMessage:(TSQuotedMessage *)quotedMessage
displayableQuotedText:(nullable DisplayableText *)displayableQuotedText
{
self = [super init];
@ -35,37 +67,31 @@ NS_ASSUME_NONNULL_BEGIN
return self;
}
OWSAssert(viewItem);
// OWSAssert(quotedMessage);
OWSAssert(textMessageFont);
OWSAssert(quotedMessage);
OWSAssert(displayableQuotedText);
_viewItem = viewItem;
// _quotedMessage = quotedMessage;
_textMessageFont = textMessageFont;
_quotedMessage = quotedMessage;
_displayableQuotedText = displayableQuotedText;
_textMessageFont = OWSMessageCell.defaultTextMessageFont;
_strokeColor = OWSMessagesBubbleImageFactory.bubbleColorIncoming;
_strokeThickness = 1.f;
self.shapeLayer = [CAShapeLayer new];
[self.layer addSublayer:self.shapeLayer];
return self;
}
- (BOOL)isIncoming
{
return self.viewItem.interaction.interactionType == OWSInteractionType_IncomingMessage;
}
- (BOOL)hasQuotedAttachmentThumbnail
{
// This should always be valid for the appropriate cell types.
OWSAssert(self.viewItem);
return (self.viewItem.hasQuotedAttachment &&
[TSAttachmentStream hasThumbnailForMimeType:self.viewItem.quotedAttachmentMimetype]);
return (self.quotedMessage.contentType.length > 0 &&
[TSAttachmentStream hasThumbnailForMimeType:self.quotedMessage.contentType]);
}
#pragma mark -
- (void)createContents
{
OWSAssert(self.viewItem.isQuotedReply);
self.backgroundColor = [UIColor whiteColor];
self.userInteractionEnabled = NO;
self.layoutMargins = UIEdgeInsetsZero;
@ -91,14 +117,15 @@ NS_ASSUME_NONNULL_BEGIN
}
OWSContactsManager *contactsManager = Environment.current.contactsManager;
NSString *quotedAuthor = [contactsManager displayNameForPhoneIdentifier:self.viewItem.quotedRecipientId];
NSString *quotedAuthor = [contactsManager displayNameForPhoneIdentifier:self.quotedMessage.authorId];
UILabel *quotedAuthorLabel = [UILabel new];
{
quotedAuthorLabel.text = quotedAuthor;
quotedAuthorLabel.font = self.quotedAuthorFont;
quotedAuthorLabel.textColor
= (self.isIncoming ? [UIColor colorWithRGBHex:0xd84315] : [UIColor colorWithRGBHex:0x007884]);
// TODO:
quotedAuthorLabel.textColor = [UIColor ows_darkGrayColor];
// = (self.isIncoming ? [UIColor colorWithRGBHex:0xd84315] : [UIColor colorWithRGBHex:0x007884]);
quotedAuthorLabel.numberOfLines = 1;
quotedAuthorLabel.lineBreakMode = NSLineBreakByTruncatingTail;
[self addSubview:quotedAuthorLabel];
@ -135,8 +162,12 @@ NS_ASSUME_NONNULL_BEGIN
[stripeAndTextContainer setCompressionResistanceLow];
// Stripe.
BOOL isIncomingQuote
= ![NSObject isNullableObject:self.quotedMessage.authorId equalTo:TSAccountManager.localNumber];
UIColor *stripeColor = (isIncomingQuote ? OWSMessagesBubbleImageFactory.bubbleColorIncoming
: OWSMessagesBubbleImageFactory.bubbleColorOutgoingSent);
UIView *quoteStripView = [UIView containerView];
quoteStripView.backgroundColor = (self.isIncoming ? [UIColor whiteColor] : [UIColor colorWithRGBHex:0x007884]);
quoteStripView.backgroundColor = stripeColor;
quoteStripView.userInteractionEnabled = NO;
[stripeAndTextContainer addSubview:quoteStripView];
[quoteStripView autoPinHeightToSuperview];
@ -163,15 +194,8 @@ NS_ASSUME_NONNULL_BEGIN
// TODO: Class method?
- (CGSize)sizeForMaxWidth:(CGFloat)maxWidth
{
OWSAssert(self.viewItem);
OWSAssert([self.viewItem.interaction isKindOfClass:[TSMessage class]]);
CGSize result = CGSizeZero;
if (!self.viewItem.isQuotedReply) {
return result;
}
result.width += self.quotedContentHInset;
CGFloat thumbnailHeight = 0.f;
@ -192,7 +216,7 @@ NS_ASSUME_NONNULL_BEGIN
CGFloat maxQuotedAuthorWidth = maxWidth - result.width;
OWSContactsManager *contactsManager = Environment.current.contactsManager;
NSString *quotedAuthor = [contactsManager displayNameForPhoneIdentifier:self.viewItem.quotedRecipientId];
NSString *quotedAuthor = [contactsManager displayNameForPhoneIdentifier:self.quotedMessage.authorId];
UILabel *quotedAuthorLabel = [UILabel new];
quotedAuthorLabel.text = quotedAuthor;
@ -253,10 +277,10 @@ NS_ASSUME_NONNULL_BEGIN
- (NSString *)quotedSnippet
{
if (self.viewItem.hasQuotedText && self.viewItem.displayableQuotedText.displayText.length > 0) {
return self.viewItem.displayableQuotedText.displayText;
if (self.displayableQuotedText.displayText.length > 0) {
return self.displayableQuotedText.displayText;
} else {
NSString *mimeType = self.viewItem.quotedAttachmentMimetype;
NSString *mimeType = self.quotedMessage.contentType;
if (mimeType.length > 0) {
return [TSAttachment emojiForMimeType:mimeType];
@ -299,7 +323,7 @@ NS_ASSUME_NONNULL_BEGIN
// TODO:
- (CGFloat)quotedReplyStripeThickness
{
return 3.f;
return 2.f;
}
// TODO:
@ -342,6 +366,101 @@ NS_ASSUME_NONNULL_BEGIN
return 8.f;
}
#pragma mark - Stroke
//- (instancetype)init
//{
// self = [super init];
// if (!self) {
// return self;
// }
//
// self.opaque = NO;
// self.backgroundColor = [UIColor clearColor];
//
//
// [self updateLayers];
//
// return self;
//}
- (void)setStrokeColor:(UIColor *)strokeColor
{
_strokeColor = strokeColor;
[self updateLayers];
}
- (void)setStrokeThickness:(CGFloat)strokeThickness
{
_strokeThickness = strokeThickness;
[self updateLayers];
}
- (void)setFrame:(CGRect)frame
{
BOOL didChange = !CGRectEqualToRect(self.frame, frame);
[super setFrame:frame];
if (didChange) {
[self updateLayers];
}
}
- (void)setBounds:(CGRect)bounds
{
BOOL didChange = !CGRectEqualToRect(self.bounds, bounds);
[super setBounds:bounds];
if (didChange) {
[self updateLayers];
}
}
- (void)setCenter:(CGPoint)center
{
[super setCenter:center];
[self updateLayers];
}
- (void)updateLayers
{
if (!self.shapeLayer) {
return;
}
// Don't fill the shape layer; we just want a stroke around the border.
self.shapeLayer.fillColor = [UIColor clearColor].CGColor;
self.clipsToBounds = YES;
if (!self.bubbleView) {
return;
}
self.shapeLayer.strokeColor = self.strokeColor.CGColor;
self.shapeLayer.lineWidth = self.strokeThickness;
self.shapeLayer.zPosition = 100.f;
UIBezierPath *bezierPath = [UIBezierPath new];
UIBezierPath *boundsBezierPath = [UIBezierPath bezierPathWithRect:self.bounds];
[bezierPath appendPath:boundsBezierPath];
UIBezierPath *bubbleBezierPath = [self.bubbleView maskPath];
// We need to convert between coordinate systems using layers, not views.
CGPoint bubbleOffset = [self.layer convertPoint:CGPointZero fromLayer:self.bubbleView.layer];
CGAffineTransform transform = CGAffineTransformMakeTranslation(bubbleOffset.x, bubbleOffset.y);
[bubbleBezierPath applyTransform:transform];
[bezierPath appendPath:bubbleBezierPath];
self.shapeLayer.path = bezierPath.CGPath;
}
@end
NS_ASSUME_NONNULL_END

View file

@ -2299,6 +2299,14 @@ typedef enum : NSUInteger {
- (void)scrollDownButtonTapped
{
#ifdef DEBUG
CGPoint contentOffset = self.collectionView.contentOffset;
contentOffset.y += self.collectionView.height
- (self.collectionView.contentInset.top + self.collectionView.contentInset.bottom);
[self.collectionView setContentOffset:contentOffset animated:NO];
return;
#endif
NSIndexPath *indexPathOfUnreadMessagesIndicator = [self indexPathOfUnreadMessagesIndicator];
if (indexPathOfUnreadMessagesIndicator != nil) {
NSInteger unreadRow = indexPathOfUnreadMessagesIndicator.row;

View file

@ -9,7 +9,8 @@ import SignalServiceKit
@objc
public class OWSMessagesBubbleImageFactory: NSObject {
static let shared = OWSMessagesBubbleImageFactory()
@objc
public static let shared = OWSMessagesBubbleImageFactory()
private let jsqFactory = JSQMessagesBubbleImageFactory()!
@ -57,12 +58,16 @@ public class OWSMessagesBubbleImageFactory: NSObject {
}
}
@objc
public static let bubbleColorIncoming = UIColor.jsq_messageBubbleLightGray()!
@objc
public static let bubbleColorOutgoingUnsent = UIColor.gray
@objc
public static let bubbleColorOutgoingSending = UIColor.ows_fadedBlue
@objc
public static let bubbleColorOutgoingSent = UIColor.ows_materialBlue
public func bubbleColor(message: TSMessage) -> UIColor {