diff --git a/Signal/src/ViewControllers/ConversationView/ConversationInputTextView.h b/Signal/src/ViewControllers/ConversationView/ConversationInputTextView.h index 726ac1627..1b499ec73 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationInputTextView.h +++ b/Signal/src/ViewControllers/ConversationView/ConversationInputTextView.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // #import @@ -23,6 +23,7 @@ NS_ASSUME_NONNULL_BEGIN @protocol ConversationTextViewToolbarDelegate - (void)textViewDidChange:(UITextView *)textView; +- (void)textViewDidChangeSelection:(UITextView *)textView; @end diff --git a/Signal/src/ViewControllers/ConversationView/ConversationInputTextView.m b/Signal/src/ViewControllers/ConversationView/ConversationInputTextView.m index a23327590..be67f543d 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationInputTextView.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationInputTextView.m @@ -189,6 +189,11 @@ NS_ASSUME_NONNULL_BEGIN [self.textViewToolbarDelegate textViewDidChange:self]; } +- (void)textViewDidChangeSelection:(UITextView *)textView +{ + [self.textViewToolbarDelegate textViewDidChangeSelection:self]; +} + #pragma mark - Key Commands - (nullable NSArray *)keyCommands diff --git a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m index 791564043..b351b4d54 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m @@ -869,6 +869,11 @@ const CGFloat kMaxTextViewHeight = 98; [self updateInputLinkPreview]; } +- (void)textViewDidChangeSelection:(UITextView *)textView +{ + [self updateInputLinkPreview]; +} + - (void)updateHeightWithTextView:(UITextView *)textView { // compute new height assuming width is unchanged @@ -922,7 +927,10 @@ const CGFloat kMaxTextViewHeight = 98; return; } - NSString *_Nullable previewUrl = [OWSLinkPreview previewUrlForMessageBodyText:body]; + // It's key that we use the *raw/unstripped* text, so we can reconcile cursor position with the + // selectedRange. + NSString *_Nullable previewUrl = [OWSLinkPreview previewUrlForRawBodyText:self.inputTextView.text + selectedRange:self.inputTextView.selectedRange]; if (previewUrl.length < 1) { [self clearLinkPreviewStateAndView]; return; diff --git a/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift b/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift index 26df0bedb..9dfcede47 100644 --- a/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift +++ b/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift @@ -432,7 +432,11 @@ public class OWSLinkPreview: MTLModel { private static var previewUrlCache: NSCache = NSCache() @objc - public class func previewUrl(forMessageBodyText body: String?) -> String? { + public class func previewUrl(forRawBodyText body: String?, selectedRange: NSRange) -> String? { + return previewUrl(forMessageBodyText: body, selectedRange: selectedRange) + } + + public class func previewUrl(forMessageBodyText body: String?, selectedRange: NSRange?) -> String? { AssertIsOnMainThread() // Exit early if link previews are not enabled in order to avoid @@ -440,10 +444,15 @@ public class OWSLinkPreview: MTLModel { guard OWSLinkPreview.featureEnabled else { return nil } + guard SSKPreferences.areLinkPreviewsEnabled() else { return nil } + guard let body = body else { + return nil + } + if let cachedUrl = previewUrlCache.object(forKey: body as AnyObject) as? String { Logger.verbose("URL parsing cache hit.") guard cachedUrl.count > 0 else { @@ -451,27 +460,45 @@ public class OWSLinkPreview: MTLModel { } return cachedUrl } - let previewUrls = allPreviewUrls(forMessageBodyText: body) - guard let previewUrl = previewUrls.first else { + let previewUrlMatches = allPreviewUrlMatches(forMessageBodyText: body) + guard let urlMatch = previewUrlMatches.first else { // Use empty string to indicate "no preview URL" in the cache. previewUrlCache.setObject("" as AnyObject, forKey: body as AnyObject) return nil } - previewUrlCache.setObject(previewUrl as AnyObject, forKey: body as AnyObject) - return previewUrl + if let selectedRange = selectedRange { + Logger.verbose("match: urlString: \(urlMatch.urlString) range: \(urlMatch.matchRange) selectedRange: \(selectedRange)") + if selectedRange.location != body.count, + urlMatch.matchRange.intersection(selectedRange) != nil { + Logger.debug("ignoring URL, since the user is currently editing it.") + // we don't want to cache the result here, as we want to fetch the link preview + // if the user moves the cursor. + return nil + } + Logger.debug("considering URL, since the user is not currently editing it.") + } + + previewUrlCache.setObject(urlMatch.urlString as AnyObject, forKey: body as AnyObject) + return urlMatch.urlString } - class func allPreviewUrls(forMessageBodyText body: String?) -> [String] { + struct URLMatchResult { + let urlString: String + let matchRange: NSRange + } + + 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 [] } - guard let body = body else { - return [] - } let detector: NSDataDetector do { @@ -481,7 +508,7 @@ public class OWSLinkPreview: MTLModel { return [] } - var previewUrls = [String]() + var urlMatches: [URLMatchResult] = [] let matches = detector.matches(in: body, options: [], range: NSRange(location: 0, length: body.count)) for match in matches { guard let matchURL = match.url else { @@ -490,10 +517,11 @@ public class OWSLinkPreview: MTLModel { } let urlString = matchURL.absoluteString if isValidLinkUrl(urlString) { - previewUrls.append(urlString) + let matchResult = URLMatchResult(urlString: urlString, matchRange: match.range) + urlMatches.append(matchResult) } } - return previewUrls + return urlMatches } // MARK: - Preview Construction