fix snapshot not completed issue
This commit is contained in:
parent
e8518188ac
commit
1d9e4c88c2
|
@ -183,6 +183,7 @@
|
|||
7BAF54D927ACD0E3003D12F8 /* String+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF54D627ACD0E3003D12F8 /* String+Localization.swift */; };
|
||||
7BAF54DA27ACD0E3003D12F8 /* UITableView+ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF54D727ACD0E3003D12F8 /* UITableView+ReusableView.swift */; };
|
||||
7BAF54DC27ACD12B003D12F8 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF54DB27ACD12B003D12F8 /* UIColor+Extensions.swift */; };
|
||||
7BBBDC44286EAD2D00747E59 /* TappableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBBDC43286EAD2D00747E59 /* TappableLabel.swift */; };
|
||||
7BC01A3E241F40AB00BC7C55 /* NotificationServiceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC01A3D241F40AB00BC7C55 /* NotificationServiceExtension.swift */; };
|
||||
7BC01A42241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 7BC01A3B241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
7BC707F227290ACB002817AD /* SessionCallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC707F127290ACB002817AD /* SessionCallManager.swift */; };
|
||||
|
@ -1191,6 +1192,7 @@
|
|||
7BAF54D627ACD0E3003D12F8 /* String+Localization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Localization.swift"; sourceTree = "<group>"; };
|
||||
7BAF54D727ACD0E3003D12F8 /* UITableView+ReusableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITableView+ReusableView.swift"; sourceTree = "<group>"; };
|
||||
7BAF54DB27ACD12B003D12F8 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = "<group>"; };
|
||||
7BBBDC43286EAD2D00747E59 /* TappableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TappableLabel.swift; sourceTree = "<group>"; };
|
||||
7BC01A3B241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SessionNotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
7BC01A3D241F40AB00BC7C55 /* NotificationServiceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationServiceExtension.swift; sourceTree = "<group>"; };
|
||||
7BC01A3F241F40AB00BC7C55 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
|
@ -2916,6 +2918,7 @@
|
|||
B8CCF638239721E20091D419 /* TabBar.swift */,
|
||||
B8BB82B423947F2D00BA5194 /* TextField.swift */,
|
||||
C3C3CF8824D8EED300E1CCE7 /* TextView.swift */,
|
||||
7BBBDC43286EAD2D00747E59 /* TappableLabel.swift */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
|
@ -4533,6 +4536,7 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
C331FF972558FA6B00070591 /* Fonts.swift in Sources */,
|
||||
7BBBDC44286EAD2D00747E59 /* TappableLabel.swift in Sources */,
|
||||
C331FF9B2558FA6B00070591 /* Gradients.swift in Sources */,
|
||||
C331FFB82558FA8D00070591 /* DeviceUtilities.swift in Sources */,
|
||||
C331FFE72558FB0000070591 /* TextField.swift in Sources */,
|
||||
|
|
|
@ -513,7 +513,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
|
|||
// Show the context menu if applicable
|
||||
guard let index = viewItems.firstIndex(where: { $0 === viewItem }),
|
||||
let cell = messagesTableView.cellForRow(at: IndexPath(row: index, section: 0)) as? VisibleMessageCell,
|
||||
let snapshot = cell.snapshot(afterScreenUpdates: false), contextMenuWindow == nil,
|
||||
let snapshot = cell.bubbleView.snapshotView(afterScreenUpdates: false), contextMenuWindow == nil,
|
||||
!ContextMenuVC.actions(for: viewItem, delegate: self).isEmpty else { return }
|
||||
UIImpactFeedbackGenerator(style: .heavy).impactOccurred()
|
||||
let frame = cell.convert(cell.bubbleView.frame, to: UIApplication.shared.keyWindow!)
|
||||
|
|
|
@ -398,6 +398,10 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate,
|
|||
func handleMentionSelected(_ mention: Mention, from view: MentionSelectionView) {
|
||||
delegate?.handleMentionSelected(mention, from: view)
|
||||
}
|
||||
|
||||
func tapableLabel(_ label: TappableLabel, didTapUrl url: String, atRange range: NSRange) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
// MARK: Convenience
|
||||
private func container(for button: InputViewButton) -> UIView {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import NVActivityIndicatorView
|
||||
import SessionUIKit
|
||||
|
||||
final class LinkPreviewView : UIView {
|
||||
private let viewItem: ConversationViewItem?
|
||||
|
@ -59,7 +60,7 @@ final class LinkPreviewView : UIView {
|
|||
return result
|
||||
}()
|
||||
|
||||
var bodyTextView: UITextView?
|
||||
var bodyTextView: TappableLabel?
|
||||
|
||||
// MARK: Settings
|
||||
private static let loaderSize: CGFloat = 24
|
||||
|
@ -163,7 +164,7 @@ final class LinkPreviewView : UIView {
|
|||
}
|
||||
|
||||
// MARK: Delegate
|
||||
protocol LinkPreviewViewDelegate : UITextViewDelegate & BodyTextViewDelegate {
|
||||
protocol LinkPreviewViewDelegate : TappableLabelDelegate {
|
||||
var lastSearchedText: String? { get }
|
||||
|
||||
func handleLinkPreviewCanceled()
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import UIKit
|
||||
import SessionUIKit
|
||||
|
||||
final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
|
||||
private var isHandlingLongPress: Bool = false
|
||||
private var unloadContent: (() -> Void)?
|
||||
private var previousX: CGFloat = 0
|
||||
var albumView: MediaAlbumView?
|
||||
var bodyTextView: UITextView?
|
||||
var bodyTextView: TappableLabel?
|
||||
// Constraints
|
||||
private lazy var headerViewTopConstraint = headerView.pin(.top, to: .top, of: self, withInset: 1)
|
||||
private lazy var authorLabelHeightConstraint = authorLabel.set(.height, to: 0)
|
||||
|
@ -631,9 +632,10 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
|
||||
delegate?.openURL(URL)
|
||||
return false
|
||||
func tapableLabel(_ label: TappableLabel, didTapUrl url: String, atRange range: NSRange) {
|
||||
if let URL = URL(string: url) {
|
||||
delegate?.openURL(URL)
|
||||
}
|
||||
}
|
||||
|
||||
private func resetReply() {
|
||||
|
@ -776,50 +778,67 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
|
|||
return isGroupThread && viewItem.shouldShowSenderProfilePicture && senderSessionID != nil
|
||||
}
|
||||
|
||||
static func getBodyTextView(for viewItem: ConversationViewItem, with availableWidth: CGFloat, textColor: UIColor, delegate: UITextViewDelegate & BodyTextViewDelegate) -> UITextView {
|
||||
static func getBodyTextView(for viewItem: ConversationViewItem, with availableWidth: CGFloat, textColor: UIColor, delegate: TappableLabelDelegate) -> TappableLabel {
|
||||
// Take care of:
|
||||
// • Highlighting mentions
|
||||
// • Linkification
|
||||
// • Highlighting search results
|
||||
|
||||
func detectLinks(body: String?) -> [String: NSRange] {
|
||||
var links: [String: NSRange] = [:]
|
||||
guard let body = body else { return links }
|
||||
let detector: NSDataDetector
|
||||
do {
|
||||
detector = try NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
|
||||
} catch {
|
||||
return [:]
|
||||
}
|
||||
let matches = detector.matches(in: body, options: [], range: NSRange(location: 0, length: body.count))
|
||||
for match in matches {
|
||||
guard let matchURL = match.url else { continue }
|
||||
|
||||
// If the URL entered didn't have a scheme it will default to 'http', we want to catch this and
|
||||
// set the scheme to 'https' instead as we don't load previews for 'http' so this will result
|
||||
// in more previews actually getting loaded without forcing the user to enter 'https://' before
|
||||
// every URL they enter
|
||||
let urlString: String = (matchURL.absoluteString == "http://\(body)" ?
|
||||
"https://\(body)" :
|
||||
matchURL.absoluteString
|
||||
)
|
||||
if URL(string: urlString) != nil {
|
||||
links[urlString] = (body as NSString).range(of: urlString)
|
||||
}
|
||||
}
|
||||
return links
|
||||
}
|
||||
|
||||
guard let message = viewItem.interaction as? TSMessage else { preconditionFailure() }
|
||||
let isOutgoing = (message.interactionType() == .outgoingMessage)
|
||||
let result = BodyTextView(snDelegate: delegate)
|
||||
result.isEditable = false
|
||||
let attributes: [NSAttributedString.Key:Any] = [
|
||||
.foregroundColor : textColor,
|
||||
.font : UIFont.systemFont(ofSize: getFontSize(for: viewItem))
|
||||
]
|
||||
let attributedText = NSMutableAttributedString(attributedString: MentionUtilities.highlightMentions(in: message.body ?? "", isOutgoingMessage: isOutgoing, threadID: viewItem.interaction.uniqueThreadId, attributes: attributes))
|
||||
let links = detectLinks(body: message.body)
|
||||
for (urlString, range) in links {
|
||||
let linkCustomAttributes: [NSAttributedString.Key : Any] = [
|
||||
.font: UIFont.systemFont(ofSize: getFontSize(for: viewItem)),
|
||||
.foregroundColor: textColor,
|
||||
.underlineColor: textColor,
|
||||
.underlineStyle: NSUnderlineStyle.single.rawValue,
|
||||
.attachment: URL(string: urlString)!]
|
||||
attributedText.addAttributes(linkCustomAttributes, range: range)
|
||||
}
|
||||
|
||||
let result = TappableLabel()
|
||||
result.attributedText = attributedText
|
||||
result.dataDetectorTypes = .link
|
||||
result.backgroundColor = .clear
|
||||
result.isOpaque = false
|
||||
result.textContainerInset = UIEdgeInsets.zero
|
||||
result.contentInset = UIEdgeInsets.zero
|
||||
result.textContainer.lineFragmentPadding = 0
|
||||
result.isScrollEnabled = false
|
||||
result.isUserInteractionEnabled = true
|
||||
result.delegate = delegate
|
||||
result.linkTextAttributes = [ .foregroundColor : textColor, .underlineStyle : NSUnderlineStyle.single.rawValue ]
|
||||
let availableSpace = CGSize(width: availableWidth, height: .greatestFiniteMagnitude)
|
||||
let size = result.sizeThatFits(availableSpace)
|
||||
result.set(.height, to: size.height)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
extension VisibleMessageCell {
|
||||
public func snapshot(afterScreenUpdates afterUpdates: Bool) -> UIView? {
|
||||
let labelForRendering = UILabel()
|
||||
labelForRendering.numberOfLines = 0
|
||||
labelForRendering.backgroundColor = self.bubbleView.backgroundColor
|
||||
if let bodyTextView = self.bodyTextView {
|
||||
labelForRendering.attributedText = bodyTextView.attributedText
|
||||
self.snContentView.addSubview(labelForRendering)
|
||||
labelForRendering.frame = self.snContentView.convert(bodyTextView.frame, to: self.snContentView)
|
||||
}
|
||||
let snapshot = self.bubbleView.snapshotView(afterScreenUpdates: true)
|
||||
labelForRendering.removeFromSuperview()
|
||||
return snapshot
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
import UIKit
|
||||
|
||||
// Requirements:
|
||||
// • Links should show up properly and be tappable.
|
||||
// • Text should * not * be selectable.
|
||||
// • The long press interaction that shows the context menu should still work.
|
||||
|
||||
// See https://stackoverflow.com/questions/47983838/how-can-you-change-the-color-of-links-in-a-uilabel
|
||||
|
||||
public protocol TappableLabelDelegate: AnyObject {
|
||||
func tapableLabel(_ label: TappableLabel, didTapUrl url: String, atRange range: NSRange)
|
||||
}
|
||||
|
||||
public class TappableLabel: UILabel {
|
||||
|
||||
private var links: [String: NSRange] = [:]
|
||||
private(set) var layoutManager = NSLayoutManager()
|
||||
private(set) var textContainer = NSTextContainer(size: CGSize.zero)
|
||||
private(set) var textStorage = NSTextStorage() {
|
||||
didSet {
|
||||
textStorage.addLayoutManager(layoutManager)
|
||||
}
|
||||
}
|
||||
|
||||
public weak var delegate: TappableLabelDelegate?
|
||||
|
||||
public override var attributedText: NSAttributedString? {
|
||||
didSet {
|
||||
if let attributedText = attributedText {
|
||||
textStorage = NSTextStorage(attributedString: attributedText)
|
||||
findLinksAndRange(attributeString: attributedText)
|
||||
} else {
|
||||
textStorage = NSTextStorage()
|
||||
links = [:]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override var lineBreakMode: NSLineBreakMode {
|
||||
didSet {
|
||||
textContainer.lineBreakMode = lineBreakMode
|
||||
}
|
||||
}
|
||||
|
||||
public override var numberOfLines: Int {
|
||||
didSet {
|
||||
textContainer.maximumNumberOfLines = numberOfLines
|
||||
}
|
||||
}
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setup()
|
||||
}
|
||||
|
||||
public required init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
setup()
|
||||
}
|
||||
|
||||
private func setup() {
|
||||
isUserInteractionEnabled = true
|
||||
layoutManager.addTextContainer(textContainer)
|
||||
textContainer.lineFragmentPadding = 0
|
||||
textContainer.lineBreakMode = lineBreakMode
|
||||
textContainer.maximumNumberOfLines = numberOfLines
|
||||
numberOfLines = 0
|
||||
}
|
||||
|
||||
public override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
textContainer.size = bounds.size
|
||||
}
|
||||
|
||||
private func findLinksAndRange(attributeString: NSAttributedString) {
|
||||
links = [:]
|
||||
let enumerationBlock: (Any?, NSRange, UnsafeMutablePointer<ObjCBool>) -> Void = { [weak self] value, range, isStop in
|
||||
guard let strongSelf = self else { return }
|
||||
if let value = value {
|
||||
let stringValue = "\(value)"
|
||||
strongSelf.links[stringValue] = range
|
||||
}
|
||||
}
|
||||
attributeString.enumerateAttribute(.link, in: NSRange(0..<attributeString.length), options: [.longestEffectiveRangeNotRequired], using: enumerationBlock)
|
||||
attributeString.enumerateAttribute(.attachment, in: NSRange(0..<attributeString.length), options: [.longestEffectiveRangeNotRequired], using: enumerationBlock)
|
||||
}
|
||||
|
||||
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
guard let locationOfTouch = touches.first?.location(in: self) else {
|
||||
return
|
||||
}
|
||||
textContainer.size = bounds.size
|
||||
let indexOfCharacter = layoutManager.glyphIndex(for: locationOfTouch, in: textContainer)
|
||||
for (urlString, range) in links where NSLocationInRange(indexOfCharacter, range) {
|
||||
delegate?.tapableLabel(self, didTapUrl: urlString, atRange: range)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue