From 810aa42f03ea85614b29683c3c8c06db2ddd77fe Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Mon, 15 Feb 2021 13:51:26 +1100 Subject: [PATCH] Finish link preview UI --- Session/Conversations V2/ConversationVC.swift | 1 + .../Input View/InputView.swift | 69 +++++++++++++++---- .../Content Views/LinkPreviewViewV2.swift | 67 ++++++++++++++---- .../Message Cells/VisibleMessageCell.swift | 2 +- .../Link Previews/OWSLinkPreview.swift | 9 +-- .../General/SNUserDefaults.swift | 1 + 6 files changed, 114 insertions(+), 35 deletions(-) diff --git a/Session/Conversations V2/ConversationVC.swift b/Session/Conversations V2/ConversationVC.swift index c5302b9b6..b36fd2793 100644 --- a/Session/Conversations V2/ConversationVC.swift +++ b/Session/Conversations V2/ConversationVC.swift @@ -8,6 +8,7 @@ // • Link previews // • Slight paging glitch // • Scrolling bug +// • Scroll button bug final class ConversationVC : BaseVC, ConversationViewModelDelegate, UITableViewDataSource, UITableViewDelegate { let thread: TSThread diff --git a/Session/Conversations V2/Input View/InputView.swift b/Session/Conversations V2/Input View/InputView.swift index c4a3723f8..8011b7b43 100644 --- a/Session/Conversations V2/Input View/InputView.swift +++ b/Session/Conversations V2/Input View/InputView.swift @@ -1,8 +1,13 @@ -final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate, QuoteViewDelegate { +final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate, QuoteViewDelegate, BodyTextViewDelegate, UITextViewDelegate { private let delegate: InputViewDelegate var quoteDraftInfo: (model: OWSQuotedReplyModel, isOutgoing: Bool)? { didSet { handleQuoteDraftChanged() } } - + + private lazy var linkPreviewView: LinkPreviewViewV2 = { + let maxWidth = self.additionalContentContainer.bounds.width - InputView.linkPreviewViewInset + return LinkPreviewViewV2(for: nil, maxWidth: maxWidth, delegate: self) + }() + var text: String { get { inputTextView.text } set { inputTextView.text = newValue } @@ -19,11 +24,14 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate, private lazy var inputTextView = InputTextView(delegate: self) - private lazy var quoteDraftContainer: UIView = { + private lazy var additionalContentContainer: UIView = { let result = UIView() result.heightAnchor.constraint(greaterThanOrEqualToConstant: 4).isActive = true return result }() + + // MARK: Settings + private static let linkPreviewViewInset: CGFloat = 6 // MARK: Lifecycle init(delegate: InputViewDelegate) { @@ -76,7 +84,7 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate, bottomStackView.spacing = Values.smallSpacing bottomStackView.alignment = .center // Main stack view - let mainStackView = UIStackView(arrangedSubviews: [ buttonStackView, quoteDraftContainer, bottomStackView ]) + let mainStackView = UIStackView(arrangedSubviews: [ buttonStackView, additionalContentContainer, bottomStackView ]) mainStackView.axis = .vertical mainStackView.isLayoutMarginsRelativeArrangement = true let adjustment = (InputViewButton.expandedSize - InputViewButton.size) / 2 @@ -90,20 +98,53 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate, // MARK: Updating func inputTextViewDidChangeSize(_ inputTextView: InputTextView) { invalidateIntrinsicContentSize() + doLinkPreviewThingies() } private func handleQuoteDraftChanged() { - quoteDraftContainer.subviews.forEach { $0.removeFromSuperview() } + additionalContentContainer.subviews.forEach { $0.removeFromSuperview() } guard let quoteDraftInfo = quoteDraftInfo else { return } let direction: QuoteView.Direction = quoteDraftInfo.isOutgoing ? .outgoing : .incoming let hInset: CGFloat = 6 - let maxWidth = quoteDraftContainer.bounds.width + let maxWidth = additionalContentContainer.bounds.width let quoteView = QuoteView(for: quoteDraftInfo.model, direction: direction, hInset: hInset, maxWidth: maxWidth, delegate: self) - quoteDraftContainer.addSubview(quoteView) - quoteView.pin(.left, to: .left, of: quoteDraftContainer, withInset: hInset) - quoteView.pin(.top, to: .top, of: quoteDraftContainer, withInset: 12) - quoteView.pin(.right, to: .right, of: quoteDraftContainer, withInset: -hInset) - quoteView.pin(.bottom, to: .bottom, of: quoteDraftContainer, withInset: -6) + additionalContentContainer.addSubview(quoteView) + quoteView.pin(.left, to: .left, of: additionalContentContainer, withInset: hInset) + quoteView.pin(.top, to: .top, of: additionalContentContainer, withInset: 12) + quoteView.pin(.right, to: .right, of: additionalContentContainer, withInset: -hInset) + quoteView.pin(.bottom, to: .bottom, of: additionalContentContainer, withInset: -6) + } + + private func doLinkPreviewThingies() { + additionalContentContainer.subviews.forEach { $0.removeFromSuperview() } + + let text = inputTextView.text! + let userDefaults = UserDefaults.standard + if !OWSLinkPreview.allPreviewUrls(forMessageBodyText: text).isEmpty && !SSKPreferences.areLinkPreviewsEnabled + && !userDefaults[.hasSeenLinkPreviewSuggestion] { + // TODO: Show suggestion + userDefaults[.hasSeenLinkPreviewSuggestion] = true + } + + guard let linkPreviewURL = OWSLinkPreview.previewUrl(forRawBodyText: text, selectedRange: inputTextView.selectedRange) else { + return + } + + linkPreviewView.linkPreviewState = LinkPreviewLoading() + additionalContentContainer.addSubview(linkPreviewView) + linkPreviewView.pin(.left, to: .left, of: additionalContentContainer, withInset: InputView.linkPreviewViewInset) + linkPreviewView.pin(.top, to: .top, of: additionalContentContainer, withInset: 10) + linkPreviewView.pin(.right, to: .right, of: additionalContentContainer) + linkPreviewView.pin(.bottom, to: .bottom, of: additionalContentContainer, withInset: -4) + + OWSLinkPreview.tryToBuildPreviewInfo(previewUrl: linkPreviewURL).done { [weak self] draft in + guard let self = self else { return } + + self.linkPreviewView.linkPreviewState = LinkPreviewDraft(linkPreviewDraft: draft) + + }.catch { _ in + + }.retainUntilComplete() } // MARK: Interaction @@ -122,11 +163,15 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate, override func resignFirstResponder() -> Bool { inputTextView.resignFirstResponder() } + + func handleLongPress() { + // Not relevant in this case + } } // MARK: Delegate protocol InputViewDelegate { - + func handleCameraButtonTapped() func handleLibraryButtonTapped() func handleGIFButtonTapped() diff --git a/Session/Conversations V2/Message Cells/Content Views/LinkPreviewViewV2.swift b/Session/Conversations V2/Message Cells/Content Views/LinkPreviewViewV2.swift index 1c3b20c4d..fdf2f48b5 100644 --- a/Session/Conversations V2/Message Cells/Content Views/LinkPreviewViewV2.swift +++ b/Session/Conversations V2/Message Cells/Content Views/LinkPreviewViewV2.swift @@ -1,17 +1,20 @@ +import NVActivityIndicatorView final class LinkPreviewViewV2 : UIView { private let viewItem: ConversationViewItem? private let maxWidth: CGFloat - private let isOutgoing: Bool private let delegate: UITextViewDelegate & BodyTextViewDelegate var linkPreviewState: LinkPreviewState? { didSet { update() } } + private lazy var imageViewContainerWidthConstraint = imageView.set(.width, to: 100) + private lazy var imageViewContainerHeightConstraint = imageView.set(.height, to: 100) - private var textColor: UIColor { + private lazy var sentLinkPreviewTextColor: UIColor = { + let isOutgoing = (viewItem!.interaction.interactionType() == .outgoingMessage) switch (isOutgoing, AppModeManager.shared.currentAppMode) { case (true, .dark), (false, .light): return .black default: return .white } - } + }() // MARK: UI Components private lazy var imageView: UIImageView = { @@ -20,9 +23,19 @@ final class LinkPreviewViewV2 : UIView { return result }() + private lazy var imageViewContainer: UIView = { + let result = UIView() + result.clipsToBounds = true + return result + }() + + private lazy var loader: NVActivityIndicatorView = { + let color: UIColor = isLightMode ? .black : .white + return NVActivityIndicatorView(frame: CGRect.zero, type: .circleStrokeSpin, color: color, padding: nil) + }() + private lazy var titleLabel: UILabel = { let result = UILabel() - result.textColor = textColor result.font = .boldSystemFont(ofSize: Values.smallFontSize) result.numberOfLines = 0 return result @@ -30,14 +43,15 @@ final class LinkPreviewViewV2 : UIView { private lazy var bodyTextViewContainer = UIView() + private lazy var hStackViewContainer = UIView() + // MARK: Settings - private static let imageSize: CGFloat = 100 + private static let loaderSize: CGFloat = 24 // MARK: Lifecycle - init(for viewItem: ConversationViewItem?, maxWidth: CGFloat, isOutgoing: Bool, delegate: UITextViewDelegate & BodyTextViewDelegate) { + init(for viewItem: ConversationViewItem?, maxWidth: CGFloat, delegate: UITextViewDelegate & BodyTextViewDelegate) { self.viewItem = viewItem self.maxWidth = maxWidth - self.isOutgoing = isOutgoing self.delegate = delegate super.init(frame: CGRect.zero) setUpViewHierarchy() @@ -53,10 +67,8 @@ final class LinkPreviewViewV2 : UIView { private func setUpViewHierarchy() { // Image view - let imageViewContainer = UIView() - imageViewContainer.set(.width, to: LinkPreviewViewV2.imageSize) - imageViewContainer.set(.height, to: LinkPreviewViewV2.imageSize) - imageViewContainer.clipsToBounds = true + imageViewContainerWidthConstraint.isActive = true + imageViewContainerHeightConstraint.isActive = true imageViewContainer.addSubview(imageView) imageView.pin(to: imageViewContainer) // Title label @@ -64,8 +76,6 @@ final class LinkPreviewViewV2 : UIView { titleLabelContainer.addSubview(titleLabel) titleLabel.pin(to: titleLabelContainer, withInset: Values.smallSpacing) // Horizontal stack view - let hStackViewContainer = UIView() - hStackViewContainer.backgroundColor = isDarkMode ? .black : UIColor.black.withAlphaComponent(0.06) let hStackView = UIStackView(arrangedSubviews: [ imageViewContainer, titleLabelContainer ]) hStackView.axis = .horizontal hStackView.alignment = .center @@ -76,19 +86,48 @@ final class LinkPreviewViewV2 : UIView { vStackView.axis = .vertical addSubview(vStackView) vStackView.pin(to: self) + // Loader + addSubview(loader) + let loaderSize = LinkPreviewViewV2.loaderSize + loader.set(.width, to: loaderSize) + loader.set(.height, to: loaderSize) + loader.center(in: self) } // MARK: Updating private func update() { guard let linkPreviewState = linkPreviewState else { return } // Image view + let imageViewContainerSize: CGFloat = (linkPreviewState is LinkPreviewSent) ? 100 : 80 + imageViewContainerWidthConstraint.constant = imageViewContainerSize + imageViewContainerHeightConstraint.constant = imageViewContainerSize + imageViewContainer.layer.cornerRadius = (linkPreviewState is LinkPreviewSent) ? 0 : 8 + if linkPreviewState is LinkPreviewLoading { + imageViewContainer.backgroundColor = .clear + } else { + imageViewContainer.backgroundColor = isDarkMode ? .black : UIColor.black.withAlphaComponent(0.06) + } imageView.image = linkPreviewState.image() + // Loader + loader.alpha = (linkPreviewState.image() != nil) ? 0 : 1 + if linkPreviewState.image() != nil { loader.stopAnimating() } else { loader.startAnimating() } // Title + switch linkPreviewState { + case is LinkPreviewSent: titleLabel.textColor = sentLinkPreviewTextColor + default: + let textColor: UIColor = isDarkMode ? .white : .black + titleLabel.textColor = textColor + } titleLabel.text = linkPreviewState.title() + // Horizontal stack view + switch linkPreviewState { + case is LinkPreviewSent: hStackViewContainer.backgroundColor = isDarkMode ? .black : UIColor.black.withAlphaComponent(0.06) + default: hStackViewContainer.backgroundColor = nil + } // Body text view bodyTextViewContainer.subviews.forEach { $0.removeFromSuperview() } if let viewItem = viewItem { - let bodyTextView = VisibleMessageCell.getBodyTextView(for: viewItem, with: maxWidth, textColor: textColor, delegate: delegate) + let bodyTextView = VisibleMessageCell.getBodyTextView(for: viewItem, with: maxWidth, textColor: sentLinkPreviewTextColor, delegate: delegate) bodyTextViewContainer.addSubview(bodyTextView) bodyTextView.pin(to: bodyTextViewContainer, withInset: 12) } diff --git a/Session/Conversations V2/Message Cells/VisibleMessageCell.swift b/Session/Conversations V2/Message Cells/VisibleMessageCell.swift index e8c1e2b5b..d1efc1094 100644 --- a/Session/Conversations V2/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations V2/Message Cells/VisibleMessageCell.swift @@ -271,7 +271,7 @@ final class VisibleMessageCell : MessageCell, UITextViewDelegate, BodyTextViewDe let inset: CGFloat = 12 let maxWidth = VisibleMessageCell.getMaxWidth(for: viewItem) - 2 * inset if let linkPreview = viewItem.linkPreview { - let linkPreviewView = LinkPreviewViewV2(for: viewItem, maxWidth: maxWidth, isOutgoing: isOutgoing, delegate: self) + let linkPreviewView = LinkPreviewViewV2(for: viewItem, maxWidth: maxWidth, delegate: self) let conversationStyle = self.conversationStyle ?? ConversationStyle(thread: viewItem.interaction.thread) linkPreviewView.linkPreviewState = LinkPreviewSent(linkPreview: linkPreview, imageAttachment: viewItem.linkPreviewAttachment, conversationStyle:conversationStyle) snContentView.addSubview(linkPreviewView) diff --git a/SessionMessagingKit/Sending & Receiving/Link Previews/OWSLinkPreview.swift b/SessionMessagingKit/Sending & Receiving/Link Previews/OWSLinkPreview.swift index a64477da7..9872b8f1b 100644 --- a/SessionMessagingKit/Sending & Receiving/Link Previews/OWSLinkPreview.swift +++ b/SessionMessagingKit/Sending & Receiving/Link Previews/OWSLinkPreview.swift @@ -370,18 +370,11 @@ public class OWSLinkPreview: MTLModel { let matchRange: NSRange } - class func allPreviewUrls(forMessageBodyText body: String) -> [String] { + public class func allPreviewUrls(forMessageBodyText body: String) -> [String] { return allPreviewUrlMatches(forMessageBodyText: body).map { $0.urlString } } class func allPreviewUrlMatches(forMessageBodyText body: String) -> [URLMatchResult] { - guard OWSLinkPreview.featureEnabled else { - return [] - } - guard SSKPreferences.areLinkPreviewsEnabled else { - return [] - } - let detector: NSDataDetector do { detector = try NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) diff --git a/SessionUtilitiesKit/General/SNUserDefaults.swift b/SessionUtilitiesKit/General/SNUserDefaults.swift index e2a51e84e..ccc37cc05 100644 --- a/SessionUtilitiesKit/General/SNUserDefaults.swift +++ b/SessionUtilitiesKit/General/SNUserDefaults.swift @@ -7,6 +7,7 @@ public enum SNUserDefaults { case hasSeenGIFMetadataWarning case hasSyncedConfiguration case hasViewedSeed + case hasSeenLinkPreviewSuggestion case isUsingFullAPNs case isMigratingToV2KeyPair case isUsingMultiDevice