diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m index 5ba4bde79..db7795629 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m @@ -29,6 +29,8 @@ const CGFloat kOWSMessageCellCornerRadius = 16; return self; } + self.layoutMargins = UIEdgeInsetsZero; + self.shapeLayer = [CAShapeLayer new]; [self.layer addSublayer:self.shapeLayer]; diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m index 9116fd033..381353023 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m @@ -27,6 +27,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) OWSBubbleShapeView *mediaClipView; +@property (nonatomic) OWSBubbleShapeView *bubbleStrokeView; + @property (nonatomic) UIStackView *stackView; @property (nonatomic) UILabel *senderNameLabel; @@ -81,6 +83,7 @@ NS_ASSUME_NONNULL_BEGIN self.mediaShadowView = [OWSBubbleShapeView bubbleShadowView]; self.mediaClipView = [OWSBubbleShapeView bubbleClipView]; + self.bubbleStrokeView = [OWSBubbleShapeView bubbleDrawView]; self.stackView = [UIStackView new]; self.stackView.axis = UILayoutConstraintAxisVertical; @@ -333,44 +336,57 @@ NS_ASSUME_NONNULL_BEGIN bodyMediaView.layer.opacity = 0.75f; } - if (self.hasBodyMediaWithThumbnail) { + if (self.hasFullWidthMediaView) { // Flush any pending "text" subviews. OWSAssert(!topTextStackView); topTextStackView = [self insertAnyTextViewsIntoStackView:textViews]; [textViews removeAllObjects]; - // 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. + if (self.hasBodyMediaWithThumbnail) { + + // 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. - UIView *bodyProxyView = [UIView new]; - [self.stackView addArrangedSubview:bodyProxyView]; + UIView *bodyProxyView = [UIView new]; + [self.stackView addArrangedSubview:bodyProxyView]; - [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 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.mediaShadowView addSubview:self.mediaClipView]; - [self.mediaClipView autoPinToSuperviewEdges]; + [self.mediaShadowView addSubview:self.mediaClipView]; + [self.mediaClipView autoPinToSuperviewEdges]; - [self.mediaClipView addSubview:bodyMediaView]; - [bodyMediaView autoPinToSuperviewEdges]; + [self.mediaClipView addSubview:bodyMediaView]; + [bodyMediaView autoPinToSuperviewEdges]; - [self.bubbleView addPartnerView:self.mediaClipView]; - [self.bubbleView addPartnerView:self.mediaShadowView]; + [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.12f; + self.mediaShadowView.layer.shadowOffset = CGSizeMake(0.f, 0.f); + self.mediaShadowView.layer.shadowRadius = 0.5f; + } else { + OWSAssert(self.cellType == OWSMessageCellType_ContactShare); - // 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.12f; - self.mediaShadowView.layer.shadowOffset = CGSizeMake(0.f, 0.f); - self.mediaShadowView.layer.shadowRadius = 0.5f; + [self.stackView addArrangedSubview:bodyMediaView]; + + // TODO: Constants. + self.bubbleStrokeView.strokeColor = [UIColor lightGrayColor]; + self.bubbleStrokeView.strokeThickness = 1.f; + [self.bubbleView addSubview:self.bubbleStrokeView]; + [self.bubbleStrokeView autoPinToSuperviewEdges]; + [self.bubbleView addPartnerView:self.bubbleStrokeView]; + } } else { - // [self.stackView addArrangedSubview:bodyMediaView]; [textViews addObject:bodyMediaView]; } } @@ -432,7 +448,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)updateBubbleColor { - BOOL hasOnlyBodyMediaView = ([self hasBodyMediaWithThumbnail] && self.stackView.subviews.count == 1); + BOOL hasOnlyBodyMediaView = ([self hasFullWidthMediaView] && self.stackView.subviews.count == 1); if (!hasOnlyBodyMediaView) { self.bubbleView.bubbleColor = self.bubbleColor; } else { @@ -469,11 +485,25 @@ NS_ASSUME_NONNULL_BEGIN } } +- (BOOL)hasFullWidthMediaView +{ + return (self.hasBodyMediaWithThumbnail || self.cellType == OWSMessageCellType_ContactShare); +} + +// Returns YES if there is a footer displayed _at the bottom_ +// of the message bubble (as opposed to overlaid on a body media +// thumbnail). - (BOOL)canFooterOverlayMedia { return self.hasBodyMediaWithThumbnail; } +- (BOOL)hasBottomFooter +{ + BOOL shouldFooterOverlayMedia = (self.canFooterOverlayMedia && !self.hasBodyText); + return !self.viewItem.shouldHideFooter && !shouldFooterOverlayMedia; +} + - (nullable UIView *)insertAnyTextViewsIntoStackView:(NSArray *)textViews { if (textViews.count < 1) { @@ -1080,7 +1110,7 @@ NS_ASSUME_NONNULL_BEGIN NSValue *_Nullable bodyMediaSize = [self bodyMediaSize]; if (bodyMediaSize) { - if (self.hasBodyMediaWithThumbnail) { + if (self.hasFullWidthMediaView) { cellSize.width = MAX(cellSize.width, bodyMediaSize.CGSizeValue.width); cellSize.height += bodyMediaSize.CGSizeValue.height; } else { @@ -1104,8 +1134,7 @@ NS_ASSUME_NONNULL_BEGIN [textViewSizes addObject:bodyTextSize]; } - BOOL shouldFooterOverlayMedia = (self.canFooterOverlayMedia && !self.hasBodyText); - if (!self.viewItem.shouldHideFooter && !shouldFooterOverlayMedia) { + if (self.hasBottomFooter) { CGSize footerSize = [self.footerView measureWithConversationViewItem:self.viewItem]; [textViewSizes addObject:[NSValue valueWithCGSize:footerSize]]; }