Finish link preview UI
This commit is contained in:
parent
b57b874110
commit
810aa42f03
|
@ -8,6 +8,7 @@
|
|||
// • Link previews
|
||||
// • Slight paging glitch
|
||||
// • Scrolling bug
|
||||
// • Scroll button bug
|
||||
|
||||
final class ConversationVC : BaseVC, ConversationViewModelDelegate, UITableViewDataSource, UITableViewDelegate {
|
||||
let thread: TSThread
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -7,6 +7,7 @@ public enum SNUserDefaults {
|
|||
case hasSeenGIFMetadataWarning
|
||||
case hasSyncedConfiguration
|
||||
case hasViewedSeed
|
||||
case hasSeenLinkPreviewSuggestion
|
||||
case isUsingFullAPNs
|
||||
case isMigratingToV2KeyPair
|
||||
case isUsingMultiDevice
|
||||
|
|
Loading…
Reference in New Issue