diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m index 328e93c50..67804e704 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m @@ -363,6 +363,13 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes } if (self.viewItem.linkPreview) { + if (self.isQuotedReply) { + UIView *spacerView = [UIView containerView]; + [spacerView autoSetDimension:ALDimensionHeight toSize:self.bodyMediaQuotedReplyVSpacing]; + [spacerView setCompressionResistanceHigh]; + [self.stackView addArrangedSubview:spacerView]; + } + self.linkPreviewView.state = self.linkPreviewState; [self.stackView addArrangedSubview:self.linkPreviewView]; [self.linkPreviewView addBorderViewsWithBubbleView:self.bubbleView]; @@ -1175,6 +1182,8 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes if (bodyMediaSize && quotedMessageSize && self.hasFullWidthMediaView) { cellSize.height += self.bodyMediaQuotedReplyVSpacing; + } else if (quotedMessageSize && self.viewItem.linkPreview) { + cellSize.height += self.bodyMediaQuotedReplyVSpacing; } } diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSQuotedMessageView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSQuotedMessageView.m index fe9798308..f92cf6ba5 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSQuotedMessageView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSQuotedMessageView.m @@ -130,7 +130,7 @@ const CGFloat kRemotelySourcedContentRowSpacing = 3; - (CGFloat)bubbleHMargin { - return 6.f; + return (self.isForPreview ? 0.f : 6.f); } - (CGFloat)hSpacing @@ -203,7 +203,12 @@ const CGFloat kRemotelySourcedContentRowSpacing = 3; hStackView.spacing = self.hSpacing; UIView *stripeView = [UIView new]; - stripeView.backgroundColor = [self.conversationStyle quotedReplyStripeColorWithIsIncoming:!self.isOutgoing]; + if (self.isForPreview) { + // TODO: + stripeView.backgroundColor = [self.conversationStyle quotedReplyStripeColorWithIsIncoming:YES]; + } else { + stripeView.backgroundColor = [self.conversationStyle quotedReplyStripeColorWithIsIncoming:!self.isOutgoing]; + } [stripeView autoSetDimension:ALDimensionWidth toSize:self.stripeThickness]; [stripeView setContentHuggingHigh]; [stripeView setCompressionResistanceHigh]; @@ -214,6 +219,8 @@ const CGFloat kRemotelySourcedContentRowSpacing = 3; vStackView.layoutMargins = UIEdgeInsetsMake(self.textVMargin, 0, self.textVMargin, 0); vStackView.layoutMarginsRelativeArrangement = YES; vStackView.spacing = self.vSpacing; + [vStackView setContentHuggingHorizontalLow]; + [vStackView setCompressionResistanceHorizontalLow]; [hStackView addArrangedSubview:vStackView]; UILabel *quotedAuthorLabel = [self configureQuotedAuthorLabel]; @@ -275,7 +282,7 @@ const CGFloat kRemotelySourcedContentRowSpacing = 3; quotedAttachmentView = wrapper; } - [quotedAttachmentView autoSetDimension:ALDimensionWidth toSize:self.quotedAttachmentSize]; + [quotedAttachmentView autoPinToSquareAspectRatio]; [quotedAttachmentView setContentHuggingHigh]; [quotedAttachmentView setCompressionResistanceHigh]; [hStackView addArrangedSubview:quotedAttachmentView]; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m index 6131298b1..64a9d9b31 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m @@ -73,6 +73,7 @@ const CGFloat kMaxTextViewHeight = 98; @property (nonatomic) UIEdgeInsets receivedSafeAreaInsets; @property (nonatomic, nullable) InputLinkPreview *inputLinkPreview; @property (nonatomic) BOOL wasLinkPreviewCancelled; +@property (nonatomic, nullable, weak) LinkPreviewView *linkPreviewView; @end @@ -122,6 +123,7 @@ const CGFloat kMaxTextViewHeight = 98; _inputTextView = [ConversationInputTextView new]; self.inputTextView.textViewToolbarDelegate = self; self.inputTextView.font = [UIFont ows_dynamicTypeBodyFont]; + self.inputTextView.backgroundColor = Theme.toolbarBackgroundColor; [self.inputTextView setContentHuggingHorizontalLow]; _textViewHeightConstraint = [self.inputTextView autoSetDimension:ALDimensionHeight toSize:kMinTextViewHeight]; @@ -302,9 +304,11 @@ const CGFloat kMaxTextViewHeight = 98; [quotedMessagePreview setCompressionResistanceHorizontalLow]; self.quotedReplyWrapper.hidden = NO; - self.quotedReplyWrapper.layoutMargins = UIEdgeInsetsMake(self.quotedMessageTopMargin, 0, 0, 0); + self.quotedReplyWrapper.layoutMargins = UIEdgeInsetsZero; [self.quotedReplyWrapper addSubview:quotedMessagePreview]; [quotedMessagePreview ows_autoPinToSuperviewMargins]; + + self.linkPreviewView.hasAsymmetricalRounding = !self.quotedReply; } - (CGFloat)quotedMessageTopMargin @@ -715,31 +719,27 @@ const CGFloat kMaxTextViewHeight = 98; OWSAssertIsOnMainThread(); if (self.wasLinkPreviewCancelled) { - self.inputLinkPreview = nil; - [self clearLinkPreviewView]; + [self clearLinkPreviewStateAndView]; return; } NSString *body = [[self messageText] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; if (body.length < 1) { - self.inputLinkPreview = nil; - [self clearLinkPreviewView]; + [self clearLinkPreviewStateAndView]; self.wasLinkPreviewCancelled = NO; return; } // Don't include link previews for oversize text messages. if ([body lengthOfBytesUsingEncoding:NSUTF8StringEncoding] >= kOversizeTextMessageSizeThreshold) { - self.inputLinkPreview = nil; - [self clearLinkPreviewView]; + [self clearLinkPreviewStateAndView]; return; } NSString *_Nullable previewUrl = [OWSLinkPreview previewUrlForMessageBodyText:body]; if (previewUrl.length < 1) { - self.inputLinkPreview = nil; - [self clearLinkPreviewView]; + [self clearLinkPreviewStateAndView]; return; } @@ -783,12 +783,24 @@ const CGFloat kMaxTextViewHeight = 98; LinkPreviewView *linkPreviewView = [[LinkPreviewView alloc] initWithDraftDelegate:self]; linkPreviewView.state = state; + linkPreviewView.hasAsymmetricalRounding = !self.quotedReply; + self.linkPreviewView = linkPreviewView; self.linkPreviewWrapper.hidden = NO; [self.linkPreviewWrapper addSubview:linkPreviewView]; [linkPreviewView ows_autoPinToSuperviewMargins]; } +- (void)clearLinkPreviewStateAndView +{ + OWSAssertIsOnMainThread(); + + self.inputLinkPreview = nil; + self.linkPreviewView = nil; + + [self clearLinkPreviewView]; +} + - (void)clearLinkPreviewView { OWSAssertIsOnMainThread(); @@ -828,8 +840,8 @@ const CGFloat kMaxTextViewHeight = 98; self.wasLinkPreviewCancelled = YES; - self.self.inputLinkPreview = nil; - [self clearLinkPreviewView]; + self.inputLinkPreview = nil; + [self clearLinkPreviewStateAndView]; } @end diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m index 539788ee5..fa13720a0 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m @@ -669,10 +669,21 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) if (self.hasBodyText && attachment == nil && message.linkPreview) { self.linkPreview = message.linkPreview; if (message.linkPreview.imageAttachmentId.length > 0) { - self.linkPreviewAttachment = + TSAttachment *_Nullable linkPreviewAttachment = [TSAttachment fetchObjectWithUniqueID:message.linkPreview.imageAttachmentId transaction:transaction]; - if (!self.linkPreviewAttachment) { + if (!linkPreviewAttachment) { OWSFailDebug(@"Could not load link preview image attachment."); + } else if (!linkPreviewAttachment.isImage) { + OWSFailDebug(@"Link preview attachment isn't an image."); + } else if ([linkPreviewAttachment isKindOfClass:[TSAttachmentStream class]]) { + TSAttachmentStream *attachmentStream = (TSAttachmentStream *)linkPreviewAttachment; + if (!attachmentStream.isValidImage) { + OWSFailDebug(@"Link preview image attachment isn't valid."); + } else { + self.linkPreviewAttachment = linkPreviewAttachment; + } + } else { + self.linkPreviewAttachment = linkPreviewAttachment; } } } diff --git a/Signal/src/views/LinkPreviewView.swift b/Signal/src/views/LinkPreviewView.swift index f5395683c..fe7a80034 100644 --- a/Signal/src/views/LinkPreviewView.swift +++ b/Signal/src/views/LinkPreviewView.swift @@ -188,7 +188,8 @@ public class LinkPreviewSent: NSObject, LinkPreviewState { guard let attachmentStream = imageAttachment as? TSAttachmentStream else { return .loading } - guard attachmentStream.isValidImage else { + guard attachmentStream.isImage, + attachmentStream.isValidImage else { return .invalid } return .loaded @@ -201,7 +202,8 @@ public class LinkPreviewSent: NSObject, LinkPreviewState { owsFailDebug("Could not load image.") return nil } - guard attachmentStream.isValidImage else { + guard attachmentStream.isImage, + attachmentStream.isValidImage else { return nil } guard let imageFilepath = attachmentStream.originalFilePath else { @@ -229,14 +231,20 @@ public protocol LinkPreviewViewDraftDelegate { public class LinkPreviewImageView: UIImageView { private let maskLayer = CAShapeLayer() + private let hasAsymmetricalRounding: Bool + @objc - public init() { + public init(hasAsymmetricalRounding: Bool) { + self.hasAsymmetricalRounding = hasAsymmetricalRounding + super.init(frame: .zero) self.layer.mask = maskLayer } public required init?(coder aDecoder: NSCoder) { + self.hasAsymmetricalRounding = false + super.init(coder: aDecoder) } @@ -271,8 +279,15 @@ public class LinkPreviewImageView: UIImageView { let bigRounding: CGFloat = 14 let smallRounding: CGFloat = 4 - let upperLeftRounding = CurrentAppContext().isRTL ? smallRounding : bigRounding - let upperRightRounding = CurrentAppContext().isRTL ? bigRounding : smallRounding + let upperLeftRounding: CGFloat + let upperRightRounding: CGFloat + if hasAsymmetricalRounding { + upperLeftRounding = CurrentAppContext().isRTL ? smallRounding : bigRounding + upperRightRounding = CurrentAppContext().isRTL ? bigRounding : smallRounding + } else { + upperLeftRounding = smallRounding + upperRightRounding = smallRounding + } let lowerRightRounding = smallRounding let lowerLeftRounding = smallRounding @@ -323,6 +338,17 @@ public class LinkPreviewView: UIStackView { } } + @objc + public var hasAsymmetricalRounding: Bool = false { + didSet { + AssertIsOnMainThread() + + if hasAsymmetricalRounding != oldValue { + updateContents() + } + } + } + @available(*, unavailable, message:"use other constructor instead.") required init(coder aDecoder: NSCoder) { notImplemented() @@ -670,7 +696,7 @@ public class LinkPreviewView: UIStackView { owsFailDebug("Could not load image.") return nil } - let imageView = LinkPreviewImageView() + let imageView = LinkPreviewImageView(hasAsymmetricalRounding: self.hasAsymmetricalRounding) imageView.image = image return imageView } diff --git a/Signal/src/views/QuotedReplyPreview.swift b/Signal/src/views/QuotedReplyPreview.swift index c806b7b2a..7354f6d11 100644 --- a/Signal/src/views/QuotedReplyPreview.swift +++ b/Signal/src/views/QuotedReplyPreview.swift @@ -10,7 +10,7 @@ protocol QuotedReplyPreviewDelegate: class { } @objc -class QuotedReplyPreview: UIView { +class QuotedReplyPreview: UIStackView { @objc public weak var delegate: QuotedReplyPreviewDelegate? @@ -19,8 +19,13 @@ class QuotedReplyPreview: UIView { private var quotedMessageView: OWSQuotedMessageView? private var heightConstraint: NSLayoutConstraint! - @objc - required init?(coder aDecoder: NSCoder) { + @available(*, unavailable, message:"use other constructor instead.") + required init(coder aDecoder: NSCoder) { + notImplemented() + } + + @available(*, unavailable, message:"use other constructor instead.") + override init(frame: CGRect) { notImplemented() } @@ -38,6 +43,8 @@ class QuotedReplyPreview: UIView { NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange), name: .UIContentSizeCategoryDidChange, object: nil) } + private let draftMarginTop: CGFloat = 6 + func updateContents() { subviews.forEach { $0.removeFromSuperview() } @@ -53,21 +60,35 @@ class QuotedReplyPreview: UIView { let cancelButton: UIButton = UIButton(type: .custom) - let buttonImage: UIImage = #imageLiteral(resourceName: "quoted-message-cancel").withRenderingMode(.alwaysTemplate) - cancelButton.setImage(buttonImage, for: .normal) + let cancelImage = UIImage(named: "compose-cancel")?.withRenderingMode(.alwaysTemplate) + cancelButton.setImage(cancelImage, for: .normal) cancelButton.imageView?.tintColor = Theme.secondaryColor cancelButton.addTarget(self, action: #selector(didTapCancel), for: .touchUpInside) + if let cancelSize = cancelImage?.size { + cancelButton.autoSetDimensions(to: cancelSize) + } - self.layoutMargins = .zero - - self.addSubview(quotedMessageView) - self.addSubview(cancelButton) - - quotedMessageView.autoPinEdges(toSuperviewMarginsExcludingEdge: .trailing) - cancelButton.autoPinEdges(toSuperviewMarginsExcludingEdge: .leading) - cancelButton.autoPinEdge(.leading, to: .trailing, of: quotedMessageView) - - cancelButton.autoSetDimensions(to: CGSize(width: 40, height: 40)) + 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: draftMarginTop, + left: CurrentAppContext().isRTL ? hMarginTrailing : hMarginLeading, + bottom: 0, + right: CurrentAppContext().isRTL ? hMarginLeading : hMarginTrailing) + + self.addArrangedSubview(quotedMessageView) + + let cancelStack = UIStackView() + cancelStack.axis = .horizontal + cancelStack.alignment = .top + cancelStack.setContentHuggingHigh() + cancelStack.setCompressionResistanceHigh() + cancelStack.addArrangedSubview(cancelButton) + self.addArrangedSubview(cancelStack) updateHeight() } diff --git a/SignalMessaging/categories/UIView+OWS.m b/SignalMessaging/categories/UIView+OWS.m index b0c0f6eb8..babbd3f9f 100644 --- a/SignalMessaging/categories/UIView+OWS.m +++ b/SignalMessaging/categories/UIView+OWS.m @@ -567,7 +567,8 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value) UIView *borderView = [UIView new]; borderView.userInteractionEnabled = NO; - borderView.backgroundColor = nil; + borderView.backgroundColor = UIColor.clearColor; + borderView.opaque = NO; borderView.layer.borderColor = color.CGColor; borderView.layer.borderWidth = strokeWidth; borderView.layer.cornerRadius = cornerRadius; diff --git a/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift b/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift index 80c1515fa..263206220 100644 --- a/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift +++ b/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift @@ -453,8 +453,6 @@ public class OWSLinkPreview: MTLModel { } class func allPreviewUrls(forMessageBodyText body: String?) -> [String] { - AssertIsOnMainThread() - guard OWSLinkPreview.featureEnabled else { return [] } @@ -650,16 +648,21 @@ public class OWSLinkPreview: MTLModel { } let data = try Data(contentsOf: URL(fileURLWithPath: asset.filePath)) + guard let srcImage = UIImage(data: data) else { + Logger.error("Could not parse image.") + return Promise(error: LinkPreviewError.invalidContent) + } + let maxImageSize: CGFloat = 1024 let shouldResize = imageSize.width > maxImageSize || imageSize.height > maxImageSize guard shouldResize else { - return Promise.value(data) + guard let dstData = UIImageJPEGRepresentation(srcImage, 0.8) else { + Logger.error("Could not write resized image.") + return Promise(error: LinkPreviewError.invalidContent) + } + return Promise.value(dstData) } - guard let srcImage = UIImage(data: data) else { - Logger.error("Could not parse image.") - return Promise(error: LinkPreviewError.invalidContent) - } guard let dstImage = srcImage.resized(withMaxDimensionPoints: maxImageSize) else { Logger.error("Could not resize image.") return Promise(error: LinkPreviewError.invalidContent) @@ -710,23 +713,14 @@ public class OWSLinkPreview: MTLModel { return downloadImage(url: imageUrl, imageMimeType: imageMimeType) .then(on: DispatchQueue.global()) { (imageData: Data) -> Promise in - let imageFilePath = OWSFileSystem.temporaryFilePath(withFileExtension: imageFileExtension) + // We always recompress images to Jpeg. + let imageFilePath = OWSFileSystem.temporaryFilePath(withFileExtension: "jpg") do { try imageData.write(to: NSURL.fileURL(withPath: imageFilePath), options: .atomicWrite) } catch let error as NSError { owsFailDebug("file write failed: \(imageFilePath), \(error)") return Promise(error: LinkPreviewError.assertionFailure) } - // NOTE: imageSize(forFilePath:...) will call ows_isValidImage(...). - let imageSize = NSData.imageSize(forFilePath: imageFilePath, mimeType: imageMimeType) - let kMaxImageSize: CGFloat = 2048 - guard imageSize.width > 0, - imageSize.height > 0, - imageSize.width < kMaxImageSize, - imageSize.height < kMaxImageSize else { - Logger.error("Image has invalid size: \(imageSize).") - return Promise(error: LinkPreviewError.assertionFailure) - } let linkPreviewDraft = OWSLinkPreviewDraft(urlString: linkUrlString, title: title, imageFilePath: imageFilePath) return Promise.value(linkPreviewDraft) diff --git a/SignalServiceKit/src/Network/ProxiedContentDownloader.swift b/SignalServiceKit/src/Network/ProxiedContentDownloader.swift index 1dbc3a79c..b90276150 100644 --- a/SignalServiceKit/src/Network/ProxiedContentDownloader.swift +++ b/SignalServiceKit/src/Network/ProxiedContentDownloader.swift @@ -643,6 +643,8 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio var request = URLRequest(url: assetRequest.assetDescription.url as URL) request.httpMethod = "HEAD" request.httpShouldUsePipelining = true + // Some services like Reddit will severely rate-limit requests without a user agent. + request.addValue("Signal", forHTTPHeaderField: "User-Agent") let task = downloadSession.dataTask(with: request, completionHandler: { data, response, error -> Void in if let data = data, data.count > 0 {