fix snapshot not completed issue

This commit is contained in:
ryanzhao 2022-07-01 16:42:55 +10:00
parent e8518188ac
commit 1d9e4c88c2
6 changed files with 159 additions and 32 deletions

View File

@ -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 */,

View File

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

View File

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

View File

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

View File

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

View File

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