diff --git a/Signal/src/ViewControllers/ConversationView/ConversationInputTextView.m b/Signal/src/ViewControllers/ConversationView/ConversationInputTextView.m index 314330367..a23327590 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationInputTextView.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationInputTextView.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // #import "ConversationInputTextView.h" @@ -26,12 +26,7 @@ NS_ASSUME_NONNULL_BEGIN [self setTranslatesAutoresizingMaskIntoConstraints:NO]; self.delegate = self; - - self.backgroundColor = (Theme.isDarkThemeEnabled ? UIColor.ows_gray90Color : UIColor.ows_gray02Color); - self.layer.borderColor - = (Theme.isDarkThemeEnabled ? [Theme.primaryColor colorWithAlphaComponent:0.06f].CGColor - : [Theme.primaryColor colorWithAlphaComponent:0.12f].CGColor); - self.layer.borderWidth = 0.5f; + self.backgroundColor = nil; self.scrollIndicatorInsets = UIEdgeInsetsMake(4, 4, 4, 4); @@ -56,7 +51,14 @@ NS_ASSUME_NONNULL_BEGIN // We need to do these steps _after_ placeholderView is configured. self.font = [UIFont ows_dynamicTypeBodyFont]; - self.textContainerInset = UIEdgeInsetsMake(7.0f, 12.0f, 7.0f, 12.0f); + CGFloat hMarginLeading = 12.f; + CGFloat hMarginTrailing = 24.f; + self.textContainerInset = UIEdgeInsetsMake(7.f, + CurrentAppContext().isRTL ? hMarginTrailing : hMarginLeading, + 7.f, + CurrentAppContext().isRTL ? hMarginLeading : hMarginTrailing); + self.textContainer.lineFragmentPadding = 0; + self.contentInset = UIEdgeInsetsZero; [self ensurePlaceholderConstraints]; [self updatePlaceholderVisibility]; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m index a29d04d30..046327a69 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m @@ -48,19 +48,17 @@ const CGFloat kMaxTextViewHeight = 98; @property (nonatomic, readonly) ConversationStyle *conversationStyle; @property (nonatomic, readonly) ConversationInputTextView *inputTextView; -@property (nonatomic, readonly) UIStackView *contentRows; -@property (nonatomic, readonly) UIStackView *composeRow; +@property (nonatomic, readonly) UIStackView *hStack; +@property (nonatomic, readonly) UIStackView *vStack; @property (nonatomic, readonly) UIButton *attachmentButton; @property (nonatomic, readonly) UIButton *sendButton; @property (nonatomic, readonly) UIButton *voiceMemoButton; +@property (nonatomic, readonly) UIView *quotedReplyWrapper; +@property (nonatomic, readonly) UIView *linkPreviewWrapper; @property (nonatomic) CGFloat textViewHeight; @property (nonatomic, readonly) NSLayoutConstraint *textViewHeightConstraint; -#pragma mark - - -@property (nonatomic, nullable) UIView *quotedMessagePreview; - #pragma mark - Voice Memo Recording UI @property (nonatomic, nullable) UIView *voiceMemoUI; @@ -73,7 +71,6 @@ const CGFloat kMaxTextViewHeight = 98; @property (nonatomic, nullable) NSArray *layoutContraints; @property (nonatomic) UIEdgeInsets receivedSafeAreaInsets; @property (nonatomic, nullable) InputLinkPreview *inputLinkPreview; -@property (nonatomic, nullable) LinkPreviewView *linkPreviewView; @property (nonatomic) BOOL wasLinkPreviewCancelled; @end @@ -122,7 +119,6 @@ const CGFloat kMaxTextViewHeight = 98; self.autoresizingMask = UIViewAutoresizingFlexibleHeight; _inputTextView = [ConversationInputTextView new]; - self.inputTextView.layer.cornerRadius = kMinTextViewHeight / 2.0f; self.inputTextView.textViewToolbarDelegate = self; self.inputTextView.font = [UIFont ows_dynamicTypeBodyFont]; [self.inputTextView setContentHuggingHorizontalLow]; @@ -171,31 +167,55 @@ const CGFloat kMaxTextViewHeight = 98; self.userInteractionEnabled = YES; - _composeRow = [[UIStackView alloc] - initWithArrangedSubviews:@[ self.attachmentButton, self.inputTextView, self.voiceMemoButton, self.sendButton ]]; - self.composeRow.axis = UILayoutConstraintAxisHorizontal; - self.composeRow.layoutMarginsRelativeArrangement = YES; - self.composeRow.layoutMargins = UIEdgeInsetsMake(6, 6, 6, 6); - self.composeRow.alignment = UIStackViewAlignmentBottom; - self.composeRow.spacing = 8; + _quotedReplyWrapper = [UIView containerView]; + self.quotedReplyWrapper.hidden = YES; - _contentRows = [[UIStackView alloc] initWithArrangedSubviews:@[ self.composeRow ]]; - self.contentRows.axis = UILayoutConstraintAxisVertical; + _linkPreviewWrapper = [UIView containerView]; + self.linkPreviewWrapper.hidden = YES; - [self addSubview:self.contentRows]; - [self.contentRows autoPinEdgeToSuperviewEdge:ALEdgeTop]; - [self.contentRows autoPinEdgeToSuperviewSafeArea:ALEdgeBottom]; + _vStack = [[UIStackView alloc] + initWithArrangedSubviews:@[ self.quotedReplyWrapper, self.linkPreviewWrapper, self.inputTextView ]]; + self.vStack.axis = UILayoutConstraintAxisVertical; + + _hStack = [[UIStackView alloc] + initWithArrangedSubviews:@[ self.attachmentButton, self.vStack, self.voiceMemoButton, self.sendButton ]]; + self.hStack.axis = UILayoutConstraintAxisHorizontal; + self.hStack.layoutMarginsRelativeArrangement = YES; + self.hStack.layoutMargins = UIEdgeInsetsMake(6, 6, 6, 6); + self.hStack.alignment = UIStackViewAlignmentBottom; + self.hStack.spacing = 8; + + [self addSubview:self.hStack]; + [self.hStack autoPinEdgeToSuperviewEdge:ALEdgeTop]; + [self.hStack autoPinEdgeToSuperviewSafeArea:ALEdgeBottom]; // See comments on updateContentLayout:. if (@available(iOS 11, *)) { - self.contentRows.insetsLayoutMarginsFromSafeArea = NO; - self.composeRow.insetsLayoutMarginsFromSafeArea = NO; + self.vStack.insetsLayoutMarginsFromSafeArea = NO; + self.hStack.insetsLayoutMarginsFromSafeArea = NO; self.insetsLayoutMarginsFromSafeArea = NO; } - self.contentRows.preservesSuperviewLayoutMargins = NO; - self.composeRow.preservesSuperviewLayoutMargins = NO; + self.vStack.preservesSuperviewLayoutMargins = NO; + self.hStack.preservesSuperviewLayoutMargins = NO; self.preservesSuperviewLayoutMargins = NO; + OWSLayerView *contentBorderView = [[OWSLayerView alloc] init]; + contentBorderView.userInteractionEnabled = NO; + contentBorderView.backgroundColor = nil; + CAShapeLayer *contentBorderLayer = [CAShapeLayer new]; + contentBorderLayer.strokeColor = Theme.secondaryColor.CGColor; + contentBorderLayer.fillColor = nil; + contentBorderLayer.lineWidth = CGHairlineWidth(); + [contentBorderView.layer addSublayer:contentBorderLayer]; + contentBorderView.layoutCallback = ^(UIView *layerView) { + contentBorderLayer.frame = layerView.bounds; + contentBorderLayer.path = [UIBezierPath bezierPathWithRoundedRect:layerView.bounds cornerRadius:18.f].CGPath; + }; + [self.vStack addSubview:contentBorderView]; + [contentBorderView autoPinEdgesToSuperviewEdges]; + [contentBorderView setCompressionResistanceLow]; + [contentBorderView setContentHuggingLow]; + [self ensureShouldShowVoiceMemoButtonAnimated:NO doLayout:NO]; } @@ -268,15 +288,11 @@ const CGFloat kMaxTextViewHeight = 98; return; } - if (self.quotedMessagePreview) { - [self clearQuotedMessagePreview]; - } - OWSAssertDebug(self.quotedMessagePreview == nil); + [self clearQuotedMessagePreview]; _quotedReply = quotedReply; if (!quotedReply) { - [self clearQuotedMessagePreview]; return; } @@ -284,14 +300,10 @@ const CGFloat kMaxTextViewHeight = 98; [[QuotedReplyPreview alloc] initWithQuotedReply:quotedReply conversationStyle:self.conversationStyle]; quotedMessagePreview.delegate = self; - UIView *wrapper = [UIView containerView]; - wrapper.layoutMargins = UIEdgeInsetsMake(self.quotedMessageTopMargin, 0, 0, 0); - [wrapper addSubview:quotedMessagePreview]; + self.quotedReplyWrapper.hidden = NO; + self.quotedReplyWrapper.layoutMargins = UIEdgeInsetsMake(self.quotedMessageTopMargin, 0, 0, 0); + [self.quotedReplyWrapper addSubview:quotedMessagePreview]; [quotedMessagePreview ows_autoPinToSuperviewMargins]; - - [self.contentRows insertArrangedSubview:wrapper atIndex:0]; - - self.quotedMessagePreview = wrapper; } - (CGFloat)quotedMessageTopMargin @@ -301,10 +313,9 @@ const CGFloat kMaxTextViewHeight = 98; - (void)clearQuotedMessagePreview { - if (self.quotedMessagePreview) { - [self.contentRows removeArrangedSubview:self.quotedMessagePreview]; - [self.quotedMessagePreview removeFromSuperview]; - self.quotedMessagePreview = nil; + self.quotedReplyWrapper.hidden = YES; + for (UIView *subview in self.quotedReplyWrapper.subviews) { + [subview removeFromSuperview]; } } @@ -367,8 +378,8 @@ const CGFloat kMaxTextViewHeight = 98; } self.layoutContraints = @[ - [self.contentRows autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:self.receivedSafeAreaInsets.left], - [self.contentRows autoPinEdgeToSuperviewEdge:ALEdgeRight withInset:self.receivedSafeAreaInsets.right], + [self.hStack autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:self.receivedSafeAreaInsets.left], + [self.hStack autoPinEdgeToSuperviewEdge:ALEdgeRight withInset:self.receivedSafeAreaInsets.right], ]; } @@ -774,9 +785,10 @@ const CGFloat kMaxTextViewHeight = 98; LinkPreviewView *linkPreviewView = [[LinkPreviewView alloc] initWithDelegate:self]; linkPreviewView.state = state; - self.linkPreviewView = linkPreviewView; - // TODO: Revisit once we have a separate quoted reply view. - [self.contentRows insertArrangedSubview:linkPreviewView atIndex:0]; + + self.linkPreviewWrapper.hidden = NO; + [self.linkPreviewWrapper addSubview:linkPreviewView]; + [linkPreviewView ows_autoPinToSuperviewMargins]; } - (void)clearLinkPreviewView @@ -784,8 +796,10 @@ const CGFloat kMaxTextViewHeight = 98; OWSAssertIsOnMainThread(); // Clear old link preview state. - [self.linkPreviewView removeFromSuperview]; - self.linkPreviewView = nil; + for (UIView *subview in self.linkPreviewWrapper.subviews) { + [subview removeFromSuperview]; + } + self.linkPreviewWrapper.hidden = YES; } - (nullable OWSLinkPreviewDraft *)linkPreviewDraft diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index cf729613f..2c3d14173 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -3991,7 +3991,6 @@ typedef enum : NSUInteger { [self messageWasSent:message]; - dispatch_async(dispatch_get_main_queue(), ^{ [BenchManager benchWithTitle:@"toggleDefaultKeyboard" block:^{ diff --git a/Signal/src/views/LinkPreviewView.swift b/Signal/src/views/LinkPreviewView.swift index 953eacca6..d71b2f18a 100644 --- a/Signal/src/views/LinkPreviewView.swift +++ b/Signal/src/views/LinkPreviewView.swift @@ -2,6 +2,18 @@ // Copyright (c) 2019 Open Whisper Systems. All rights reserved. // +public extension CGPoint { + public func plusX(_ value: CGFloat) -> CGPoint { + return CGPoint(x: x + value, y: y) + } + + public func plusY(_ value: CGFloat) -> CGPoint { + return CGPoint(x: x, y: y + value) + } +} + +// MARK: - + @objc public enum LinkPreviewImageState: Int { case none @@ -213,6 +225,90 @@ public protocol LinkPreviewViewDelegate { // MARK: - +@objc +public class LinkPreviewImageView: UIImageView { + private let maskLayer = CAShapeLayer() + + @objc + public init() { + super.init(frame: .zero) + + self.layer.mask = maskLayer + } + + public required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } + + public override var bounds: CGRect { + didSet { + updateMaskLayer() + } + } + + public override var frame: CGRect { + didSet { + updateMaskLayer() + } + } + + public override var center: CGPoint { + didSet { + updateMaskLayer() + } + } + + private func updateMaskLayer() { + let layerBounds = self.bounds + + // One of the corners has assymetrical rounding to match the input toolbar border. + // This is somewhat inconvenient. + let upperLeft = CGPoint(x: 0, y: 0) + let upperRight = CGPoint(x: layerBounds.size.width, y: 0) + let lowerRight = CGPoint(x: layerBounds.size.width, y: layerBounds.size.height) + let lowerLeft = CGPoint(x: 0, y: layerBounds.size.height) + + let bigRounding: CGFloat = 14 + let smallRounding: CGFloat = 4 + + let upperLeftRounding = CurrentAppContext().isRTL ? smallRounding : bigRounding + let upperRightRounding = CurrentAppContext().isRTL ? bigRounding : smallRounding + let lowerRightRounding = smallRounding + let lowerLeftRounding = smallRounding + + let path = UIBezierPath() + + // It's sufficient to "draw" the rounded corners and not the edges that connect them. + path.addArc(withCenter: upperLeft.plusX(+upperLeftRounding).plusY(+upperLeftRounding), + radius: upperLeftRounding, + startAngle: CGFloat.pi * 1.0, + endAngle: CGFloat.pi * 1.5, + clockwise: true) + + path.addArc(withCenter: upperRight.plusX(-upperRightRounding).plusY(+upperRightRounding), + radius: upperRightRounding, + startAngle: CGFloat.pi * 1.5, + endAngle: CGFloat.pi * 0.0, + clockwise: true) + + path.addArc(withCenter: lowerRight.plusX(-lowerRightRounding).plusY(-lowerRightRounding), + radius: lowerRightRounding, + startAngle: CGFloat.pi * 0.0, + endAngle: CGFloat.pi * 0.5, + clockwise: true) + + path.addArc(withCenter: lowerLeft.plusX(+lowerLeftRounding).plusY(-lowerLeftRounding), + radius: lowerLeftRounding, + startAngle: CGFloat.pi * 0.5, + endAngle: CGFloat.pi * 1.0, + clockwise: true) + + maskLayer.path = path.cgPath + } +} + +// MARK: - + @objc public class LinkPreviewView: UIStackView { private weak var delegate: LinkPreviewViewDelegate? @@ -436,21 +532,27 @@ public class LinkPreviewView: UIStackView { return label } - private let approvalHeight: CGFloat = 76 + private let approvalHeight: CGFloat = 72 + private let approvalMarginTop: CGFloat = 6 private func createApprovalContents(state: LinkPreviewState) { self.axis = .horizontal self.alignment = .fill self.distribution = .fill self.spacing = 8 + self.isLayoutMarginsRelativeArrangement = true + let hMarginLeading: CGFloat = 6 + let hMarginTrailing: CGFloat = 12 + self.layoutMargins = UIEdgeInsets(top: approvalMarginTop, + left: CurrentAppContext().isRTL ? hMarginTrailing : hMarginLeading, + bottom: 0, + right: CurrentAppContext().isRTL ? hMarginLeading : hMarginTrailing) - NSLayoutConstraint.autoSetPriority(UILayoutPriority.defaultHigh) { - self.layoutConstraints.append(self.autoSetDimension(.height, toSize: approvalHeight)) - } + self.layoutConstraints.append(self.autoSetDimension(.height, toSize: approvalHeight + approvalMarginTop)) // Image - if let imageView = createImageView(state: state) { + if let imageView = createApprovalImageView(state: state) { imageView.contentMode = .scaleAspectFill imageView.autoPinToSquareAspectRatio() let imageSize = approvalHeight @@ -458,7 +560,6 @@ public class LinkPreviewView: UIStackView { imageView.setContentHuggingHigh() imageView.setCompressionResistanceHigh() imageView.clipsToBounds = true - // TODO: Cropping, stroke. addArrangedSubview(imageView) } @@ -554,13 +655,29 @@ public class LinkPreviewView: UIStackView { return imageView } + private func createApprovalImageView(state: LinkPreviewState) -> UIImageView? { + guard state.isLoaded() else { + owsFailDebug("State not loaded.") + return nil + } + + guard state.imageState() == .loaded else { + return nil + } + guard let image = state.image() else { + owsFailDebug("Could not load image.") + return nil + } + let imageView = LinkPreviewImageView() + imageView.image = image + return imageView + } + private func createLoadingContents() { self.axis = .vertical self.alignment = .center - NSLayoutConstraint.autoSetPriority(UILayoutPriority.defaultHigh) { - self.layoutConstraints.append(self.autoSetDimension(.height, toSize: approvalHeight)) - } + self.layoutConstraints.append(self.autoSetDimension(.height, toSize: approvalHeight + approvalMarginTop)) let activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray) activityIndicator.startAnimating() diff --git a/SignalMessaging/Views/OWSLayerView.swift b/SignalMessaging/Views/OWSLayerView.swift index b62b7ec19..0c1cd4b1a 100644 --- a/SignalMessaging/Views/OWSLayerView.swift +++ b/SignalMessaging/Views/OWSLayerView.swift @@ -1,11 +1,12 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // import Foundation @objc public class OWSLayerView: UIView { + @objc public var layoutCallback: ((UIView) -> Void) @objc