* Allow to force "compact" style for a link preview view. Link previews should be displayed in "compact" mode when there's a significant amount of text. * RoundMediaButton should never be allowed to be made smaller than its desired size. * Improved layout for text stories: both composer and viewer. There is now a shared UI layout logic between editor and viewer, editor being a subclass of viewer. New behavior for composer: • text view will grow in height as much as possible and becomes scollable when max height is reached. • link preview starts in "regular" layout (image above the text) and then is updated to "compact" layout (image on the left, text on the right) as text view grows. New behavior for viewer: • updated font sizes for match spec. • for extra tall stories text label is scaled down to fit. * Fix bug where Proceed button would stay disabled after composing a text story. * Fix two UX issues reported in PR review.
645 lines
25 KiB
Swift
645 lines
25 KiB
Swift
//
|
|
// Copyright (c) 2022 Open Whisper Systems. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import UIKit
|
|
import SignalServiceKit
|
|
|
|
open class TextAttachmentView: UIView {
|
|
|
|
private var textAttachment: TextAttachment?
|
|
|
|
public let contentLayoutGuide = UILayoutGuide()
|
|
|
|
convenience public init(attachment: TextAttachment) {
|
|
self.init(
|
|
text: attachment.text,
|
|
textStyle: attachment.textStyle,
|
|
textForegroundColor: attachment.textForegroundColor,
|
|
textBackgroundColor: attachment.textBackgroundColor,
|
|
background: attachment.background,
|
|
linkPreview: attachment.preview
|
|
)
|
|
self.textAttachment = attachment
|
|
}
|
|
|
|
public init(
|
|
text: String?,
|
|
textStyle: TextAttachment.TextStyle,
|
|
textForegroundColor: UIColor?,
|
|
textBackgroundColor: UIColor?,
|
|
background: TextAttachment.Background,
|
|
linkPreview: OWSLinkPreview?
|
|
) {
|
|
self.text = text
|
|
self.textStyle = textStyle
|
|
self.textForegroundColor = textForegroundColor ?? Theme.darkThemePrimaryColor
|
|
self.textBackgroundColor = textBackgroundColor
|
|
self.background = background
|
|
|
|
super.init(frame: .zero)
|
|
|
|
addLayoutGuide(contentLayoutGuide)
|
|
let constraints = [
|
|
contentLayoutGuide.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 24),
|
|
contentLayoutGuide.topAnchor.constraint(equalTo: topAnchor, constant: 20),
|
|
contentLayoutGuide.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -24),
|
|
contentLayoutGuide.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20)
|
|
]
|
|
constraints.forEach { $0.priority = .defaultHigh }
|
|
addConstraints(constraints)
|
|
|
|
if let linkPreview = linkPreview {
|
|
var attachment: TSAttachment?
|
|
if let imageAttachmentId = linkPreview.imageAttachmentId {
|
|
attachment = databaseStorage.read(block: { TSAttachment.anyFetch(uniqueId: imageAttachmentId, transaction: $0) })
|
|
}
|
|
self.linkPreview = LinkPreviewSent(
|
|
linkPreview: linkPreview,
|
|
imageAttachment: attachment,
|
|
conversationStyle: nil
|
|
)
|
|
}
|
|
|
|
updateTextAttributes()
|
|
reloadLinkPreviewAppearance()
|
|
updateBackground()
|
|
}
|
|
|
|
public func asThumbnailView() -> TextAttachmentThumbnailView { TextAttachmentThumbnailView(self) }
|
|
|
|
required public init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
public enum LayoutConstants {
|
|
public static let textBackgroundHMargin: CGFloat = 16
|
|
public static let textBackgroundVMargin: CGFloat = 16
|
|
public static let textBackgroundCornerRadius: CGFloat = 18
|
|
public static let linkPreviewAreaTopMargin: CGFloat = 8
|
|
public static let linkPreviewHMargin: CGFloat = 12
|
|
public static let linkPreviewVMargin: CGFloat = 20
|
|
}
|
|
|
|
open var isEditing: Bool { false }
|
|
|
|
public private(set) var textContentSize: CGSize = .zero
|
|
|
|
public override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
|
|
// Resize link preview view to its desired size.
|
|
if let linkPreviewView = linkPreviewView {
|
|
let linkPreviewMaxSize = contentLayoutGuide.layoutFrame.inset(by: linkPreviewWrapperView.layoutMargins).size
|
|
let linkPreviewSize = linkPreviewView.systemLayoutSizeFitting(
|
|
linkPreviewMaxSize,
|
|
withHorizontalFittingPriority: .required,
|
|
verticalFittingPriority: .fittingSizeLevel
|
|
)
|
|
|
|
linkPreviewWrapperView.bounds.size = CGSize(
|
|
width: linkPreviewSize.width + 2 * LayoutConstants.linkPreviewHMargin,
|
|
height: linkPreviewSize.height + 2 * LayoutConstants.linkPreviewVMargin
|
|
)
|
|
linkPreviewView.frame = linkPreviewWrapperView.bounds.insetBy(
|
|
dx: LayoutConstants.linkPreviewHMargin,
|
|
dy: LayoutConstants.linkPreviewVMargin
|
|
)
|
|
}
|
|
|
|
textContentSize = calculateTextContentSize()
|
|
|
|
// If link preview view has "regular" (tall) layout and there's no enough vertical space for both link and text,
|
|
// we force "compact" layout for the link preview and trigger a new layout pass.
|
|
if let linkPreviewView = linkPreviewView, linkPreviewView.layout == .regular, textContentSize.height > 0 {
|
|
let contentHeight = textContentSize.height + LayoutConstants.linkPreviewAreaTopMargin + linkPreviewWrapperView.frame.height
|
|
if contentHeight > contentLayoutGuide.layoutFrame.height {
|
|
forceCompactLayoutForLinkPreview = true
|
|
reloadLinkPreviewAppearance()
|
|
return
|
|
}
|
|
}
|
|
|
|
layoutTextContentAndLinkPreview()
|
|
}
|
|
|
|
open func layoutTextContentAndLinkPreview() {
|
|
var maxTextAreaHeight = contentLayoutGuide.layoutFrame.height
|
|
var linkPreviewAreaHeight: CGFloat = 0
|
|
if linkPreviewView != nil {
|
|
linkPreviewAreaHeight = linkPreviewWrapperView.frame.height
|
|
maxTextAreaHeight -= (linkPreviewAreaHeight + LayoutConstants.linkPreviewAreaTopMargin)
|
|
}
|
|
|
|
var textAreaHeight: CGFloat = 0
|
|
|
|
// Position text and/or link preview.
|
|
if let textLabel = textLabel, textContentSize.height > 0 {
|
|
textLabel.bounds.size = textContentSize
|
|
|
|
let cappedTextContentHeight = min(textContentSize.height, maxTextAreaHeight - 2 * LayoutConstants.textBackgroundVMargin)
|
|
let scaleFactor = min(1, cappedTextContentHeight / textContentSize.height)
|
|
textLabel.transform = CGAffineTransform.scale(scaleFactor)
|
|
|
|
let verticalOffset = linkPreviewAreaHeight > 0 ? 0.5 * (linkPreviewAreaHeight + LayoutConstants.linkPreviewAreaTopMargin) : 0
|
|
textLabel.center = CGPoint(
|
|
x: contentLayoutGuide.layoutFrame.center.x,
|
|
y: contentLayoutGuide.layoutFrame.center.y - verticalOffset
|
|
)
|
|
if let textBackgroundView = textBackgroundView {
|
|
textBackgroundView.frame = convert(textLabel.bounds, from: textLabel).insetBy(
|
|
dx: -LayoutConstants.textBackgroundHMargin,
|
|
dy: -LayoutConstants.textBackgroundVMargin
|
|
)
|
|
}
|
|
|
|
textAreaHeight = cappedTextContentHeight + 2 * LayoutConstants.textBackgroundVMargin
|
|
}
|
|
if linkPreviewView != nil {
|
|
let verticalOffset = textAreaHeight > 0 ? 0.5 * (textAreaHeight + LayoutConstants.linkPreviewAreaTopMargin) : 0
|
|
linkPreviewWrapperView.center = CGPoint(
|
|
x: contentLayoutGuide.layoutFrame.center.x,
|
|
y: contentLayoutGuide.layoutFrame.center.y + verticalOffset
|
|
)
|
|
}
|
|
}
|
|
|
|
open func calculateTextContentSize() -> CGSize {
|
|
guard let textLabel = textLabel else {
|
|
return .zero
|
|
}
|
|
|
|
let maxTextLabelSize = contentLayoutGuide.layoutFrame.insetBy(
|
|
dx: LayoutConstants.textBackgroundHMargin,
|
|
dy: LayoutConstants.textBackgroundVMargin
|
|
).size
|
|
return textLabel.systemLayoutSizeFitting(
|
|
maxTextLabelSize,
|
|
withHorizontalFittingPriority: .required,
|
|
verticalFittingPriority: .fittingSizeLevel
|
|
)
|
|
}
|
|
|
|
// MARK: - Attributes
|
|
|
|
public var text: String? {
|
|
didSet { updateTextAttributes() }
|
|
}
|
|
|
|
public var textStyle: TextAttachment.TextStyle = .regular {
|
|
didSet { updateTextAttributes() }
|
|
}
|
|
|
|
public private(set) var textForegroundColor: UIColor = Theme.darkThemePrimaryColor
|
|
|
|
public private(set) var textBackgroundColor: UIColor?
|
|
|
|
public func setTextForegroundColor(_ textForegroundColor: UIColor, backgroundColor: UIColor?) {
|
|
self.textForegroundColor = textForegroundColor
|
|
self.textBackgroundColor = backgroundColor
|
|
updateTextAttributes()
|
|
}
|
|
|
|
// MARK: - Text
|
|
|
|
public func sizeAndAlignment(forText text: String) -> (fontPointSize: CGFloat, textAlignment: NSTextAlignment) {
|
|
switch text.count {
|
|
case ..<50: return (34, .center)
|
|
case 50...199: return (24, .center)
|
|
default: return (18, .natural)
|
|
}
|
|
}
|
|
|
|
public func updateTextAttributes() {
|
|
defer { updateVisibilityOfComponents(animated: false) }
|
|
|
|
guard let text = text else { return }
|
|
|
|
var textLabel: UILabel
|
|
if let existingLabel = self.textLabel {
|
|
textLabel = existingLabel
|
|
} else {
|
|
textLabel = UILabel()
|
|
textLabel.adjustsFontSizeToFitWidth = true
|
|
textLabel.allowsDefaultTighteningForTruncation = true
|
|
textLabel.lineBreakMode = .byWordWrapping
|
|
textLabel.minimumScaleFactor = 0.2
|
|
textLabel.numberOfLines = 0
|
|
addSubview(textLabel)
|
|
self.textLabel = textLabel
|
|
}
|
|
|
|
let (fontPointSize, textAlignment) = sizeAndAlignment(forText: text)
|
|
textLabel.text = transformedText(text, for: textStyle)
|
|
textLabel.textAlignment = textAlignment
|
|
textLabel.font = font(for: textStyle, withPointSize: fontPointSize)
|
|
textLabel.textColor = textForegroundColor
|
|
|
|
if let textBackgroundColor = textBackgroundColor {
|
|
var textBackgroundView: UIView
|
|
if let existingBackgroundView = self.textBackgroundView {
|
|
textBackgroundView = existingBackgroundView
|
|
} else {
|
|
textBackgroundView = UIView()
|
|
textBackgroundView.layer.cornerRadius = LayoutConstants.textBackgroundCornerRadius
|
|
insertSubview(textBackgroundView, belowSubview: textLabel)
|
|
self.textBackgroundView = textBackgroundView
|
|
}
|
|
textBackgroundView.backgroundColor = textBackgroundColor
|
|
}
|
|
|
|
setNeedsLayout()
|
|
}
|
|
|
|
private func transformedText(_ text: String, for textStyle: TextAttachment.TextStyle) -> String {
|
|
guard case .condensed = textStyle else { return text }
|
|
return text.uppercased()
|
|
}
|
|
|
|
public func font(for textStyle: TextAttachment.TextStyle, withPointSize pointSize: CGFloat) -> UIFont {
|
|
let attributes: [UIFontDescriptor.AttributeName: Any]
|
|
|
|
switch textStyle {
|
|
case .regular:
|
|
attributes = [.name: "Inter-Regular_Medium"]
|
|
case .bold:
|
|
attributes = [.name: "Inter-Regular_Black"]
|
|
case .serif:
|
|
attributes = [.name: "EBGaramond-Regular"]
|
|
case .script:
|
|
attributes = [.name: "Parisienne-Regular"]
|
|
case .condensed:
|
|
// TODO: Ideally we could set an attribute to make this font
|
|
// all caps, but iOS deprecated that ability and didn't add
|
|
// a new equivalent function.
|
|
attributes = [.name: "BarlowCondensed-Medium"]
|
|
}
|
|
|
|
// TODO: Eventually we'll want to provide a cascadeList here to fallback
|
|
// to different fonts for different scripts rather than just relying on
|
|
// the built in OS fallbacks that don't tend to match the desired style.
|
|
let descriptor = UIFontDescriptor(fontAttributes: attributes)
|
|
|
|
return UIFont(descriptor: descriptor, size: pointSize)
|
|
}
|
|
|
|
open func updateVisibilityOfComponents(animated: Bool) {
|
|
let isEditing = isEditing
|
|
if text != nil {
|
|
textLabel?.setIsHidden(isEditing, animated: animated)
|
|
textBackgroundView?.setIsHidden(isEditing || textBackgroundColor == nil, animated: animated)
|
|
} else {
|
|
textLabel?.setIsHidden(true, animated: animated)
|
|
textBackgroundView?.setIsHidden(true, animated: animated)
|
|
}
|
|
}
|
|
|
|
public private(set) var textLabel: UILabel?
|
|
|
|
public private(set) var textBackgroundView: UIView?
|
|
|
|
// MARK: - Background
|
|
|
|
public var background: TextAttachment.Background {
|
|
didSet { updateBackground() }
|
|
}
|
|
|
|
private var gradientView: GradientView?
|
|
|
|
private func updateBackground() {
|
|
switch background {
|
|
case .color(let color):
|
|
if let gradientView = gradientView {
|
|
gradientView.isHidden = true
|
|
}
|
|
backgroundColor = color
|
|
|
|
case .gradient(let gradient):
|
|
var gradientView: GradientView
|
|
if let existingGradientView = self.gradientView {
|
|
gradientView = existingGradientView
|
|
} else {
|
|
gradientView = GradientView(colors: [])
|
|
insertSubview(gradientView, at: 0)
|
|
gradientView.autoPinEdgesToSuperviewEdges()
|
|
self.gradientView = gradientView
|
|
}
|
|
gradientView.isHidden = false
|
|
gradientView.colors = gradient.colors
|
|
gradientView.locations = gradient.locations
|
|
gradientView.setAngle(gradient.angle)
|
|
}
|
|
}
|
|
|
|
// MARK: - Link Preview
|
|
|
|
public var linkPreview: LinkPreviewState? {
|
|
didSet { reloadLinkPreviewAppearance() }
|
|
}
|
|
|
|
public private(set) var linkPreviewView: LinkPreviewView?
|
|
|
|
public private(set) lazy var linkPreviewWrapperView = UIView()
|
|
|
|
private var forceCompactLayoutForLinkPreview = false
|
|
|
|
private func shouldUseCompactLayoutForLinkPreview() -> Bool {
|
|
if let text = text, text.count >= 50 { return true }
|
|
return forceCompactLayoutForLinkPreview
|
|
}
|
|
|
|
open func reloadLinkPreviewAppearance() {
|
|
if let linkPreviewView = linkPreviewView {
|
|
linkPreviewView.removeFromSuperview()
|
|
self.linkPreviewView = nil
|
|
}
|
|
|
|
defer {
|
|
setNeedsLayout()
|
|
}
|
|
|
|
guard let linkPreview = linkPreview else {
|
|
linkPreviewWrapperView.isHidden = true
|
|
return
|
|
}
|
|
|
|
if linkPreviewWrapperView.superview == nil {
|
|
addSubview(linkPreviewWrapperView)
|
|
}
|
|
linkPreviewWrapperView.isHidden = false
|
|
|
|
let linkPreviewView = TextAttachmentView.LinkPreviewView(
|
|
linkPreview: linkPreview,
|
|
forceCompactSize: shouldUseCompactLayoutForLinkPreview()
|
|
)
|
|
linkPreviewWrapperView.addSubview(linkPreviewView)
|
|
self.linkPreviewView = linkPreviewView
|
|
}
|
|
|
|
public var isPresentingLinkTooltip: Bool { linkPreviewTooltipView != nil }
|
|
|
|
private var linkPreviewTooltipView: LinkPreviewTooltipView?
|
|
|
|
public func willHandleTapGesture(_ gesture: UITapGestureRecognizer) -> Bool {
|
|
if let linkPreviewTooltipView = linkPreviewTooltipView {
|
|
if let container = linkPreviewTooltipView.superview,
|
|
linkPreviewTooltipView.frame.contains(gesture.location(in: container)) {
|
|
CurrentAppContext().open(linkPreviewTooltipView.url)
|
|
} else {
|
|
linkPreviewTooltipView.removeFromSuperview()
|
|
self.linkPreviewTooltipView = nil
|
|
}
|
|
|
|
return true
|
|
} else if let linkPreviewView = linkPreviewView,
|
|
let urlString = textAttachment?.preview?.urlString,
|
|
let container = linkPreviewView.superview,
|
|
linkPreviewView.frame.contains(gesture.location(in: container)) {
|
|
let tooltipView = LinkPreviewTooltipView(
|
|
fromView: self,
|
|
tailReferenceView: linkPreviewView,
|
|
url: URL(string: urlString)!
|
|
)
|
|
self.linkPreviewTooltipView = tooltipView
|
|
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// MARK: - LinkPreviewView
|
|
|
|
public class LinkPreviewView: UIStackView {
|
|
|
|
public enum Layout {
|
|
case regular
|
|
case compact
|
|
case draft
|
|
case domainOnly
|
|
}
|
|
public private(set) var layout: Layout = .regular
|
|
|
|
public init(linkPreview: LinkPreviewState, isDraft: Bool = false, forceCompactSize: Bool = false) {
|
|
super.init(frame: .zero)
|
|
|
|
let backgroundColor: UIColor = isDraft ? Theme.darkThemeTableView2PresentedBackgroundColor : .ows_gray02
|
|
let backgroundView = addBackgroundView(withBackgroundColor: backgroundColor)
|
|
|
|
let title = linkPreview.title()
|
|
let description = linkPreview.previewDescription()
|
|
let hasTitleOrDescription = title != nil || description != nil
|
|
if isDraft {
|
|
layout = .draft
|
|
} else if hasTitleOrDescription {
|
|
layout = forceCompactSize ? .compact : .regular
|
|
} else {
|
|
layout = .domainOnly
|
|
}
|
|
|
|
let thumbnailImageView = UIImageView()
|
|
thumbnailImageView.clipsToBounds = true
|
|
if layout != .domainOnly && linkPreview.imageState() == .loaded {
|
|
thumbnailImageView.contentMode = .scaleAspectFill
|
|
|
|
// Downgrade "regular" to "compact" if thumbnail is too small.
|
|
let imageSize = linkPreview.imagePixelSize
|
|
if layout == .regular && (imageSize.width < 300 || imageSize.height < 300) {
|
|
layout = .compact
|
|
}
|
|
linkPreview.imageAsync(thumbnailQuality: layout == .regular ? .mediumLarge : .small) { image in
|
|
thumbnailImageView.image = image
|
|
}
|
|
} else {
|
|
// Dark placeholder icon on light background if there's no thumbnail associated with the link preview.
|
|
layout = .compact
|
|
thumbnailImageView.backgroundColor = .ows_gray02
|
|
thumbnailImageView.contentMode = .center
|
|
thumbnailImageView.image = UIImage(imageLiteralResourceName: "link-diagonal")
|
|
thumbnailImageView.tintColor = Theme.lightThemePrimaryColor
|
|
}
|
|
|
|
alignment = .fill
|
|
axis = layout == .regular ? .vertical : .horizontal
|
|
|
|
switch layout {
|
|
case .regular:
|
|
backgroundView.layer.cornerRadius = 18
|
|
thumbnailImageView.autoSetDimension(.height, toSize: 152)
|
|
thumbnailImageView.layer.maskedCorners = [ .layerMinXMinYCorner, .layerMaxXMinYCorner ]
|
|
|
|
case .compact:
|
|
backgroundView.layer.cornerRadius = 18
|
|
thumbnailImageView.autoSetDimension(.width, toSize: 88)
|
|
// Allow thumbnail to grow vertically with the text.
|
|
thumbnailImageView.autoSetDimension(.height, toSize: 88, relation: .greaterThanOrEqual)
|
|
thumbnailImageView.layer.maskedCorners = [ .layerMinXMinYCorner, .layerMinXMaxYCorner ]
|
|
|
|
case .draft:
|
|
backgroundView.layer.cornerRadius = 8
|
|
thumbnailImageView.autoSetDimension(.width, toSize: 76)
|
|
// Allow thumbnail to grow vertically with the text.
|
|
thumbnailImageView.autoSetDimension(.height, toSize: 76, relation: .greaterThanOrEqual)
|
|
thumbnailImageView.layer.maskedCorners = .all
|
|
|
|
case .domainOnly:
|
|
backgroundView.layer.cornerRadius = 12
|
|
thumbnailImageView.autoSetDimensions(to: CGSize(width: 50, height: 50))
|
|
}
|
|
thumbnailImageView.layer.cornerRadius = backgroundView.layer.cornerRadius
|
|
addArrangedSubview(thumbnailImageView)
|
|
|
|
let previewVStack = UIStackView()
|
|
previewVStack.axis = .vertical
|
|
previewVStack.spacing = 2
|
|
previewVStack.alignment = .leading
|
|
previewVStack.isLayoutMarginsRelativeArrangement = true
|
|
previewVStack.layoutMargins = UIEdgeInsets(hMargin: 12, vMargin: 8)
|
|
// Make placeholder icon look centered between leading edge of the panel and text.
|
|
if layout == .domainOnly {
|
|
previewVStack.layoutMargins.leading = 0
|
|
}
|
|
addArrangedSubview(previewVStack)
|
|
|
|
if let title = title {
|
|
let titleLabel = UILabel()
|
|
titleLabel.text = title
|
|
titleLabel.font = .ows_dynamicTypeSubheadlineClamped.ows_semibold
|
|
titleLabel.textColor = isDraft ? Theme.darkThemePrimaryColor : Theme.lightThemePrimaryColor
|
|
titleLabel.numberOfLines = 2
|
|
titleLabel.setCompressionResistanceVerticalHigh()
|
|
titleLabel.setContentHuggingVerticalHigh()
|
|
previewVStack.addArrangedSubview(titleLabel)
|
|
}
|
|
|
|
if let description = description {
|
|
let descriptionLabel = UILabel()
|
|
descriptionLabel.text = description
|
|
descriptionLabel.font = .ows_dynamicTypeFootnoteClamped
|
|
descriptionLabel.textColor = isDraft ? Theme.darkThemePrimaryColor : Theme.lightThemePrimaryColor
|
|
descriptionLabel.numberOfLines = 2
|
|
descriptionLabel.setCompressionResistanceVerticalHigh()
|
|
descriptionLabel.setContentHuggingVerticalHigh()
|
|
previewVStack.addArrangedSubview(descriptionLabel)
|
|
}
|
|
|
|
let footerLabel = UILabel()
|
|
footerLabel.numberOfLines = 1
|
|
if hasTitleOrDescription {
|
|
footerLabel.font = .ows_dynamicTypeCaption1Clamped
|
|
footerLabel.textColor = isDraft ? Theme.darkThemeSecondaryTextAndIconColor : .ows_gray60
|
|
} else {
|
|
footerLabel.font = .ows_dynamicTypeSubheadlineClamped.ows_semibold
|
|
footerLabel.textColor = isDraft ? Theme.darkThemePrimaryColor : Theme.lightThemePrimaryColor
|
|
}
|
|
footerLabel.setCompressionResistanceVerticalHigh()
|
|
footerLabel.setContentHuggingVerticalHigh()
|
|
previewVStack.addArrangedSubview(footerLabel)
|
|
|
|
var footerText: String
|
|
if let displayDomain = OWSLinkPreviewManager.displayDomain(forUrl: linkPreview.urlString()) {
|
|
footerText = displayDomain.lowercased()
|
|
} else {
|
|
footerText = NSLocalizedString(
|
|
"LINK_PREVIEW_UNKNOWN_DOMAIN",
|
|
comment: "Label for link previews with an unknown host."
|
|
).uppercased()
|
|
}
|
|
if let date = linkPreview.date() {
|
|
footerText.append(" ⋅ \(Self.dateFormatter.string(from: date))")
|
|
}
|
|
footerLabel.text = footerText
|
|
}
|
|
|
|
required init(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
private static let dateFormatter: DateFormatter = {
|
|
let formatter = DateFormatter()
|
|
formatter.dateStyle = .medium
|
|
formatter.timeStyle = .none
|
|
return formatter
|
|
}()
|
|
}
|
|
}
|
|
|
|
private class LinkPreviewTooltipView: TooltipView {
|
|
let url: URL
|
|
init(fromView: UIView, tailReferenceView: UIView, url: URL) {
|
|
self.url = url
|
|
super.init(
|
|
fromView: fromView,
|
|
widthReferenceView: fromView,
|
|
tailReferenceView: tailReferenceView,
|
|
wasTappedBlock: nil
|
|
)
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
public override func bubbleContentView() -> UIView {
|
|
let titleLabel = UILabel()
|
|
titleLabel.text = NSLocalizedString(
|
|
"STORY_LINK_PREVIEW_VISIT_LINK_TOOLTIP",
|
|
comment: "Tooltip prompting the user to visit a story link."
|
|
)
|
|
titleLabel.font = UIFont.ows_dynamicTypeBody2Clamped.ows_semibold
|
|
titleLabel.textColor = .ows_white
|
|
|
|
let urlLabel = UILabel()
|
|
urlLabel.text = url.absoluteString
|
|
urlLabel.font = .ows_dynamicTypeCaption1Clamped
|
|
urlLabel.textColor = .ows_white
|
|
|
|
let stackView = UIStackView(arrangedSubviews: [titleLabel, urlLabel])
|
|
stackView.axis = .vertical
|
|
stackView.alignment = .leading
|
|
stackView.spacing = 1
|
|
stackView.layoutMargins = UIEdgeInsets(hMargin: 16, vMargin: 14)
|
|
stackView.isLayoutMarginsRelativeArrangement = true
|
|
|
|
return stackView
|
|
}
|
|
|
|
public override var bubbleColor: UIColor { .ows_black }
|
|
public override var bubbleHSpacing: CGFloat { 16 }
|
|
|
|
public override var tailDirection: TooltipView.TailDirection { .down }
|
|
public override var dismissOnTap: Bool { false }
|
|
}
|
|
|
|
public class TextAttachmentThumbnailView: UIView {
|
|
// By default, we render the textView at a large 3:2 size (matching the aspect
|
|
// of the thumbnail container), so the fonts and gradients all render properly
|
|
// for the preview. We then scale it down to render a "thumbnail" view.
|
|
public static let defaultRenderSize = CGSize(width: 375, height: 563)
|
|
|
|
public lazy var renderSize = Self.defaultRenderSize {
|
|
didSet {
|
|
textAttachmentView.transform = .scale(width / renderSize.width)
|
|
}
|
|
}
|
|
|
|
private let textAttachmentView: TextAttachmentView
|
|
public init(_ textAttachmentView: TextAttachmentView) {
|
|
self.textAttachmentView = textAttachmentView
|
|
super.init(frame: .zero)
|
|
addSubview(textAttachmentView)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
public override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
|
|
textAttachmentView.transform = .scale(width / renderSize.width)
|
|
textAttachmentView.frame = bounds
|
|
}
|
|
}
|