From 9cc3a3b7b3998770dc08ffcf7254caf830b0e615 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Wed, 27 Jun 2018 12:44:06 -0400 Subject: [PATCH] Add body media shadows. --- Signal.xcodeproj/project.pbxproj | 12 +- .../Cells/OWSBubbleShapeView.h | 25 +++ .../Cells/OWSBubbleShapeView.m | 198 ++++++++++++++++++ .../Cells/OWSBubbleStrokeView.h | 18 -- .../Cells/OWSBubbleStrokeView.m | 129 ------------ .../ConversationView/Cells/OWSBubbleView.h | 2 + .../ConversationView/Cells/OWSBubbleView.m | 1 + .../Cells/OWSMessageBubbleView.m | 83 +++++--- .../Cells/OWSQuotedMessageView.h | 4 +- .../Cells/OWSQuotedMessageView.m | 6 +- 10 files changed, 295 insertions(+), 183 deletions(-) create mode 100644 Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleShapeView.h create mode 100644 Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleShapeView.m delete mode 100644 Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleStrokeView.h delete mode 100644 Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleStrokeView.m diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 6ff0d9e95..09356503b 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -229,7 +229,7 @@ 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 */; }; - 34DBF007206C3CB200025978 /* OWSBubbleStrokeView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34DBF006206C3CB200025978 /* OWSBubbleStrokeView.m */; }; + 34DBF007206C3CB200025978 /* OWSBubbleShapeView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34DBF006206C3CB200025978 /* OWSBubbleShapeView.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 */; }; @@ -891,8 +891,8 @@ 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 = ""; }; - 34DBF005206C3CB100025978 /* OWSBubbleStrokeView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBubbleStrokeView.h; sourceTree = ""; }; - 34DBF006206C3CB200025978 /* OWSBubbleStrokeView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBubbleStrokeView.m; sourceTree = ""; }; + 34DBF005206C3CB100025978 /* OWSBubbleShapeView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBubbleShapeView.h; sourceTree = ""; }; + 34DBF006206C3CB200025978 /* OWSBubbleShapeView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBubbleShapeView.m; 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 = ""; }; @@ -1742,8 +1742,8 @@ 34D1F0971F867BFC0066283D /* ConversationViewCell.m */, 34D1F0B81F8800D90066283D /* OWSAudioMessageView.h */, 34D1F0B91F8800D90066283D /* OWSAudioMessageView.m */, - 34DBF005206C3CB100025978 /* OWSBubbleStrokeView.h */, - 34DBF006206C3CB200025978 /* OWSBubbleStrokeView.m */, + 34DBF005206C3CB100025978 /* OWSBubbleShapeView.h */, + 34DBF006206C3CB200025978 /* OWSBubbleShapeView.m */, 34DBF002206BD5A500025978 /* OWSBubbleView.h */, 34DBF001206BD5A500025978 /* OWSBubbleView.m */, 34D1F09A1F867BFC0066283D /* OWSContactOffersCell.h */, @@ -3186,7 +3186,7 @@ 34D1F0BD1F8D108C0066283D /* AttachmentUploadView.m in Sources */, 452EC6DF205E9E30000E787C /* MediaGalleryViewController.swift in Sources */, 34386A52207D0C01009F5D9C /* HomeViewCell.m in Sources */, - 34DBF007206C3CB200025978 /* OWSBubbleStrokeView.m in Sources */, + 34DBF007206C3CB200025978 /* OWSBubbleShapeView.m in Sources */, 34D1F0BA1F8800D90066283D /* OWSAudioMessageView.m in Sources */, 34D8C02B1ED3685800188D7C /* DebugUIContacts.m in Sources */, 45C9DEB81DF4E35A0065CA84 /* WebRTCCallMessageHandler.swift in Sources */, diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleShapeView.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleShapeView.h new file mode 100644 index 000000000..c6aca76a2 --- /dev/null +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleShapeView.h @@ -0,0 +1,25 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSBubbleView.h" + +NS_ASSUME_NONNULL_BEGIN + +@class OWSBubbleView; + +@interface OWSBubbleShapeView : UIView + +@property (nonatomic, nullable) UIColor *fillColor; +@property (nonatomic, nullable) UIColor *strokeColor; +@property (nonatomic) CGFloat strokeThickness; + +- (instancetype)init NS_UNAVAILABLE; + ++ (OWSBubbleShapeView *)bubbleDrawView; ++ (OWSBubbleShapeView *)bubbleShadowView; ++ (OWSBubbleShapeView *)bubbleClipView; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleShapeView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleShapeView.m new file mode 100644 index 000000000..7d387696d --- /dev/null +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleShapeView.m @@ -0,0 +1,198 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSBubbleShapeView.h" +#import "OWSBubbleView.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSUInteger, OWSBubbleShapeViewMode) { + // For stroking or filling. + OWSBubbleShapeViewMode_Draw, + OWSBubbleShapeViewMode_Shadow, + OWSBubbleShapeViewMode_Clip, +}; + +@interface OWSBubbleShapeView () + +@property (nonatomic) OWSBubbleShapeViewMode mode; + +@property (nonatomic) CAShapeLayer *shapeLayer; +@property (nonatomic) CAShapeLayer *maskLayer; + +@property (nonatomic, weak) OWSBubbleView *bubbleView; + +@end + +#pragma mark - + +@implementation OWSBubbleShapeView + +- (instancetype)init +{ + self = [super init]; + if (!self) { + return self; + } + + self.mode = OWSBubbleShapeViewMode_Draw; + self.opaque = NO; + self.backgroundColor = [UIColor clearColor]; + self.layoutMargins = UIEdgeInsetsZero; + + self.shapeLayer = [CAShapeLayer new]; + [self.layer addSublayer:self.shapeLayer]; + + self.maskLayer = [CAShapeLayer new]; + + return self; +} + ++ (OWSBubbleShapeView *)bubbleDrawView +{ + OWSBubbleShapeView *instance = [OWSBubbleShapeView new]; + instance.mode = OWSBubbleShapeViewMode_Draw; + return instance; +} + ++ (OWSBubbleShapeView *)bubbleShadowView +{ + OWSBubbleShapeView *instance = [OWSBubbleShapeView new]; + instance.mode = OWSBubbleShapeViewMode_Shadow; + return instance; +} + ++ (OWSBubbleShapeView *)bubbleClipView +{ + OWSBubbleShapeView *instance = [OWSBubbleShapeView new]; + instance.mode = OWSBubbleShapeViewMode_Clip; + return instance; +} + +- (void)setFillColor:(nullable UIColor *)fillColor +{ + _fillColor = fillColor; + + [self updateLayers]; +} + +- (void)setStrokeColor:(nullable 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; + } + + if (!self.bubbleView) { + return; + } + + // Prevent the layer from animating changes. + [CATransaction begin]; + [CATransaction setDisableActions:YES]; + + UIBezierPath *bezierPath = [UIBezierPath new]; + + // Add the bubble view's path to the local path. + 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]; + + switch (self.mode) { + case OWSBubbleShapeViewMode_Draw: { + UIBezierPath *boundsBezierPath = [UIBezierPath bezierPathWithRect:self.bounds]; + [bezierPath appendPath:boundsBezierPath]; + + self.clipsToBounds = YES; + + if (self.strokeColor) { + self.shapeLayer.strokeColor = self.strokeColor.CGColor; + self.shapeLayer.lineWidth = self.strokeThickness; + self.shapeLayer.zPosition = 100.f; + } else { + self.shapeLayer.strokeColor = nil; + self.shapeLayer.lineWidth = 0.f; + } + if (self.fillColor) { + self.shapeLayer.fillColor = self.fillColor.CGColor; + } else { + self.shapeLayer.fillColor = nil; + } + + self.shapeLayer.path = bezierPath.CGPath; + + break; + } + case OWSBubbleShapeViewMode_Shadow: + self.clipsToBounds = NO; + + if (self.fillColor) { + self.shapeLayer.fillColor = self.fillColor.CGColor; + } else { + self.shapeLayer.fillColor = nil; + } + + self.shapeLayer.path = bezierPath.CGPath; + self.shapeLayer.frame = self.bounds; + self.shapeLayer.masksToBounds = YES; + + break; + case OWSBubbleShapeViewMode_Clip: + self.maskLayer.path = bezierPath.CGPath; + self.layer.mask = self.maskLayer; + break; + } + + [CATransaction commit]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleStrokeView.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleStrokeView.h deleted file mode 100644 index c02a4b70e..000000000 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleStrokeView.h +++ /dev/null @@ -1,18 +0,0 @@ -// -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. -// - -#import "OWSBubbleView.h" - -NS_ASSUME_NONNULL_BEGIN - -@class OWSBubbleView; - -@interface OWSBubbleStrokeView : UIView - -@property (nonatomic) UIColor *strokeColor; -@property (nonatomic) CGFloat strokeThickness; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleStrokeView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleStrokeView.m deleted file mode 100644 index 415de838b..000000000 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleStrokeView.m +++ /dev/null @@ -1,129 +0,0 @@ -// -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. -// - -#import "OWSBubbleStrokeView.h" -#import "OWSBubbleView.h" -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface OWSBubbleStrokeView () - -@property (nonatomic) CAShapeLayer *shapeLayer; - -@property (nonatomic, weak) OWSBubbleView *bubbleView; - -@end - -#pragma mark - - -@implementation OWSBubbleStrokeView - -- (instancetype)init -{ - self = [super init]; - if (!self) { - return self; - } - - self.opaque = NO; - self.backgroundColor = [UIColor clearColor]; - - self.shapeLayer = [CAShapeLayer new]; - [self.layer addSublayer:self.shapeLayer]; - - 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; - } - - // Prevent the shape layer from animating changes. - [CATransaction begin]; - [CATransaction setDisableActions:YES]; - - // Don't fill the shape layer; we just want a stroke around the border. - self.shapeLayer.fillColor = [UIColor clearColor].CGColor; - - [CATransaction commit]; - - self.clipsToBounds = YES; - - if (!self.bubbleView) { - return; - } - - 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]; - - // Prevent the shape layer from animating changes. - [CATransaction begin]; - [CATransaction setDisableActions:YES]; - - self.shapeLayer.strokeColor = self.strokeColor.CGColor; - self.shapeLayer.lineWidth = self.strokeThickness; - self.shapeLayer.zPosition = 100.f; - self.shapeLayer.path = bezierPath.CGPath; - - [CATransaction commit]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h index bb1a9f4f1..a4d133dc1 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h @@ -24,6 +24,8 @@ extern const CGFloat kOWSMessageCellCornerRadius; @property (nonatomic, nullable) UIColor *bubbleColor; +@property (nonatomic, nullable, weak) id delegate; + - (UIBezierPath *)maskPath; #pragma mark - Coordination diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m index 5ba4bde79..fd1f95d65 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m @@ -160,6 +160,7 @@ const CGFloat kOWSMessageCellCornerRadius = 16; for (id partnerView in self.partnerViews) { [partnerView updateLayers]; } + [self.delegate updateLayers]; } + (CGFloat)minWidth diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m index 5ec04addc..777b80eab 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m @@ -6,7 +6,7 @@ #import "AttachmentUploadView.h" #import "ConversationViewItem.h" #import "OWSAudioMessageView.h" -#import "OWSBubbleStrokeView.h" +#import "OWSBubbleShapeView.h" #import "OWSBubbleView.h" #import "OWSContactShareView.h" #import "OWSGenericAttachmentView.h" @@ -23,6 +23,10 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) OWSBubbleView *bubbleView; +@property (nonatomic) OWSBubbleShapeView *mediaShadowView; + +@property (nonatomic) OWSBubbleShapeView *mediaClipView; + @property (nonatomic) UIStackView *stackView; @property (nonatomic) UILabel *senderNameLabel; @@ -75,6 +79,9 @@ NS_ASSUME_NONNULL_BEGIN [self addSubview:self.bubbleView]; [self.bubbleView autoPinEdgesToSuperviewEdges]; + self.mediaShadowView = [OWSBubbleShapeView bubbleShadowView]; + self.mediaClipView = [OWSBubbleShapeView bubbleClipView]; + self.stackView = [UIStackView new]; self.stackView.axis = UILayoutConstraintAxisVertical; self.stackView.alignment = UIStackViewAlignmentFill; @@ -278,7 +285,7 @@ NS_ASSUME_NONNULL_BEGIN } UIView *_Nullable bodyMediaView = nil; - BOOL bodyMediaViewHasGreedyWidth = NO; + BOOL hasThumbnailForBodyMedia = NO; switch (self.cellType) { case OWSMessageCellType_Unknown: case OWSMessageCellType_TextMessage: @@ -287,41 +294,37 @@ NS_ASSUME_NONNULL_BEGIN case OWSMessageCellType_StillImage: OWSAssert(self.viewItem.attachmentStream); bodyMediaView = [self loadViewForStillImage]; + hasThumbnailForBodyMedia = YES; break; case OWSMessageCellType_AnimatedImage: OWSAssert(self.viewItem.attachmentStream); bodyMediaView = [self loadViewForAnimatedImage]; + hasThumbnailForBodyMedia = YES; break; case OWSMessageCellType_Video: OWSAssert(self.viewItem.attachmentStream); bodyMediaView = [self loadViewForVideo]; + hasThumbnailForBodyMedia = YES; break; case OWSMessageCellType_Audio: OWSAssert(self.viewItem.attachmentStream); bodyMediaView = [self loadViewForAudio]; - bodyMediaViewHasGreedyWidth = YES; break; case OWSMessageCellType_GenericAttachment: bodyMediaView = [self loadViewForGenericAttachment]; - bodyMediaViewHasGreedyWidth = YES; break; case OWSMessageCellType_DownloadingAttachment: bodyMediaView = [self loadViewForDownloadingAttachment]; - bodyMediaViewHasGreedyWidth = YES; break; case OWSMessageCellType_ContactShare: bodyMediaView = [self loadViewForContactShare]; - bodyMediaViewHasGreedyWidth = YES; break; } - BOOL shouldFooterOverlayMedia = NO; if (bodyMediaView) { OWSAssert(self.loadCellContentBlock); OWSAssert(self.unloadCellContentBlock); - shouldFooterOverlayMedia = self.canFooterOverlayMedia; - // Flush any pending "text" subviews. [self insertAnyTextViewsIntoStackView:textViews]; [textViews removeAllObjects]; @@ -334,22 +337,40 @@ NS_ASSUME_NONNULL_BEGIN bodyMediaView.layer.opacity = 0.75f; } - [self.stackView addArrangedSubview:bodyMediaView]; + if (hasThumbnailForBodyMedia) { + // The "body media" view casts a shadow "downward" onto adjacent views, + // so we use a "proxy" view to take its place within the v-stack + // view and then insert the body media view above its proxy so that + // it floats above the other content of the bubble view. - BOOL shouldStrokeMediaView = ([bodyMediaView isKindOfClass:[UIImageView class]] || - [bodyMediaView isKindOfClass:[OWSContactShareView class]]); - if (shouldStrokeMediaView) { - OWSBubbleStrokeView *bubbleStrokeView = [OWSBubbleStrokeView new]; - bubbleStrokeView.strokeThickness = 1.f; - bubbleStrokeView.strokeColor = [UIColor colorWithWhite:0.f alpha:0.1f]; + UIView *bodyProxyView = [UIView new]; + [self.stackView addArrangedSubview:bodyProxyView]; - [self.bubbleView addSubview:bubbleStrokeView]; - [bubbleStrokeView autoPinEdge:ALEdgeTop toEdge:ALEdgeTop ofView:bodyMediaView]; - [bubbleStrokeView autoPinEdge:ALEdgeBottom toEdge:ALEdgeBottom ofView:bodyMediaView]; - [bubbleStrokeView autoPinEdge:ALEdgeLeft toEdge:ALEdgeLeft ofView:bodyMediaView]; - [bubbleStrokeView autoPinEdge:ALEdgeRight toEdge:ALEdgeRight ofView:bodyMediaView]; + [self addSubview:self.mediaShadowView]; + [self.mediaShadowView autoPinEdge:ALEdgeTop toEdge:ALEdgeTop ofView:bodyProxyView]; + [self.mediaShadowView autoPinEdge:ALEdgeBottom toEdge:ALEdgeBottom ofView:bodyProxyView]; + [self.mediaShadowView autoPinEdge:ALEdgeLeading toEdge:ALEdgeLeading ofView:bodyProxyView]; + [self.mediaShadowView autoPinEdge:ALEdgeTrailing toEdge:ALEdgeTrailing ofView:bodyProxyView]; - [self.bubbleView addPartnerView:bubbleStrokeView]; + [self.mediaShadowView addSubview:self.mediaClipView]; + [self.mediaClipView autoPinToSuperviewEdges]; + + [self.mediaClipView addSubview:bodyMediaView]; + [bodyMediaView autoPinToSuperviewEdges]; + + [self.bubbleView addPartnerView:self.mediaClipView]; + [self.bubbleView addPartnerView:self.mediaShadowView]; + + // TODO: Constants + // TODO: What's the difference between an inner and outer shadow? + self.mediaShadowView.fillColor = self.bubbleColor; + self.mediaShadowView.layer.shadowColor = [UIColor blackColor].CGColor; + self.mediaShadowView.layer.shadowOpacity = 0.08f; + self.mediaShadowView.layer.shadowOpacity = 1.f; + self.mediaShadowView.layer.shadowOffset = CGSizeMake(0.f, 2.f); + self.mediaShadowView.layer.shadowRadius = 8.f; + } else { + [self.stackView addArrangedSubview:bodyMediaView]; } } @@ -372,6 +393,7 @@ NS_ASSUME_NONNULL_BEGIN } } + BOOL shouldFooterOverlayMedia = (self.canFooterOverlayMedia && bodyMediaView && !self.hasBodyText); if (self.viewItem.shouldHideFooter) { // Do nothing. } else if (shouldFooterOverlayMedia) { @@ -424,8 +446,7 @@ NS_ASSUME_NONNULL_BEGIN break; } if (!hasOnlyBodyMediaView) { - TSMessage *message = (TSMessage *)self.viewItem.interaction; - self.bubbleView.bubbleColor = [self.bubbleFactory bubbleColorWithMessage:message]; + self.bubbleView.bubbleColor = self.bubbleColor; } 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. @@ -433,6 +454,14 @@ NS_ASSUME_NONNULL_BEGIN } } +- (UIColor *)bubbleColor +{ + OWSAssert([self.viewItem.interaction isKindOfClass:[TSMessage class]]); + + TSMessage *message = (TSMessage *)self.viewItem.interaction; + return [self.bubbleFactory bubbleColorWithMessage:message]; +} + - (BOOL)canFooterOverlayMedia { switch (self.cellType) { @@ -1089,7 +1118,8 @@ NS_ASSUME_NONNULL_BEGIN // TODO: Update this to reflect generic attachment, downloading attachments and // contact shares. - if (!self.viewItem.shouldHideFooter && !self.canFooterOverlayMedia) { + BOOL shouldFooterOverlayMedia = (self.canFooterOverlayMedia && !self.hasBodyText); + if (!self.viewItem.shouldHideFooter && !shouldFooterOverlayMedia) { CGSize footerSize = [self.footerView measureWithConversationViewItem:self.viewItem]; cellSize.width = MAX(cellSize.width, footerSize.width + self.conversationStyle.textInsetHorizontal * 2); cellSize.height += self.textViewVSpacing + footerSize.height; @@ -1174,6 +1204,9 @@ NS_ASSUME_NONNULL_BEGIN [self.quotedMessageView removeFromSuperview]; self.quotedMessageView = nil; + [self.mediaShadowView removeFromSuperview]; + [self.mediaClipView removeFromSuperview]; + [self.footerView removeFromSuperview]; for (UIView *subview in self.stackView.subviews) { diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSQuotedMessageView.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSQuotedMessageView.h index b5aaf1700..f2b94469e 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSQuotedMessageView.h +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSQuotedMessageView.h @@ -5,7 +5,7 @@ NS_ASSUME_NONNULL_BEGIN @class DisplayableText; -@class OWSBubbleStrokeView; +@class OWSBubbleShapeView; @class OWSQuotedReplyModel; @class TSAttachmentPointer; @class TSQuotedMessage; @@ -19,7 +19,7 @@ NS_ASSUME_NONNULL_BEGIN @interface OWSQuotedMessageView : UIView -@property (nonatomic, nullable, readonly) OWSBubbleStrokeView *boundsStrokeView; +@property (nonatomic, nullable, readonly) OWSBubbleShapeView *boundsStrokeView; @property (nonatomic, nullable, weak) id delegate; - (instancetype)init NS_UNAVAILABLE; diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSQuotedMessageView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSQuotedMessageView.m index 972955740..4c194cdfe 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSQuotedMessageView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSQuotedMessageView.m @@ -5,7 +5,7 @@ #import "OWSQuotedMessageView.h" #import "ConversationViewItem.h" #import "Environment.h" -#import "OWSBubbleStrokeView.h" +#import "OWSBubbleShapeView.h" #import "Signal-Swift.h" #import #import @@ -22,7 +22,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) OWSQuotedReplyModel *quotedMessage; @property (nonatomic, nullable, readonly) DisplayableText *displayableQuotedText; -@property (nonatomic, nullable) OWSBubbleStrokeView *boundsStrokeView; +@property (nonatomic, nullable) OWSBubbleShapeView *boundsStrokeView; @property (nonatomic, readonly) BOOL isForPreview; @property (nonatomic, readonly) BOOL isOutgoing; @@ -119,7 +119,7 @@ NS_ASSUME_NONNULL_BEGIN self.layoutMargins = UIEdgeInsetsZero; self.clipsToBounds = YES; - self.boundsStrokeView = [OWSBubbleStrokeView new]; + self.boundsStrokeView = [OWSBubbleShapeView new]; self.boundsStrokeView.strokeColor = OWSMessagesBubbleImageFactory.bubbleColorIncoming; self.boundsStrokeView.strokeThickness = 1.f; [self addSubview:self.boundsStrokeView];