From cb00b2287013834cc9423b9fa30792aa42469783 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Wed, 28 Mar 2018 10:01:01 -0400 Subject: [PATCH] Bubble collapse. --- Signal.xcodeproj/project.pbxproj | 12 + Signal/src/Signal-Bridging-Header.h | 1 + .../ConversationView/Cells/OWSBubbleView.h | 28 ++ .../ConversationView/Cells/OWSBubbleView.m | 130 +++++++++ .../ConversationView/Cells/OWSMessageCell.h | 2 - .../ConversationView/Cells/OWSMessageCell.m | 274 +----------------- .../Cells/OWSMessageTextView.h | 13 + .../Cells/OWSMessageTextView.m | 65 +++++ .../MediaGalleryViewController.swift | 4 +- 9 files changed, 259 insertions(+), 270 deletions(-) create mode 100644 Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h create mode 100644 Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m create mode 100644 Signal/src/ViewControllers/ConversationView/Cells/OWSMessageTextView.h create mode 100644 Signal/src/ViewControllers/ConversationView/Cells/OWSMessageTextView.m diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 3906643d0..c10d5fb81 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -214,6 +214,8 @@ 34D8C02B1ED3685800188D7C /* DebugUIContacts.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D8C02A1ED3685800188D7C /* DebugUIContacts.m */; }; 34D99C931F2937CC00D284D6 /* OWSAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D99C911F2937CC00D284D6 /* OWSAnalytics.swift */; }; 34DB0BED2011548B007B313F /* OWSDatabaseConverterTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 34DB0BEC2011548B007B313F /* OWSDatabaseConverterTest.m */; }; + 34DBF003206BD5A500025978 /* OWSMessageTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34DBEFFF206BD5A400025978 /* OWSMessageTextView.m */; }; + 34DBF004206BD5A500025978 /* OWSBubbleView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34DBF001206BD5A500025978 /* OWSBubbleView.m */; }; 34E3E5681EC4B19400495BAC /* AudioProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34E3E5671EC4B19400495BAC /* AudioProgressView.swift */; }; 34E3EF0D1EFC235B007F6822 /* DebugUIDiskUsage.m in Sources */ = {isa = PBXBuildFile; fileRef = 34E3EF0C1EFC235B007F6822 /* DebugUIDiskUsage.m */; }; 34E3EF101EFC2684007F6822 /* DebugUIPage.m in Sources */ = {isa = PBXBuildFile; fileRef = 34E3EF0F1EFC2684007F6822 /* DebugUIPage.m */; }; @@ -835,6 +837,10 @@ 34D99C911F2937CC00D284D6 /* OWSAnalytics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSAnalytics.swift; sourceTree = ""; }; 34DB0BEB2011548A007B313F /* OWSDatabaseConverterTest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSDatabaseConverterTest.h; sourceTree = ""; }; 34DB0BEC2011548B007B313F /* OWSDatabaseConverterTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSDatabaseConverterTest.m; sourceTree = ""; }; + 34DBEFFF206BD5A400025978 /* OWSMessageTextView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessageTextView.m; sourceTree = ""; }; + 34DBF000206BD5A400025978 /* OWSMessageTextView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageTextView.h; sourceTree = ""; }; + 34DBF001206BD5A500025978 /* OWSBubbleView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBubbleView.m; sourceTree = ""; }; + 34DBF002206BD5A500025978 /* OWSBubbleView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBubbleView.h; sourceTree = ""; }; 34E3E5671EC4B19400495BAC /* AudioProgressView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioProgressView.swift; sourceTree = ""; }; 34E3EF0B1EFC235B007F6822 /* DebugUIDiskUsage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUIDiskUsage.h; sourceTree = ""; }; 34E3EF0C1EFC235B007F6822 /* DebugUIDiskUsage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugUIDiskUsage.m; sourceTree = ""; }; @@ -1645,6 +1651,8 @@ 34D1F0971F867BFC0066283D /* ConversationViewCell.m */, 34D1F0B81F8800D90066283D /* OWSAudioMessageView.h */, 34D1F0B91F8800D90066283D /* OWSAudioMessageView.m */, + 34DBF002206BD5A500025978 /* OWSBubbleView.h */, + 34DBF001206BD5A500025978 /* OWSBubbleView.m */, 34D1F09A1F867BFC0066283D /* OWSContactOffersCell.h */, 34D1F09B1F867BFC0066283D /* OWSContactOffersCell.m */, 34D1F09C1F867BFC0066283D /* OWSExpirableMessageView.h */, @@ -1654,6 +1662,8 @@ 34D1F0B61F87F8850066283D /* OWSGenericAttachmentView.m */, 34D1F0A11F867BFC0066283D /* OWSMessageCell.h */, 34D1F0A21F867BFC0066283D /* OWSMessageCell.m */, + 34DBF000206BD5A400025978 /* OWSMessageTextView.h */, + 34DBEFFF206BD5A400025978 /* OWSMessageTextView.m */, 34D1F0A51F867BFC0066283D /* OWSSystemMessageCell.h */, 34D1F0A61F867BFC0066283D /* OWSSystemMessageCell.m */, 34D1F0A71F867BFC0066283D /* OWSUnreadIndicatorCell.h */, @@ -3213,6 +3223,7 @@ 45F32C222057297A00A300D5 /* MediaDetailViewController.m in Sources */, 34B3F8851E8DF1700035BE1A /* NewGroupViewController.m in Sources */, 34D8C0271ED3673300188D7C /* DebugUIMessages.m in Sources */, + 34DBF003206BD5A500025978 /* OWSMessageTextView.m in Sources */, 34D1F0B41F86D31D0066283D /* ConversationCollectionView.m in Sources */, 34B3F8821E8DF1700035BE1A /* NewContactThreadViewController.m in Sources */, 45D308AD2049A439000189E4 /* PinEntryView.m in Sources */, @@ -3244,6 +3255,7 @@ 458DE9D61DEE3FD00071BB03 /* PeerConnectionClient.swift in Sources */, 45F32C242057297A00A300D5 /* MessageDetailViewController.swift in Sources */, 34D1F0841F8678AA0066283D /* ConversationInputToolbar.m in Sources */, + 34DBF004206BD5A500025978 /* OWSBubbleView.m in Sources */, FCC81A981A44558300DFEC7D /* UIDevice+TSHardwareVersion.m in Sources */, 76EB054018170B33006006FC /* AppDelegate.m in Sources */, 34D1F0831F8678AA0066283D /* ConversationInputTextView.m in Sources */, diff --git a/Signal/src/Signal-Bridging-Header.h b/Signal/src/Signal-Bridging-Header.h index a950f6f37..15ef23f82 100644 --- a/Signal/src/Signal-Bridging-Header.h +++ b/Signal/src/Signal-Bridging-Header.h @@ -22,6 +22,7 @@ #import "OWSBackup.h" #import "OWSBackupIO.h" #import "OWSBezierPathView.h" +#import "OWSBubbleView.h" #import "OWSCallNotificationsAdaptee.h" #import "OWSDatabaseMigration.h" #import "OWSMessageCell.h" diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h new file mode 100644 index 000000000..a1f5436aa --- /dev/null +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h @@ -0,0 +1,28 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +NS_ASSUME_NONNULL_BEGIN + +extern const CGFloat kOWSMessageCellCornerRadius; + +extern const CGFloat kBubbleVRounding; +extern const CGFloat kBubbleHRounding; +extern const CGFloat kBubbleThornSideInset; +extern const CGFloat kBubbleThornVInset; +extern const CGFloat kBubbleTextHInset; +extern const CGFloat kBubbleTextVInset; + +@interface OWSBubbleView : UIView + +@property (nonatomic) BOOL isOutgoing; +@property (nonatomic) BOOL hideTail; + +@property (nonatomic) CAShapeLayer *maskLayer; +@property (nonatomic) CAShapeLayer *shapeLayer; + +@property (nonatomic, nullable) UIColor *bubbleColor; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m new file mode 100644 index 000000000..0bde2233a --- /dev/null +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m @@ -0,0 +1,130 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSBubbleView.h" +#import + +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 kBubbleVRounding = kOWSMessageCellCornerRadius; +const CGFloat kBubbleHRounding = kOWSMessageCellCornerRadius; +const CGFloat kBubbleThornSideInset = 5.f; +const CGFloat kBubbleThornVInset = 0; +const CGFloat kBubbleTextHInset = 10.f; +const CGFloat kBubbleTextVInset = 10.f; + +@implementation OWSBubbleView + +- (void)setIsOutgoing:(BOOL)isOutgoing +{ + BOOL didChange = _isOutgoing != isOutgoing; + + _isOutgoing = isOutgoing; + + if (didChange || !self.shapeLayer) { + [self updateMask]; + } +} + +- (void)setFrame:(CGRect)frame +{ + BOOL didChange = !CGSizeEqualToSize(self.frame.size, frame.size); + + [super setFrame:frame]; + + if (didChange || !self.shapeLayer) { + [self updateMask]; + } +} + +- (void)setBounds:(CGRect)bounds +{ + BOOL didChange = !CGSizeEqualToSize(self.bounds.size, bounds.size); + + [super setBounds:bounds]; + + if (didChange || !self.shapeLayer) { + [self updateMask]; + } +} + +- (void)setBubbleColor:(nullable UIColor *)bubbleColor +{ + _bubbleColor = bubbleColor; + + if (!self.shapeLayer) { + [self updateMask]; + } + self.shapeLayer.fillColor = bubbleColor.CGColor; +} + +- (void)updateMask +{ + if (!self.shapeLayer) { + self.shapeLayer = [CAShapeLayer new]; + [self.layer addSublayer:self.shapeLayer]; + } + if (!self.maskLayer) { + self.maskLayer = [CAShapeLayer new]; + self.layer.mask = self.maskLayer; + } + + 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; + + self.maskLayer.path = bezierPath.CGPath; +} + ++ (UIBezierPath *)maskPathForSize:(CGSize)size isOutgoing:(BOOL)isOutgoing isRTL:(BOOL)isRTL +{ + UIBezierPath *bezierPath = [UIBezierPath new]; + + CGFloat bubbleLeft = 0.f; + CGFloat bubbleRight = size.width - kBubbleThornSideInset; + CGFloat bubbleTop = 0.f; + CGFloat bubbleBottom = size.height - kBubbleThornVInset; + + [bezierPath moveToPoint:CGPointMake(bubbleLeft + kBubbleHRounding, bubbleTop)]; + [bezierPath addLineToPoint:CGPointMake(bubbleRight - kBubbleHRounding, bubbleTop)]; + [bezierPath addQuadCurveToPoint:CGPointMake(bubbleRight, bubbleTop + kBubbleVRounding) + controlPoint:CGPointMake(bubbleRight, bubbleTop)]; + [bezierPath addLineToPoint:CGPointMake(bubbleRight, bubbleBottom - kBubbleVRounding)]; + [bezierPath addQuadCurveToPoint:CGPointMake(bubbleRight - kBubbleHRounding, bubbleBottom) + controlPoint:CGPointMake(bubbleRight, bubbleBottom)]; + [bezierPath addLineToPoint:CGPointMake(bubbleLeft + kBubbleHRounding, bubbleBottom)]; + [bezierPath addQuadCurveToPoint:CGPointMake(bubbleLeft, bubbleBottom - kBubbleVRounding) + controlPoint:CGPointMake(bubbleLeft, bubbleBottom)]; + [bezierPath addLineToPoint:CGPointMake(bubbleLeft, bubbleTop + kBubbleVRounding)]; + [bezierPath addQuadCurveToPoint:CGPointMake(bubbleLeft + kBubbleHRounding, bubbleTop) + controlPoint:CGPointMake(bubbleLeft, bubbleTop)]; + + // Thorn Tip + CGPoint thornTip = CGPointMake(size.width + 1, size.height); + CGPoint thornA = CGPointMake(bubbleRight - kBubbleHRounding * 0.5f, bubbleBottom - kBubbleVRounding); + CGPoint thornB = CGPointMake(bubbleRight, bubbleBottom - kBubbleVRounding); + [bezierPath moveToPoint:thornTip]; + [bezierPath addQuadCurveToPoint:thornA controlPoint:CGPointMake(thornA.x, bubbleBottom)]; + [bezierPath addLineToPoint:thornB]; + [bezierPath addQuadCurveToPoint:thornTip controlPoint:CGPointMake(thornB.x, bubbleBottom)]; + [bezierPath addLineToPoint:thornTip]; + + // Horizontal Flip If Necessary + BOOL shouldFlip = isOutgoing == isRTL; + if (shouldFlip) { + CGAffineTransform flipTransform = CGAffineTransformMakeTranslation(size.width, 0.0); + flipTransform = CGAffineTransformScale(flipTransform, -1.0, 1.0); + [bezierPath applyTransform:flipTransform]; + } + return bezierPath; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.h index 3116bce7e..789b94fbb 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.h +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.h @@ -6,8 +6,6 @@ NS_ASSUME_NONNULL_BEGIN -extern const CGFloat OWSMessageCellCornerRadius; - @interface OWSMessageCell : ConversationViewCell + (NSString *)cellReuseIdentifier; diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m index 5697316b6..c2fc47ba0 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m @@ -8,8 +8,10 @@ #import "ConversationViewItem.h" #import "NSAttributedString+OWS.h" #import "OWSAudioMessageView.h" +#import "OWSBubbleView.h" #import "OWSExpirationTimerView.h" #import "OWSGenericAttachmentView.h" +#import "OWSMessageTextView.h" #import "Signal-Swift.h" #import "UIColor+OWS.h" #import @@ -18,7 +20,6 @@ NS_ASSUME_NONNULL_BEGIN -// TODO: Choose best thorn. // TODO: Review all comments. CG_INLINE CGSize CGSizeCeil(CGSize size) @@ -26,270 +27,6 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) return CGSizeMake((CGFloat)ceil(size.width), (CGFloat)ceil(size.height)); } -// This approximates the curve of our message bubbles, which makes the animation feel a little smoother. -const CGFloat OWSMessageCellCornerRadius = 17; - -// TODO: We could make the bubble shape respond to dynamic text. -static const CGFloat kBubbleVRounding = 8.5f; -static const CGFloat kBubbleHRounding = 10.f; -//static const CGFloat kBubbleThornSideInset = 3.f; -//static const CGFloat kBubbleThornVInset = 3.f; -//static const CGFloat kBubbleThornSideInset = 6.f; -//static const CGFloat kBubbleThornVInset = 0.f; -static const CGFloat kBubbleThornSideInset = kBubbleHRounding * 0.3f; -static const CGFloat kBubbleThornVInset = kBubbleVRounding * 0.3f; -static const CGFloat kBubbleTextHInset = 6.f; -static const CGFloat kBubbleTextVInset = 6.f; - -@interface OWSBubbleView : UIView - -@property (nonatomic) BOOL isOutgoing; -@property (nonatomic) BOOL hideTail; -//@property (nonatomic, nullable, weak) UIView *maskedSubview; -@property (nonatomic) CAShapeLayer *maskLayer; - -//@property (nonatomic) BOOL isOutgoing; -@property (nonatomic) CAShapeLayer *shapeLayer; -@property (nonatomic, nullable) UIColor *bubbleColor; - -@end - -#pragma mark - - -@implementation OWSBubbleView - -- (void)setIsOutgoing:(BOOL)isOutgoing { - BOOL didChange = _isOutgoing != isOutgoing; - - _isOutgoing = isOutgoing; - - if (didChange || !self.shapeLayer) { - [self updateMask]; - } -} - -- (void)setFrame:(CGRect)frame -{ - BOOL didChange = !CGSizeEqualToSize(self.frame.size, frame.size); - - [super setFrame:frame]; - - if (didChange || !self.shapeLayer) { - [self updateMask]; - } -} - -- (void)setBounds:(CGRect)bounds -{ - BOOL didChange = !CGSizeEqualToSize(self.bounds.size, bounds.size); - - [super setBounds:bounds]; - - if (didChange || !self.shapeLayer) { - [self updateMask]; - } -} - -- (void)setBubbleColor:(nullable UIColor *)bubbleColor -{ - _bubbleColor = bubbleColor; - - if (!self.shapeLayer) { - [self updateMask]; - } - self.shapeLayer.fillColor = bubbleColor.CGColor; -} - -- (void)updateMask -{ - if (!self.shapeLayer) { - self.shapeLayer = [CAShapeLayer new]; - [self.layer addSublayer:self.shapeLayer]; - } - if (!self.maskLayer) { - self.maskLayer = [CAShapeLayer new]; - self.layer.mask = self.maskLayer; - } - - 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; - - self.maskLayer.path = bezierPath.CGPath; -} - -+ (UIBezierPath *)maskPathForSize:(CGSize)size - isOutgoing:(BOOL)isOutgoing - isRTL:(BOOL)isRTL -{ - UIBezierPath *bezierPath = [UIBezierPath new]; - - CGFloat bubbleLeft = 0.f; - CGFloat bubbleRight = size.width - kBubbleThornSideInset; - CGFloat bubbleTop = 0.f; - CGFloat bubbleBottom = size.height - kBubbleThornVInset; - - [bezierPath moveToPoint:CGPointMake(bubbleLeft + kBubbleHRounding, bubbleTop)]; - [bezierPath addLineToPoint:CGPointMake(bubbleRight - kBubbleHRounding, bubbleTop)]; - [bezierPath addQuadCurveToPoint:CGPointMake(bubbleRight, bubbleTop + kBubbleVRounding) - controlPoint:CGPointMake(bubbleRight, bubbleTop)]; - [bezierPath addLineToPoint:CGPointMake(bubbleRight, bubbleBottom - kBubbleVRounding)]; - [bezierPath addQuadCurveToPoint:CGPointMake(bubbleRight - kBubbleHRounding, bubbleBottom) - controlPoint:CGPointMake(bubbleRight, bubbleBottom)]; - [bezierPath addLineToPoint:CGPointMake(bubbleLeft + kBubbleHRounding, bubbleBottom)]; - [bezierPath addQuadCurveToPoint:CGPointMake(bubbleLeft, bubbleBottom - kBubbleVRounding) - controlPoint:CGPointMake(bubbleLeft, bubbleBottom)]; - [bezierPath addLineToPoint:CGPointMake(bubbleLeft, bubbleTop + kBubbleVRounding)]; - [bezierPath addQuadCurveToPoint:CGPointMake(bubbleLeft + kBubbleHRounding, bubbleTop) - controlPoint:CGPointMake(bubbleLeft, bubbleTop)]; - - // Thorn Tip - CGPoint thornTip = CGPointMake(size.width, - size.height); - CGPoint thornA = CGPointMake(bubbleRight - kBubbleHRounding, bubbleBottom); - CGPoint thornB = CGPointMake(bubbleRight, bubbleBottom - kBubbleVRounding); - [bezierPath moveToPoint:thornTip]; - // [bezierPath addLineToPoint:CGPointMake(bubbleRight - kBubbleHRounding * 0.85f, bubbleBottom)]; - // [bezierPath addLineToPoint:thornA]; - // [bezierPath addLineToPoint:thornB]; - // [bezierPath addLineToPoint:CGPointMake(bubbleRight, bubbleBottom - kBubbleVRounding * 0.5f)]; - [bezierPath addQuadCurveToPoint:CGPointMake(bubbleRight - kBubbleHRounding * 0.8f, bubbleBottom) - controlPoint:CGPointMake(bubbleRight - kBubbleHRounding * 0.4f, bubbleBottom)]; - [bezierPath addLineToPoint:thornA]; - [bezierPath addLineToPoint:thornB]; - [bezierPath addLineToPoint:CGPointMake(bubbleRight, bubbleBottom - kBubbleVRounding * 0.7f)]; - [bezierPath addQuadCurveToPoint:thornTip - controlPoint:CGPointMake(bubbleRight, bubbleBottom - kBubbleVRounding * 0.3f)]; - - // // Thorn Tip - // [bezierPath moveToPoint:CGPointMake(bubbleRight - kBubbleHRounding, bubbleBottom)]; - // [bezierPath addLineToPoint:CGPointMake(bubbleRight, bubbleBottom - kBubbleVRounding)]; - // [bezierPath addQuadCurveToPoint:CGPointMake(bubbleRight + kBubbleThornSideInset, bubbleBottom - 0.f) - // controlPoint:CGPointMake(bubbleRight, bubbleBottom)]; - // // [bezierPath addQuadCurveToPoint:CGPointMake(bubbleRight + kBubbleThornSideInset - 1.f, bubbleBottom - - // 0.5f) - // // controlPoint:CGPointMake(bubbleRight, bubbleBottom)]; - // // [bezierPath addQuadCurveToPoint:CGPointMake(bubbleRight + kBubbleThornSideInset, bubbleBottom) - // // controlPoint:CGPointMake(bubbleRight + kBubbleThornSideInset, bubbleBottom - 0.5f)]; - // [bezierPath addLineToPoint:CGPointMake(bubbleRight + kBubbleThornSideInset, bubbleBottom)]; - - // // Thorn Tip - // CGFloat kThornPinchingA = 0.f; - // CGFloat kThornPinchingB = 3.5f; - // CGPoint thornTip = CGPointMake(self.width, - // self.height); - // CGPoint thornA = CGPointMake(bubbleRight - kBubbleHRounding, bubbleBottom - kThornPinchingA); - // CGPoint thornB = CGPointMake(bubbleRight - kThornPinchingB, bubbleBottom - kBubbleVRounding); - // [bezierPath moveToPoint:thornTip]; - // [bezierPath addQuadCurveToPoint:thornA - // controlPoint:CGPointMake(bubbleRight - kBubbleHRounding, bubbleBottom - kThornPinchingA)]; - // [bezierPath addLineToPoint:thornB]; - // [bezierPath addQuadCurveToPoint:thornTip - // controlPoint:CGPointMake(bubbleRight - kThornPinchingB, bubbleBottom - kBubbleVRounding * 0.1f)]; - - // Thorn Tip - // CGFloat kThornPinchingA = 0.f; - // CGFloat kThornPinchingB = 3.5f; - // CGPoint thornA = CGPointMake(bubbleRight, bubbleBottom - kBubbleVRounding * 1.65f); - // CGPoint thornB = CGPointMake(bubbleRight, bubbleBottom - kBubbleVRounding * 1.f); - - // CGPoint thornA = CGPointMake(bubbleRight, bubbleTop + kBubbleVRounding * 1.f); - // CGPoint thornB = CGPointMake(bubbleRight, bubbleTop + kBubbleVRounding * 1.65f); - // CGPoint thornTip = CGPointMake(bubbleRight + kBubbleThornSideInset * 0.85f, - // (thornA.y + thornB.y) * 0.5f); - // [bezierPath moveToPoint:thornTip]; - // [bezierPath addLineToPoint:thornA]; - // [bezierPath addLineToPoint:thornB]; - - // [bezierPath addQuadCurveToPoint:thornA - // controlPoint:CGPointMake(bubbleRight - kBubbleHRounding, bubbleBottom - kThornPinchingA)]; - // [bezierPath addLineToPoint:thornB]; - // [bezierPath addQuadCurveToPoint:thornTip - // controlPoint:CGPointMake(bubbleRight - kThornPinchingB, bubbleBottom - kBubbleVRounding * 0.1f)]; - - // Horizontal Flip If Necessary - BOOL shouldFlip = isOutgoing == isRTL; - if (shouldFlip) { - CGAffineTransform flipTransform = CGAffineTransformMakeTranslation(size.width, 0.0); - flipTransform = CGAffineTransformScale(flipTransform, -1.0, 1.0); - [bezierPath applyTransform:flipTransform]; - } - return bezierPath; -} - -@end - -#pragma mark - - -@interface OWSMessageTextView : UITextView - -@property (nonatomic) BOOL shouldIgnoreEvents; - -@end - -#pragma mark - - -@implementation OWSMessageTextView - -// Our message text views are never used for editing; -// suppress their ability to become first responder -// so that tapping on them doesn't hide keyboard. -- (BOOL)canBecomeFirstResponder -{ - return NO; -} - -// Ignore interactions with the text view _except_ taps on links. -// -// We want to disable "partial" selection of text in the message -// and we want to enable "tap to resend" by tapping on a message. -- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *_Nullable)event -{ - if (self.shouldIgnoreEvents) { - // We ignore all events for failed messages so that users - // can tap-to-resend even "all link" messages. - return NO; - } - - // Find the nearest text position to the event. - UITextPosition *_Nullable position = [self closestPositionToPoint:point]; - if (!position) { - return NO; - } - // Find the range of the character in the text which contains the event. - // - // Try every layout direction (this might not be necessary). - UITextRange *_Nullable range = nil; - for (NSNumber *textLayoutDirection in @[ - @(UITextLayoutDirectionLeft), - @(UITextLayoutDirectionRight), - @(UITextLayoutDirectionUp), - @(UITextLayoutDirectionDown), - ]) { - range = [self.tokenizer rangeEnclosingPosition:position - withGranularity:UITextGranularityCharacter - inDirection:(UITextDirection)textLayoutDirection.intValue]; - if (range) { - break; - } - } - if (!range) { - return NO; - } - // Ignore the event unless it occurred inside a link. - NSInteger startIndex = [self offsetFromPosition:self.beginningOfDocument toPosition:range.start]; - BOOL result = - [self.attributedText attribute:NSLinkAttributeName atIndex:(NSUInteger)startIndex effectiveRange:nil] != nil; - return result; -} - -@end - -#pragma mark - - @interface OWSMessageCell () // The nullable properties are created as needed. @@ -481,7 +218,6 @@ static const CGFloat kBubbleTextVInset = 6.f; [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)]; [self.bubbleView addGestureRecognizer:tap]; - UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPressGesture:)]; [self.bubbleView addGestureRecognizer:longPress]; @@ -684,6 +420,11 @@ static const CGFloat kBubbleTextVInset = 6.f; OWSAssert(self.viewItem.interaction); OWSAssert([self.viewItem.interaction isKindOfClass:[TSMessage class]]); + // TODO: We might not need to hide it. + self.bubbleView.hidden = NO; + self.bubbleView.isOutgoing = self.isOutgoing; + // TODO: Hide tails/thorns here? + if (self.shouldHaveFailedSendBadge) { self.failedSendBadgeView = [UIImageView new]; self.failedSendBadgeView.image = @@ -722,6 +463,7 @@ static const CGFloat kBubbleTextVInset = 6.f; } else { // Media-only messages should have no background color; they will fill the bubble's bounds // and we don't want artifacts at the edges. + self.bubbleView.bubbleColor = nil; } //<<<<<<< HEAD diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageTextView.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageTextView.h new file mode 100644 index 000000000..6b7294f8c --- /dev/null +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageTextView.h @@ -0,0 +1,13 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSMessageTextView : UITextView + +@property (nonatomic) BOOL shouldIgnoreEvents; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageTextView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageTextView.m new file mode 100644 index 000000000..f172de9d5 --- /dev/null +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageTextView.m @@ -0,0 +1,65 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSMessageTextView.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation OWSMessageTextView + +// Our message text views are never used for editing; +// suppress their ability to become first responder +// so that tapping on them doesn't hide keyboard. +- (BOOL)canBecomeFirstResponder +{ + return NO; +} + +// Ignore interactions with the text view _except_ taps on links. +// +// We want to disable "partial" selection of text in the message +// and we want to enable "tap to resend" by tapping on a message. +- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *_Nullable)event +{ + if (self.shouldIgnoreEvents) { + // We ignore all events for failed messages so that users + // can tap-to-resend even "all link" messages. + return NO; + } + + // Find the nearest text position to the event. + UITextPosition *_Nullable position = [self closestPositionToPoint:point]; + if (!position) { + return NO; + } + // Find the range of the character in the text which contains the event. + // + // Try every layout direction (this might not be necessary). + UITextRange *_Nullable range = nil; + for (NSNumber *textLayoutDirection in @[ + @(UITextLayoutDirectionLeft), + @(UITextLayoutDirectionRight), + @(UITextLayoutDirectionUp), + @(UITextLayoutDirectionDown), + ]) { + range = [self.tokenizer rangeEnclosingPosition:position + withGranularity:UITextGranularityCharacter + inDirection:(UITextDirection)textLayoutDirection.intValue]; + if (range) { + break; + } + } + if (!range) { + return NO; + } + // Ignore the event unless it occurred inside a link. + NSInteger startIndex = [self offsetFromPosition:self.beginningOfDocument toPosition:range.start]; + BOOL result = + [self.attributedText attribute:NSLinkAttributeName atIndex:(NSUInteger)startIndex effectiveRange:nil] != nil; + return result; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/MediaGalleryViewController.swift b/Signal/src/ViewControllers/MediaGalleryViewController.swift index c866da1a8..0a8c1e015 100644 --- a/Signal/src/ViewControllers/MediaGalleryViewController.swift +++ b/Signal/src/ViewControllers/MediaGalleryViewController.swift @@ -325,7 +325,7 @@ class MediaGalleryViewController: UINavigationController, MediaGalleryDataSource detailView.backgroundColor = .clear self.view.backgroundColor = .clear - self.presentationView.layer.cornerRadius = OWSMessageCellCornerRadius + self.presentationView.layer.cornerRadius = kOWSMessageCellCornerRadius fromViewController.present(self, animated: false) { @@ -478,7 +478,7 @@ class MediaGalleryViewController: UINavigationController, MediaGalleryDataSource if changedItems { self.presentationView.alpha = 0 } else { - self.presentationView.layer.cornerRadius = OWSMessageCellCornerRadius + self.presentationView.layer.cornerRadius = kOWSMessageCellCornerRadius } }, completion: { (_: Bool) in