Finish link preview UI

This commit is contained in:
nielsandriesse 2021-02-15 13:51:26 +11:00
parent b57b874110
commit 810aa42f03
6 changed files with 114 additions and 35 deletions

View File

@ -8,6 +8,7 @@
// Link previews
// Slight paging glitch
// Scrolling bug
// Scroll button bug
final class ConversationVC : BaseVC, ConversationViewModelDelegate, UITableViewDataSource, UITableViewDelegate {
let thread: TSThread

View File

@ -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()

View File

@ -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)
}

View File

@ -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)

View File

@ -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)

View File

@ -7,6 +7,7 @@ public enum SNUserDefaults {
case hasSeenGIFMetadataWarning
case hasSyncedConfiguration
case hasViewedSeed
case hasSeenLinkPreviewSuggestion
case isUsingFullAPNs
case isMigratingToV2KeyPair
case isUsingMultiDevice