From 6525ccdb05c0cab763508f68fa65289696449d3c Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Tue, 27 Mar 2018 13:25:02 -0400 Subject: [PATCH] Bubble collapse. --- .../ConversationView/Cells/OWSMessageCell.m | 361 ++++-------------- 1 file changed, 78 insertions(+), 283 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m index aca0422cb..5697316b6 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m @@ -50,7 +50,7 @@ static const CGFloat kBubbleTextVInset = 6.f; //@property (nonatomic) BOOL isOutgoing; @property (nonatomic) CAShapeLayer *shapeLayer; -@property (nonatomic) UIColor *bubbleColor; +@property (nonatomic, nullable) UIColor *bubbleColor; @end @@ -90,7 +90,7 @@ static const CGFloat kBubbleTextVInset = 6.f; } } -- (void)setBubbleColor:(UIColor *)bubbleColor +- (void)setBubbleColor:(nullable UIColor *)bubbleColor { _bubbleColor = bubbleColor; @@ -121,97 +121,6 @@ static const CGFloat kBubbleTextVInset = 6.f; self.maskLayer.path = bezierPath.CGPath; } -//- (void)updateMask -//{ -// UIView *_Nullable maskedSubview = self.maskedSubview; -// if (!maskedSubview) { -// return; -// } -// maskedSubview.frame = self.bounds; -// //<<<<<<< HEAD -// // // The JSQ masks are not RTL-safe, so we need to invert the -// // // mask orientation manually. -// // BOOL hasOutgoingMask = self.isOutgoing ^ self.isRTL; -// // -// // // Since the caption has it's own tail, the media bubble just above -// // // it looks better without a tail. -// // if (self.hideTail) { -// // if (hasOutgoingMask) { -// // self.layoutMargins = UIEdgeInsetsMake(0, 0, 2, 8); -// // } else { -// // self.layoutMargins = UIEdgeInsetsMake(0, 8, 2, 0); -// // } -// // maskedSubview.clipsToBounds = YES; -// // -// // // I arrived at this cornerRadius by superimposing the generated corner -// // // over that generated from the JSQMessagesMediaViewBubbleImageMasker -// // maskedSubview.layer.cornerRadius = 17; -// // } else { -// // [JSQMessagesMediaViewBubbleImageMasker applyBubbleImageMaskToMediaView:maskedSubview -// // isOutgoing:hasOutgoingMask]; -// // } -// //||||||| merged common ancestors -// // // The JSQ masks are not RTL-safe, so we need to invert the -// // // mask orientation manually. -// // BOOL hasOutgoingMask = self.isOutgoing ^ self.isRTL; -// // [JSQMessagesMediaViewBubbleImageMasker applyBubbleImageMaskToMediaView:maskedSubview -// isOutgoing:hasOutgoingMask]; -// //======= -// -// -// UIBezierPath *bezierPath = [BubbleFillView maskPathForSize:self.bounds.size -// isOutgoing:self.isOutgoing -// isRTL:self.isRTL]; -// self.maskLayer.path = bezierPath.CGPath; -// maskedSubview.layer.mask = self.maskLayer; -// //>>>>>>> SQUASHED -//} - -//- (void)setIsOutgoing:(BOOL)isOutgoing { -// if (_isOutgoing == isOutgoing) { -// return; -// } -// _isOutgoing = isOutgoing; -// [self updateMask]; -//} -// -//- (void)setFrame:(CGRect)frame -//{ -// BOOL didSizeChange = !CGSizeEqualToSize(self.frame.size, frame.size); -// -// [super setFrame:frame]; -// -// if (didSizeChange || !self.shapeLayer) { -// [self updateMask]; -// } -//} -// -//- (void)setBounds:(CGRect)bounds -//{ -// BOOL didSizeChange = !CGSizeEqualToSize(self.bounds.size, bounds.size); -// -// [super setBounds:bounds]; -// -// if (didSizeChange || !self.shapeLayer) { -// [self updateMask]; -// } -//} -// -//- (void)updateMask -//{ -// if (!self.shapeLayer) { -// self.shapeLayer = [CAShapeLayer new]; -// [self.layer addSublayer:self.shapeLayer]; -// } -// -// UIBezierPath *bezierPath = [self.class maskPathForSize:self.bounds.size -// isOutgoing:self.isOutgoing -// isRTL:self.isRTL]; -// -// self.shapeLayer.fillColor = self.bubbleColor.CGColor; -// self.shapeLayer.path = bezierPath.CGPath; -//} - + (UIBezierPath *)maskPathForSize:(CGSize)size isOutgoing:(BOOL)isOutgoing isRTL:(BOOL)isRTL @@ -311,187 +220,10 @@ static const CGFloat kBubbleTextVInset = 6.f; return bezierPath; } -//- (void)setBubbleColor:(UIColor *)bubbleColor { -// _bubbleColor = bubbleColor; -// -// self.shapeLayer.fillColor = bubbleColor.CGColor; -//} - @end #pragma mark - -//@interface BubbleFillView : UIView -// -////@property (nonatomic) BOOL isOutgoing; -////@property (nonatomic) CAShapeLayer *shapeLayer; -////@property (nonatomic) UIColor *bubbleColor; -// -//@end -// -//#pragma mark - -// -//@implementation BubbleFillView -// -////- (void)setIsOutgoing:(BOOL)isOutgoing { -//// if (_isOutgoing == isOutgoing) { -//// return; -//// } -//// _isOutgoing = isOutgoing; -//// [self updateMask]; -////} -//// -////- (void)setFrame:(CGRect)frame -////{ -//// BOOL didSizeChange = !CGSizeEqualToSize(self.frame.size, frame.size); -//// -//// [super setFrame:frame]; -//// -//// if (didSizeChange || !self.shapeLayer) { -//// [self updateMask]; -//// } -////} -//// -////- (void)setBounds:(CGRect)bounds -////{ -//// BOOL didSizeChange = !CGSizeEqualToSize(self.bounds.size, bounds.size); -//// -//// [super setBounds:bounds]; -//// -//// if (didSizeChange || !self.shapeLayer) { -//// [self updateMask]; -//// } -////} -//// -////- (void)updateMask -////{ -//// if (!self.shapeLayer) { -//// self.shapeLayer = [CAShapeLayer new]; -//// [self.layer addSublayer:self.shapeLayer]; -//// } -//// -//// UIBezierPath *bezierPath = [self.class maskPathForSize:self.bounds.size -//// isOutgoing:self.isOutgoing -//// isRTL:self.isRTL]; -//// -//// self.shapeLayer.fillColor = self.bubbleColor.CGColor; -//// self.shapeLayer.path = bezierPath.CGPath; -////} -// -////- (void)setBubbleColor:(UIColor *)bubbleColor { -//// _bubbleColor = bubbleColor; -//// -//// self.shapeLayer.fillColor = bubbleColor.CGColor; -////} -// -//@end -// -//#pragma mark - -// -//@interface BubbleMaskingView : UIView -// -//@property (nonatomic) BOOL isOutgoing; -//@property (nonatomic) BOOL hideTail; -//@property (nonatomic, nullable, weak) UIView *maskedSubview; -//@property (nonatomic) CAShapeLayer *maskLayer; -// -//@end -// -//#pragma mark - -// -//@implementation BubbleMaskingView -// -//- (void)setMaskedSubview:(UIView * _Nullable)maskedSubview { -// if (_maskedSubview == maskedSubview) { -// return; -// } -// _maskedSubview = maskedSubview; -// [self updateMask]; -//} -// -//- (void)setIsOutgoing:(BOOL)isOutgoing { -// if (_isOutgoing == isOutgoing) { -// return; -// } -// _isOutgoing = isOutgoing; -// [self updateMask]; -//} -// -//- (void)setFrame:(CGRect)frame -//{ -// BOOL didSizeChange = !CGSizeEqualToSize(self.frame.size, frame.size); -// -// [super setFrame:frame]; -// -// if (didSizeChange) { -// [self updateMask]; -// } -//} -// -//- (void)setBounds:(CGRect)bounds -//{ -// BOOL didSizeChange = !CGSizeEqualToSize(self.bounds.size, bounds.size); -// -// [super setBounds:bounds]; -// -// if (didSizeChange) { -// [self updateMask]; -// } -//} -// -//- (void)updateMask -//{ -// UIView *_Nullable maskedSubview = self.maskedSubview; -// if (!maskedSubview) { -// return; -// } -// maskedSubview.frame = self.bounds; -// //<<<<<<< HEAD -// // // The JSQ masks are not RTL-safe, so we need to invert the -// // // mask orientation manually. -// // BOOL hasOutgoingMask = self.isOutgoing ^ self.isRTL; -// // -// // // Since the caption has it's own tail, the media bubble just above -// // // it looks better without a tail. -// // if (self.hideTail) { -// // if (hasOutgoingMask) { -// // self.layoutMargins = UIEdgeInsetsMake(0, 0, 2, 8); -// // } else { -// // self.layoutMargins = UIEdgeInsetsMake(0, 8, 2, 0); -// // } -// // maskedSubview.clipsToBounds = YES; -// // -// // // I arrived at this cornerRadius by superimposing the generated corner -// // // over that generated from the JSQMessagesMediaViewBubbleImageMasker -// // maskedSubview.layer.cornerRadius = 17; -// // } else { -// // [JSQMessagesMediaViewBubbleImageMasker applyBubbleImageMaskToMediaView:maskedSubview -// // isOutgoing:hasOutgoingMask]; -// // } -// //||||||| merged common ancestors -// // // The JSQ masks are not RTL-safe, so we need to invert the -// // // mask orientation manually. -// // BOOL hasOutgoingMask = self.isOutgoing ^ self.isRTL; -// // [JSQMessagesMediaViewBubbleImageMasker applyBubbleImageMaskToMediaView:maskedSubview -// isOutgoing:hasOutgoingMask]; -// //======= -// -// if (!self.maskLayer) { -// self.maskLayer = [CAShapeLayer new]; -// } -// -// UIBezierPath *bezierPath = [OWSBubbleView maskPathForSize:self.bounds.size -// isOutgoing:self.isOutgoing -// isRTL:self.isRTL]; -// self.maskLayer.path = bezierPath.CGPath; -// maskedSubview.layer.mask = self.maskLayer; -// //>>>>>>> SQUASHED -//} -// -//@end - -#pragma mark - - @interface OWSMessageTextView : UITextView @property (nonatomic) BOOL shouldIgnoreEvents; @@ -602,7 +334,7 @@ static const CGFloat kBubbleTextVInset = 6.f; @property (nonatomic) UILabel *footerLabel; @property (nonatomic, nullable) OWSExpirationTimerView *expirationTimerView; -@property (nonatomic, nullable) UIImageView *lastImageView; +@property (nonatomic, nullable) UIView *lastBodyMediaView; // Should lazy-load expensive view contents (images, etc.). // Should do nothing if view is already loaded. @@ -743,6 +475,22 @@ static const CGFloat kBubbleTextVInset = 6.f; [self.footerView autoPinEdgeToSuperviewEdge:ALEdgeBottom]; [self.footerView autoPinWidthToSuperview]; + self.bubbleView.userInteractionEnabled = YES; + + UITapGestureRecognizer *tap = + [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)]; + [self.bubbleView addGestureRecognizer:tap]; + + + UILongPressGestureRecognizer *longPress = + [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPressGesture:)]; + [self.bubbleView addGestureRecognizer:longPress]; + + PanDirectionGestureRecognizer *panGesture = [[PanDirectionGestureRecognizer alloc] + initWithDirection:(self.isRTL ? PanDirectionLeft : PanDirectionRight)target:self + action:@selector(handlePanGesture:)]; + [self addGestureRecognizer:panGesture]; + // UITapGestureRecognizer *mediaTap = // [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleMediaTapGesture:)]; // [self.mediaMaskingView addGestureRecognizer:mediaTap]; @@ -972,7 +720,8 @@ static const CGFloat kBubbleTextVInset = 6.f; self.bubbleView.bubbleColor = [self.bubbleFactory bubbleColorWithMessage:message]; // self.bubbleFillView.bubbleColor = [self.bubbleFactory bubbleColorWithMessage:message]; } else { - OWSFail(@"%@ Unknown interaction type: %@", self.logTag, self.viewItem.interaction.class); + // Media-only messages should have no background color; they will fill the bubble's bounds + // and we don't want artifacts at the edges. } //<<<<<<< HEAD @@ -1104,6 +853,7 @@ static const CGFloat kBubbleTextVInset = 6.f; OWSAssert(self.unloadCellContentBlock); OWSAssert(!lastSubview); + self.lastBodyMediaView = bodyMediaView; bodyMediaView.userInteractionEnabled = NO; if (self.isMediaBeingSent) { bodyMediaView.layer.opacity = 0.75f; @@ -1750,7 +1500,6 @@ static const CGFloat kBubbleTextVInset = 6.f; self.unloadCellContentBlock = ^{ stillImageView.image = nil; }; - self.lastImageView = stillImageView; return stillImageView; } @@ -1793,7 +1542,6 @@ static const CGFloat kBubbleTextVInset = 6.f; self.unloadCellContentBlock = ^{ animatedImageView.image = nil; }; - self.lastImageView = animatedImageView; return animatedImageView; } @@ -1878,7 +1626,6 @@ static const CGFloat kBubbleTextVInset = 6.f; self.unloadCellContentBlock = ^{ stillImageView.image = nil; }; - self.lastImageView = stillImageView; return stillImageView; } @@ -2252,6 +1999,7 @@ static const CGFloat kBubbleTextVInset = 6.f; // TODO: self.bubbleView.hidden = YES; + self.bubbleView.bubbleColor = nil; //<<<<<<< HEAD // self.textBubbleImageView.image = nil; // self.textBubbleImageView.hidden = YES; @@ -2297,8 +2045,8 @@ static const CGFloat kBubbleTextVInset = 6.f; [self.expirationTimerView removeFromSuperview]; self.expirationTimerView = nil; - [self.lastImageView removeFromSuperview]; - self.lastImageView = nil; + [self.lastBodyMediaView removeFromSuperview]; + self.lastBodyMediaView = nil; [self hideMenuControllerIfNecessary]; } @@ -2331,6 +2079,30 @@ static const CGFloat kBubbleTextVInset = 6.f; #pragma mark - Gesture recognizers +- (void)handleTapGesture:(UITapGestureRecognizer *)sender +{ + OWSAssert(self.delegate); + + if (sender.state != UIGestureRecognizerStateRecognized) { + DDLogVerbose(@"%@ Ignoring tap on message: %@", self.logTag, self.viewItem.interaction.debugDescription); + return; + } + + if (self.lastBodyMediaView) { + // Treat this as a "body media" gesture if: + // + // * There is a "body media" view. + // * The gesture occured within or above the "body media" view. + CGPoint location = [sender locationInView:self.lastBodyMediaView]; + if (location.y <= self.lastBodyMediaView.height) { + [self handleMediaTapGesture:sender]; + return; + } + } + + [self handleTextTapGesture:sender]; +} + - (void)handleTextTapGesture:(UITapGestureRecognizer *)sender { OWSAssert(self.delegate); @@ -2388,25 +2160,25 @@ static const CGFloat kBubbleTextVInset = 6.f; } break; case OWSMessageCellType_StillImage: - OWSAssert(self.lastImageView); + OWSAssert(self.lastBodyMediaView); [self.delegate didTapImageViewItem:self.viewItem attachmentStream:self.attachmentStream - imageView:self.lastImageView]; + imageView:self.lastBodyMediaView]; break; case OWSMessageCellType_AnimatedImage: - OWSAssert(self.lastImageView); + OWSAssert(self.lastBodyMediaView); [self.delegate didTapImageViewItem:self.viewItem attachmentStream:self.attachmentStream - imageView:self.lastImageView]; + imageView:self.lastBodyMediaView]; break; case OWSMessageCellType_Audio: [self.delegate didTapAudioViewItem:self.viewItem attachmentStream:self.attachmentStream]; return; case OWSMessageCellType_Video: - OWSAssert(self.lastImageView); + OWSAssert(self.lastBodyMediaView); [self.delegate didTapVideoViewItem:self.viewItem attachmentStream:self.attachmentStream - imageView:self.lastImageView]; + imageView:self.lastBodyMediaView]; return; case OWSMessageCellType_GenericAttachment: [AttachmentSharing showShareUIForAttachment:self.attachmentStream]; @@ -2421,6 +2193,29 @@ static const CGFloat kBubbleTextVInset = 6.f; } } +- (void)handleLongPressGesture:(UILongPressGestureRecognizer *)sender +{ + OWSAssert(self.delegate); + + if (sender.state != UIGestureRecognizerStateBegan) { + return; + } + + if (self.lastBodyMediaView) { + // Treat this as a "body media" gesture if: + // + // * There is a "body media" view. + // * The gesture occured within or above the "body media" view. + CGPoint location = [sender locationInView:self.lastBodyMediaView]; + if (location.y <= self.lastBodyMediaView.height) { + [self handleMediaLongPressGesture:sender]; + return; + } + } + + [self handleTextLongPressGesture:sender]; +} + - (void)handleTextLongPressGesture:(UILongPressGestureRecognizer *)sender { OWSAssert(self.delegate);