diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.h index 6da61337a..d1b3fd1ed 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.h +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // NS_ASSUME_NONNULL_BEGIN @@ -10,6 +10,7 @@ NS_ASSUME_NONNULL_BEGIN @protocol ConversationViewItem; @class OWSContact; +@class OWSLinkPreview; @class OWSQuotedReplyModel; @class TSAttachmentPointer; @class TSAttachmentStream; @@ -21,6 +22,7 @@ typedef NS_ENUM(NSUInteger, OWSMessageGestureLocation) { OWSMessageGestureLocation_OversizeText, OWSMessageGestureLocation_Media, OWSMessageGestureLocation_QuotedReply, + OWSMessageGestureLocation_LinkPreview, }; extern const UIDataDetectorTypes kOWSAllowedDataDetectorTypes; @@ -46,6 +48,8 @@ extern const UIDataDetectorTypes kOWSAllowedDataDetectorTypes; quotedReply:(OWSQuotedReplyModel *)quotedReply failedThumbnailDownloadAttachmentPointer:(TSAttachmentPointer *)attachmentPointer; +- (void)didTapConversationItem:(id)viewItem linkPreview:(OWSLinkPreview *)linkPreview; + - (void)didTapContactShareViewItem:(id)viewItem; - (void)didTapSendMessageToContactShare:(ContactShareViewModel *)contactShare diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m index 2cb3d6a67..e4a043e5b 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m @@ -40,6 +40,8 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes @property (nonatomic, nullable) UIView *bodyMediaView; +@property (nonatomic, nullable) LinkPreviewView *linkPreviewView; + // Should lazy-load expensive view contents (images, etc.). // Should do nothing if view is already loaded. @property (nonatomic, nullable) dispatch_block_t loadCellContentBlock; @@ -1158,6 +1160,13 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes } } + if (self.viewItem.linkPreview) { + CGSize linkPreviewSize = [LinkPreviewView measureWithConversationViewItem:self.viewItem]; + linkPreviewSize.width = MIN(linkPreviewSize.width, self.conversationStyle.maxMessageWidth); + cellSize.width = MAX(cellSize.width, linkPreviewSize.width); + cellSize.height += linkPreviewSize.height; + } + NSValue *_Nullable bodyTextSize = [self bodyTextSize]; if (bodyTextSize) { [textViewSizes addObject:bodyTextSize]; @@ -1284,6 +1293,9 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes [self.contactShareButtonsView removeFromSuperview]; self.contactShareButtonsView = nil; + + [self.linkPreviewView removeFromSuperview]; + self.linkPreviewView = nil; } #pragma mark - Gestures @@ -1338,6 +1350,13 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes OWSFailDebug(@"Missing quoted message."); } break; + case OWSMessageGestureLocation_LinkPreview: + if (self.viewItem.linkPreview) { + [self.delegate didTapConversationItem:self.viewItem linkPreview:self.viewItem.linkPreview]; + } else { + OWSFailDebug(@"Missing link preview."); + } + break; } } @@ -1426,6 +1445,17 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes } } + if (self.linkPreviewView) { + // Treat this as a "quoted reply" gesture if: + // + // * There is a "quoted reply" view. + // * The gesture occured within or above the "quoted reply" view. + CGPoint location = [self convertPoint:locationInMessageBubble toView:self.linkPreviewView]; + if (CGRectContainsPoint(self.linkPreviewView.bounds, location)) { + return OWSMessageGestureLocation_LinkPreview; + } + } + if (self.bodyMediaView) { // Treat this as a "body media" gesture if: // diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m index a8429c7aa..1da289a14 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // #import "OWSMessageCell.h" @@ -438,7 +438,8 @@ NS_ASSUME_NONNULL_BEGIN CGPoint locationInMessageBubble = [sender locationInView:self.messageBubbleView]; switch ([self.messageBubbleView gestureLocationForLocation:locationInMessageBubble]) { case OWSMessageGestureLocation_Default: - case OWSMessageGestureLocation_OversizeText: { + case OWSMessageGestureLocation_OversizeText: + case OWSMessageGestureLocation_LinkPreview: { [self.delegate conversationCell:self shouldAllowReply:shouldAllowReply didLongpressTextViewItem:self.viewItem]; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m index 47b198e56..576561141 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m @@ -765,7 +765,8 @@ const CGFloat kMaxTextViewHeight = 98; [self clearLinkPreviewView]; - LinkPreviewView *linkPreviewView = [[LinkPreviewView alloc] initWithState:state delegate:self]; + 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]; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 8f711837a..1338b9851 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -2338,6 +2338,13 @@ typedef enum : NSUInteger { // TODO: Highlight the quoted message? } +- (void)didTapConversationItem:(id)viewItem linkPreview:(OWSLinkPreview *)linkPreview +{ + OWSAssertIsOnMainThread(); + + // TODO: +} + - (void)showDetailViewForViewItem:(id)conversationItem { OWSAssertIsOnMainThread(); diff --git a/Signal/src/ViewControllers/MessageDetailViewController.swift b/Signal/src/ViewControllers/MessageDetailViewController.swift index 107614773..1be08d7e2 100644 --- a/Signal/src/ViewControllers/MessageDetailViewController.swift +++ b/Signal/src/ViewControllers/MessageDetailViewController.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // import Foundation @@ -710,6 +710,12 @@ class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDele // no - op } + func didTapConversationItem(_ viewItem: ConversationViewItem, linkPreview: OWSLinkPreview) { + // no - op + + // TODO: + } + @objc func didLongPressSent(sender: UIGestureRecognizer) { guard sender.state == .began else { return diff --git a/Signal/src/views/LinkPreviewView.swift b/Signal/src/views/LinkPreviewView.swift index 6afa8a4ff..6cfb538f7 100644 --- a/Signal/src/views/LinkPreviewView.swift +++ b/Signal/src/views/LinkPreviewView.swift @@ -197,7 +197,16 @@ public protocol LinkPreviewViewDelegate { @objc public class LinkPreviewView: UIStackView { private weak var delegate: LinkPreviewViewDelegate? - private let state: LinkPreviewState + + @objc + public var state: LinkPreviewState? { + didSet { + AssertIsOnMainThread() + assert(oldValue == nil) + + updateContents() + } + } @available(*, unavailable, message:"use other constructor instead.") required init(coder aDecoder: NSCoder) { @@ -209,30 +218,44 @@ public class LinkPreviewView: UIStackView { notImplemented() } - private let imageView = UIImageView() - private let titleLabel = UILabel() - private let domainLabel = UILabel() + private var cancelButton: UIButton? + private var layoutConstraints = [NSLayoutConstraint]() @objc - public init(state: LinkPreviewState, - delegate: LinkPreviewViewDelegate?) { - self.state = state + public init(delegate: LinkPreviewViewDelegate?) { self.delegate = delegate super.init(frame: .zero) - createContents() + self.isUserInteractionEnabled = true + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(wasTapped))) } private var isApproval: Bool { return delegate != nil } - private func createContents() { + private func resetContents() { + for subview in subviews { + subview.removeFromSuperview() + } + self.axis = .horizontal + self.alignment = .center + self.distribution = .fill + self.spacing = 0 - self.isUserInteractionEnabled = true - self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(wasTapped))) + cancelButton = nil + NSLayoutConstraint.deactivate(layoutConstraints) + layoutConstraints = [] + } + + private func updateContents() { + resetContents() + + guard let state = state else { + return + } guard state.isLoaded() else { createLoadingContents() return @@ -241,7 +264,7 @@ public class LinkPreviewView: UIStackView { createMessageContents() return } - createApprovalContents() + createApprovalContents(state: state) } private func createMessageContents() { @@ -250,9 +273,7 @@ public class LinkPreviewView: UIStackView { private let approvalHeight: CGFloat = 76 - private var cancelButton: UIButton? - - private func createApprovalContents() { + private func createApprovalContents(state: LinkPreviewState) { self.axis = .horizontal self.alignment = .fill self.distribution = .equalSpacing @@ -260,7 +281,7 @@ public class LinkPreviewView: UIStackView { // Image - if let imageView = createImageView() { + if let imageView = createImageView(state: state) { imageView.contentMode = .scaleAspectFill imageView.autoPinToSquareAspectRatio() let imageSize = approvalHeight @@ -346,7 +367,7 @@ public class LinkPreviewView: UIStackView { strokeView.autoSetDimension(.height, toSize: CGHairlineWidth()) } - private func createImageView() -> UIImageView? { + private func createImageView(state: LinkPreviewState) -> UIImageView? { guard state.isLoaded() else { owsFailDebug("State not loaded.") return nil @@ -367,7 +388,7 @@ public class LinkPreviewView: UIStackView { private func createLoadingContents() { self.axis = .vertical self.alignment = .center - self.autoSetDimension(.height, toSize: approvalHeight) + self.layoutConstraints.append(self.autoSetDimension(.height, toSize: approvalHeight)) let activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray) activityIndicator.startAnimating() @@ -379,6 +400,9 @@ public class LinkPreviewView: UIStackView { // MARK: Events @objc func wasTapped(sender: UIGestureRecognizer) { + guard let state = state else { + return + } guard sender.state == .recognized else { return } @@ -392,7 +416,15 @@ public class LinkPreviewView: UIStackView { return } } - self.delegate?.linkPreviewDidTap?(urlString: self.state.urlString()) + self.delegate?.linkPreviewDidTap?(urlString: state.urlString()) + } + + // MARK: Measurement + + @objc + public class func measure(withConversationViewItem item: ConversationViewItem) -> CGSize { + // TODO: + return CGSize.zero } @objc func didTapCancel(sender: UIButton) {