Respond to CR.

pull/1/head
Matthew Chen 7 years ago
parent 31f062ed11
commit f98c45603c

@ -1,5 +1,5 @@
// //
// Copyright (c) 2017 Open Whisper Systems. All rights reserved. // Copyright (c) 2018 Open Whisper Systems. All rights reserved.
// //
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
@ -19,7 +19,6 @@ typedef void (^AttachmentStateBlock)(BOOL isAttachmentReady);
@interface AttachmentUploadView : UIView @interface AttachmentUploadView : UIView
- (instancetype)initWithAttachment:(TSAttachmentStream *)attachment - (instancetype)initWithAttachment:(TSAttachmentStream *)attachment
superview:(UIView *)superview
attachmentStateCallback:(AttachmentStateBlock _Nullable)attachmentStateCallback; attachmentStateCallback:(AttachmentStateBlock _Nullable)attachmentStateCallback;
@end @end

@ -1,5 +1,5 @@
// //
// Copyright (c) 2017 Open Whisper Systems. All rights reserved. // Copyright (c) 2018 Open Whisper Systems. All rights reserved.
// //
#import "AttachmentUploadView.h" #import "AttachmentUploadView.h"
@ -32,21 +32,16 @@ NS_ASSUME_NONNULL_BEGIN
@implementation AttachmentUploadView @implementation AttachmentUploadView
- (instancetype)initWithAttachment:(TSAttachmentStream *)attachment - (instancetype)initWithAttachment:(TSAttachmentStream *)attachment
superview:(UIView *)superview
attachmentStateCallback:(AttachmentStateBlock _Nullable)attachmentStateCallback attachmentStateCallback:(AttachmentStateBlock _Nullable)attachmentStateCallback
{ {
self = [super init]; self = [super init];
if (self) { if (self) {
OWSAssert(attachment); OWSAssert(attachment);
OWSAssert(superview);
self.attachment = attachment; self.attachment = attachment;
self.attachmentStateCallback = attachmentStateCallback; self.attachmentStateCallback = attachmentStateCallback;
[superview addSubview:self];
[self autoPinToSuperviewEdges];
_bezierPathView = [OWSBezierPathView new]; _bezierPathView = [OWSBezierPathView new];
self.bezierPathView.configureShapeLayerBlock = ^(CAShapeLayer *layer, CGRect bounds) { self.bezierPathView.configureShapeLayerBlock = ^(CAShapeLayer *layer, CGRect bounds) {
layer.path = [UIBezierPath bezierPathWithRect:bounds].CGPath; layer.path = [UIBezierPath bezierPathWithRect:bounds].CGPath;

@ -28,6 +28,9 @@ NS_ASSUME_NONNULL_BEGIN
self.opaque = NO; self.opaque = NO;
self.backgroundColor = [UIColor clearColor]; self.backgroundColor = [UIColor clearColor];
self.shapeLayer = [CAShapeLayer new];
[self.layer addSublayer:self.shapeLayer];
return self; return self;
} }
@ -47,7 +50,7 @@ NS_ASSUME_NONNULL_BEGIN
- (void)setFrame:(CGRect)frame - (void)setFrame:(CGRect)frame
{ {
BOOL didChange = !CGSizeEqualToSize(self.frame.size, frame.size); BOOL didChange = !CGRectEqualToRect(self.frame, frame);
[super setFrame:frame]; [super setFrame:frame];
@ -58,7 +61,7 @@ NS_ASSUME_NONNULL_BEGIN
- (void)setBounds:(CGRect)bounds - (void)setBounds:(CGRect)bounds
{ {
BOOL didChange = !CGSizeEqualToSize(self.bounds.size, bounds.size); BOOL didChange = !CGRectEqualToRect(self.bounds, bounds);
[super setBounds:bounds]; [super setBounds:bounds];
@ -76,10 +79,7 @@ NS_ASSUME_NONNULL_BEGIN
- (void)updateLayers - (void)updateLayers
{ {
if (!self.shapeLayer) { OWSAssert(self.shapeLayer);
self.shapeLayer = [CAShapeLayer new];
[self.layer addSublayer:self.shapeLayer];
}
// Don't fill the shape layer; we just want a stroke around the border. // Don't fill the shape layer; we just want a stroke around the border.
self.shapeLayer.fillColor = [UIColor clearColor].CGColor; self.shapeLayer.fillColor = [UIColor clearColor].CGColor;

@ -8,7 +8,6 @@
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
// This approximates the curve of our message bubbles, which makes the animation feel a little smoother.
const CGFloat kOWSMessageCellCornerRadius = 17; const CGFloat kOWSMessageCellCornerRadius = 17;
const CGFloat kBubbleVRounding = kOWSMessageCellCornerRadius; const CGFloat kBubbleVRounding = kOWSMessageCellCornerRadius;
@ -29,6 +28,22 @@ const CGFloat kBubbleTextVInset = 10.f;
@implementation OWSBubbleView @implementation OWSBubbleView
- (instancetype)init
{
self = [super init];
if (!self) {
return self;
}
self.shapeLayer = [CAShapeLayer new];
[self.layer addSublayer:self.shapeLayer];
self.maskLayer = [CAShapeLayer new];
self.layer.mask = self.maskLayer;
return self;
}
- (void)setIsOutgoing:(BOOL)isOutgoing - (void)setIsOutgoing:(BOOL)isOutgoing
{ {
BOOL didChange = _isOutgoing != isOutgoing; BOOL didChange = _isOutgoing != isOutgoing;
@ -64,29 +79,35 @@ const CGFloat kBubbleTextVInset = 10.f;
- (void)setFrame:(CGRect)frame - (void)setFrame:(CGRect)frame
{ {
BOOL didChange = !CGSizeEqualToSize(self.frame.size, frame.size); // We only need to update our layers if the _size_ of this view
// changes since the contents of the layers are in local coordinates.
BOOL didChangeSize = !CGSizeEqualToSize(self.frame.size, frame.size);
[super setFrame:frame]; [super setFrame:frame];
if (didChange || !self.shapeLayer) { if (didChangeSize || !self.shapeLayer) {
[self updateLayers]; [self updateLayers];
} }
// We need to inform the "bubble stroke view" (if any) any time our frame changes. // 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.bubbleStrokeView updateLayers];
} }
- (void)setBounds:(CGRect)bounds - (void)setBounds:(CGRect)bounds
{ {
BOOL didChange = !CGSizeEqualToSize(self.bounds.size, bounds.size); // We only need to update our layers if the _size_ of this view
// changes since the contents of the layers are in local coordinates.
BOOL didChangeSize = !CGSizeEqualToSize(self.bounds.size, bounds.size);
[super setBounds:bounds]; [super setBounds:bounds];
if (didChange || !self.shapeLayer) { if (didChangeSize || !self.shapeLayer) {
[self updateLayers]; [self updateLayers];
} }
// We need to inform the "bubble stroke view" (if any) any time our frame changes. // 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.bubbleStrokeView updateLayers];
} }
@ -94,7 +115,8 @@ const CGFloat kBubbleTextVInset = 10.f;
{ {
[super setCenter:center]; [super setCenter:center];
// We need to inform the "bubble stroke view" (if any) any time our frame changes. // 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.bubbleStrokeView updateLayers];
} }
@ -110,20 +132,13 @@ const CGFloat kBubbleTextVInset = 10.f;
- (void)updateLayers - (void)updateLayers
{ {
if (!self.shapeLayer) { OWSAssert(self.maskLayer);
self.shapeLayer = [CAShapeLayer new]; OWSAssert(self.shapeLayer);
[self.layer addSublayer:self.shapeLayer];
}
if (!self.maskLayer) {
self.maskLayer = [CAShapeLayer new];
self.layer.mask = self.maskLayer;
}
UIBezierPath *bezierPath = [self maskPath]; UIBezierPath *bezierPath = [self maskPath];
self.shapeLayer.fillColor = self.bubbleColor.CGColor; self.shapeLayer.fillColor = self.bubbleColor.CGColor;
self.shapeLayer.path = bezierPath.CGPath; self.shapeLayer.path = bezierPath.CGPath;
self.maskLayer.path = bezierPath.CGPath; self.maskLayer.path = bezierPath.CGPath;
} }

@ -42,7 +42,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size)
@property (nonatomic) OWSBubbleView *bubbleView; @property (nonatomic) OWSBubbleView *bubbleView;
@property (nonatomic) UILabel *dateHeaderLabel; @property (nonatomic) UILabel *dateHeaderLabel;
@property (nonatomic) OWSMessageTextView *bodyTextViewCached; @property (nonatomic) OWSMessageTextView *bodyTextView;
@property (nonatomic, nullable) UIImageView *failedSendBadgeView; @property (nonatomic, nullable) UIImageView *failedSendBadgeView;
@property (nonatomic) UIView *footerView; @property (nonatomic) UIView *footerView;
@property (nonatomic) UILabel *footerLabel; @property (nonatomic) UILabel *footerLabel;
@ -75,7 +75,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size)
- (void)commontInit - (void)commontInit
{ {
OWSAssert(!self.bodyTextViewCached); OWSAssert(!self.bodyTextView);
_viewConstraints = [NSMutableArray new]; _viewConstraints = [NSMutableArray new];
@ -95,7 +95,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size)
self.dateHeaderLabel.textColor = [UIColor lightGrayColor]; self.dateHeaderLabel.textColor = [UIColor lightGrayColor];
[self.contentView addSubview:self.dateHeaderLabel]; [self.contentView addSubview:self.dateHeaderLabel];
self.bodyTextViewCached = [self newTextView]; self.bodyTextView = [self newTextView];
self.footerLabel = [UILabel new]; self.footerLabel = [UILabel new];
self.footerLabel.font = [UIFont ows_regularFontWithSize:12.f]; self.footerLabel.font = [UIFont ows_regularFontWithSize:12.f];
@ -103,7 +103,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size)
[self.footerView addSubview:self.footerLabel]; [self.footerView addSubview:self.footerLabel];
// Hide these views by default. // Hide these views by default.
self.bodyTextViewCached.hidden = YES; self.bodyTextView.hidden = YES;
self.dateHeaderLabel.hidden = YES; self.dateHeaderLabel.hidden = YES;
self.footerLabel.hidden = YES; self.footerLabel.hidden = YES;
@ -140,6 +140,8 @@ CG_INLINE CGSize CGSizeCeil(CGSize size)
textView.contentInset = UIEdgeInsetsZero; textView.contentInset = UIEdgeInsetsZero;
textView.textContainer.lineFragmentPadding = 0; textView.textContainer.lineFragmentPadding = 0;
textView.scrollEnabled = NO; textView.scrollEnabled = NO;
// Setting dataDetectorTypes is expensive. Do it just once.
textView.dataDetectorTypes = (UIDataDetectorTypeLink | UIDataDetectorTypeAddress | UIDataDetectorTypeCalendarEvent);
return textView; return textView;
} }
@ -401,6 +403,9 @@ CG_INLINE CGSize CGSizeCeil(CGSize size)
[bodyMediaView autoPinLeadingToSuperviewWithMargin:0], [bodyMediaView autoPinLeadingToSuperviewWithMargin:0],
[bodyMediaView autoPinTrailingToSuperviewWithMargin:0], [bodyMediaView autoPinTrailingToSuperviewWithMargin:0],
]]; ]];
// We need constraints to control the vertical sizing of media and text views, but we use
// lower priority so that when a message only contains media it uses the exact bounds of
// the message view.
[NSLayoutConstraint [NSLayoutConstraint
autoSetPriority:UILayoutPriorityDefaultLow autoSetPriority:UILayoutPriorityDefaultLow
forConstraints:^{ forConstraints:^{
@ -446,6 +451,9 @@ CG_INLINE CGSize CGSizeCeil(CGSize size)
[bodyTextView autoPinLeadingToSuperviewWithMargin:self.textLeadingMargin], [bodyTextView autoPinLeadingToSuperviewWithMargin:self.textLeadingMargin],
[bodyTextView autoPinTrailingToSuperviewWithMargin:self.textTrailingMargin], [bodyTextView autoPinTrailingToSuperviewWithMargin:self.textTrailingMargin],
]]; ]];
// We need constraints to control the vertical sizing of media and text views, but we use
// lower priority so that when a message only contains media it uses the exact bounds of
// the message view.
[NSLayoutConstraint [NSLayoutConstraint
autoSetPriority:UILayoutPriorityDefaultLow autoSetPriority:UILayoutPriorityDefaultLow
forConstraints:^{ forConstraints:^{
@ -523,7 +531,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size)
DDLogError(@"%@ Failed to load cell media: %@", [self logTag], [self.attachmentStream mediaURL]); DDLogError(@"%@ Failed to load cell media: %@", [self logTag], [self.attachmentStream mediaURL]);
self.viewItem.didCellMediaFailToLoad = YES; self.viewItem.didCellMediaFailToLoad = YES;
// TODO: Do we need to hide/remove the media view? // TODO: Do we need to hide/remove the media view?
[self showAttachmentErrorView:mediaView]; [self showAttachmentErrorViewWithMediaView:mediaView];
} }
return cellMedia; return cellMedia;
} }
@ -721,12 +729,12 @@ CG_INLINE CGSize CGSizeCeil(CGSize size)
TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.viewItem.interaction; TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.viewItem.interaction;
shouldIgnoreEvents = outgoingMessage.messageState != TSOutgoingMessageStateSentToService; shouldIgnoreEvents = outgoingMessage.messageState != TSOutgoingMessageStateSentToService;
} }
[self.class loadForTextDisplay:self.bodyTextViewCached [self.class loadForTextDisplay:self.bodyTextView
text:self.displayableBodyText.displayText text:self.displayableBodyText.displayText
textColor:self.textColor textColor:self.textColor
font:self.textMessageFont font:self.textMessageFont
shouldIgnoreEvents:shouldIgnoreEvents]; shouldIgnoreEvents:shouldIgnoreEvents];
return self.bodyTextViewCached; return self.bodyTextView;
} }
+ (void)loadForTextDisplay:(OWSMessageTextView *)textView + (void)loadForTextDisplay:(OWSMessageTextView *)textView
@ -745,7 +753,6 @@ CG_INLINE CGSize CGSizeCeil(CGSize size)
NSForegroundColorAttributeName : textColor, NSForegroundColorAttributeName : textColor,
NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle | NSUnderlinePatternSolid) NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle | NSUnderlinePatternSolid)
}; };
textView.dataDetectorTypes = (UIDataDetectorTypeLink | UIDataDetectorTypeAddress | UIDataDetectorTypeCalendarEvent);
textView.shouldIgnoreEvents = shouldIgnoreEvents; textView.shouldIgnoreEvents = shouldIgnoreEvents;
} }
@ -797,10 +804,13 @@ CG_INLINE CGSize CGSizeCeil(CGSize size)
if (!strongSelf) { if (!strongSelf) {
return; return;
} }
OWSCAssert(strongSelf.lastBodyMediaView == stillImageView);
if (stillImageView.image) { if (stillImageView.image) {
return; return;
} }
// Don't cache large still images. // Don't cache large still images.
//
// TODO: Don't use full size images in the message cells.
const NSUInteger kMaxCachableSize = 1024 * 1024; const NSUInteger kMaxCachableSize = 1024 * 1024;
BOOL shouldSkipCache = BOOL shouldSkipCache =
[OWSFileSystem fileSizeOfPath:strongSelf.attachmentStream.filePath].unsignedIntegerValue < kMaxCachableSize; [OWSFileSystem fileSizeOfPath:strongSelf.attachmentStream.filePath].unsignedIntegerValue < kMaxCachableSize;
@ -813,6 +823,11 @@ CG_INLINE CGSize CGSizeCeil(CGSize size)
shouldSkipCache:shouldSkipCache]; shouldSkipCache:shouldSkipCache];
}; };
self.unloadCellContentBlock = ^{ self.unloadCellContentBlock = ^{
OWSMessageCell *strongSelf = weakSelf;
if (!strongSelf) {
return;
}
OWSCAssert(strongSelf.lastBodyMediaView == stillImageView);
stillImageView.image = nil; stillImageView.image = nil;
}; };
@ -836,6 +851,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size)
if (!strongSelf) { if (!strongSelf) {
return; return;
} }
OWSCAssert(strongSelf.lastBodyMediaView == animatedImageView);
if (animatedImageView.image) { if (animatedImageView.image) {
return; return;
} }
@ -854,6 +870,11 @@ CG_INLINE CGSize CGSizeCeil(CGSize size)
shouldSkipCache:NO]; shouldSkipCache:NO];
}; };
self.unloadCellContentBlock = ^{ self.unloadCellContentBlock = ^{
OWSMessageCell *strongSelf = weakSelf;
if (!strongSelf) {
return;
}
OWSCAssert(strongSelf.lastBodyMediaView == animatedImageView);
animatedImageView.image = nil; animatedImageView.image = nil;
}; };
@ -911,6 +932,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size)
if (!strongSelf) { if (!strongSelf) {
return; return;
} }
OWSCAssert(strongSelf.lastBodyMediaView == stillImageView);
if (stillImageView.image) { if (stillImageView.image) {
return; return;
} }
@ -924,6 +946,11 @@ CG_INLINE CGSize CGSizeCeil(CGSize size)
shouldSkipCache:NO]; shouldSkipCache:NO];
}; };
self.unloadCellContentBlock = ^{ self.unloadCellContentBlock = ^{
OWSMessageCell *strongSelf = weakSelf;
if (!strongSelf) {
return;
}
OWSCAssert(strongSelf.lastBodyMediaView == stillImageView);
stillImageView.image = nil; stillImageView.image = nil;
}; };
@ -999,26 +1026,25 @@ CG_INLINE CGSize CGSizeCeil(CGSize size)
if (self.isOutgoing) { if (self.isOutgoing) {
if (!self.attachmentStream.isUploaded) { if (!self.attachmentStream.isUploaded) {
__unused AttachmentUploadView *attachmentUploadView = AttachmentUploadView *attachmentUploadView =
// self.attachmentUploadView =
// This view will be added to attachmentView which will retain a strong reference to it.
[[AttachmentUploadView alloc] initWithAttachment:self.attachmentStream [[AttachmentUploadView alloc] initWithAttachment:self.attachmentStream
superview:attachmentView
attachmentStateCallback:attachmentStateCallback]; attachmentStateCallback:attachmentStateCallback];
[attachmentView addSubview:attachmentUploadView];
[attachmentUploadView autoPinToSuperviewEdges];
} }
} }
} }
- (void)showAttachmentErrorView:(UIView *)mediaView - (void)showAttachmentErrorViewWithMediaView:(UIView *)mediaView
{ {
OWSAssert(mediaView); OWSAssert(mediaView);
// TODO: We could do a better job of indicating that the media could not be loaded. // TODO: We could do a better job of indicating that the media could not be loaded.
UIView *customView = [UIView new]; UIView *errorView = [UIView new];
customView.backgroundColor = [UIColor colorWithWhite:0.85f alpha:1.f]; errorView.backgroundColor = [UIColor colorWithWhite:0.85f alpha:1.f];
customView.userInteractionEnabled = NO; errorView.userInteractionEnabled = NO;
[mediaView addSubview:customView]; [mediaView addSubview:errorView];
[customView autoPinEdgesToSuperviewEdges]; [errorView autoPinEdgesToSuperviewEdges];
} }
#pragma mark - Measurement #pragma mark - Measurement
@ -1037,10 +1063,10 @@ CG_INLINE CGSize CGSizeCeil(CGSize size)
const int maxMessageWidth = [self maxMessageWidthForContentWidth:contentWidth]; const int maxMessageWidth = [self maxMessageWidthForContentWidth:contentWidth];
const int maxTextWidth = (int)floor(maxMessageWidth - (leftMargin + rightMargin)); const int maxTextWidth = (int)floor(maxMessageWidth - (leftMargin + rightMargin));
self.bodyTextViewCached.text = self.displayableBodyText.displayText; self.bodyTextView.text = self.displayableBodyText.displayText;
// Honor dynamic type in the message bodies. // Honor dynamic type in the message bodies.
self.bodyTextViewCached.font = [self textMessageFont]; self.bodyTextView.font = [self textMessageFont];
CGSize textSize = CGSizeCeil([self.bodyTextViewCached sizeThatFits:CGSizeMake(maxTextWidth, CGFLOAT_MAX)]); CGSize textSize = CGSizeCeil([self.bodyTextView sizeThatFits:CGSizeMake(maxTextWidth, CGFLOAT_MAX)]);
CGSize textViewSize = textSize; CGSize textViewSize = textSize;
if (includeMargins) { if (includeMargins) {
@ -1213,10 +1239,9 @@ CG_INLINE CGSize CGSizeCeil(CGSize size)
self.dateHeaderLabel.text = nil; self.dateHeaderLabel.text = nil;
self.dateHeaderLabel.hidden = YES; self.dateHeaderLabel.hidden = YES;
[self.bodyTextViewCached removeFromSuperview]; [self.bodyTextView removeFromSuperview];
self.bodyTextViewCached.text = nil; self.bodyTextView.text = nil;
self.bodyTextViewCached.hidden = YES; self.bodyTextView.hidden = YES;
self.bodyTextViewCached.dataDetectorTypes = UIDataDetectorTypeNone;
[self.failedSendBadgeView removeFromSuperview]; [self.failedSendBadgeView removeFromSuperview];
self.failedSendBadgeView = nil; self.failedSendBadgeView = nil;
self.footerLabel.text = nil; self.footerLabel.text = nil;

@ -20,22 +20,22 @@ public class OWSMessagesBubbleImageFactory: NSObject {
}() }()
public lazy var incoming: JSQMessagesBubbleImage = { public lazy var incoming: JSQMessagesBubbleImage = {
let color = UIColor.jsq_messageBubbleLightGray()! let color = bubbleColorIncoming
return self.incoming(color: color) return self.incoming(color: color)
}() }()
public lazy var outgoing: JSQMessagesBubbleImage = { public lazy var outgoing: JSQMessagesBubbleImage = {
let color = UIColor.ows_materialBlue let color = bubbleColorOutgoingSent
return self.outgoing(color: color) return self.outgoing(color: color)
}() }()
public lazy var currentlyOutgoing: JSQMessagesBubbleImage = { public lazy var currentlyOutgoing: JSQMessagesBubbleImage = {
let color = UIColor.ows_fadedBlue let color = bubbleColorOutgoingSending
return self.outgoing(color: color) return self.outgoing(color: color)
}() }()
public lazy var outgoingFailed: JSQMessagesBubbleImage = { public lazy var outgoingFailed: JSQMessagesBubbleImage = {
let color = UIColor.gray let color = bubbleColorOutgoingUnsent
return self.outgoing(color: color) return self.outgoing(color: color)
}() }()

Loading…
Cancel
Save