in-conversation search

- use MediaTime for computing benchmarks
This commit is contained in:
Michael Kirk 2019-02-11 09:49:26 -07:00
parent 6095500f80
commit 71dd4eb151
32 changed files with 697 additions and 70 deletions

View File

@ -338,7 +338,7 @@
45194F941FD7216000333B2C /* TSUnreadIndicatorInteraction.h in Headers */ = {isa = PBXBuildFile; fileRef = 34C42D641F4734ED0072EC04 /* TSUnreadIndicatorInteraction.h */; settings = {ATTRIBUTES = (Public, ); }; };
45194F951FD7216600333B2C /* TSUnreadIndicatorInteraction.m in Sources */ = {isa = PBXBuildFile; fileRef = 34C42D651F4734ED0072EC04 /* TSUnreadIndicatorInteraction.m */; };
451A13B11E13DED2000A50FD /* AppNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451A13B01E13DED2000A50FD /* AppNotifications.swift */; };
451F8A341FD710C3005CB9DA /* ConversationSearcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451777C71FD61554001225FF /* ConversationSearcher.swift */; };
451F8A341FD710C3005CB9DA /* FullTextSearcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451777C71FD61554001225FF /* FullTextSearcher.swift */; };
451F8A351FD710DE005CB9DA /* Searcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45360B8C1F9521F800FA666C /* Searcher.swift */; };
451F8A3B1FD71297005CB9DA /* UIUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = B97940261832BD2400BD66CB /* UIUtil.m */; };
451F8A3C1FD71392005CB9DA /* UIUtil.h in Headers */ = {isa = PBXBuildFile; fileRef = B97940251832BD2400BD66CB /* UIUtil.h */; settings = {ATTRIBUTES = (Public, ); }; };
@ -490,6 +490,7 @@
4CC0B59C20EC5F2E00CF6EE0 /* ConversationConfigurationSyncOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC0B59B20EC5F2E00CF6EE0 /* ConversationConfigurationSyncOperation.swift */; };
4CC1ECF9211A47CE00CC13BE /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CC1ECF8211A47CD00CC13BE /* StoreKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
4CC1ECFB211A553000CC13BE /* AppUpdateNag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC1ECFA211A553000CC13BE /* AppUpdateNag.swift */; };
4CC613362227A00400E21A3A /* ConversationSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC613352227A00400E21A3A /* ConversationSearch.swift */; };
4CEB78C92178EBAB00F315D2 /* OWSSessionResetJobRecord.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CEB78C82178EBAB00F315D2 /* OWSSessionResetJobRecord.m */; };
4CFE6B6C21F92BA700006701 /* LegacyNotificationsAdaptee.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFE6B6B21F92BA700006701 /* LegacyNotificationsAdaptee.swift */; };
70377AAB1918450100CAF501 /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 70377AAA1918450100CAF501 /* MobileCoreServices.framework */; };
@ -1055,7 +1056,7 @@
450DF2081E0DD2C6003D14BE /* UserNotificationsAdaptee.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; name = UserNotificationsAdaptee.swift; path = UserInterface/Notifications/UserNotificationsAdaptee.swift; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
451166BF1FD86B98000739BA /* AccountManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountManager.swift; sourceTree = "<group>"; };
451764291DE939FD00EDB8B9 /* ContactCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactCell.swift; sourceTree = "<group>"; };
451777C71FD61554001225FF /* ConversationSearcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationSearcher.swift; sourceTree = "<group>"; };
451777C71FD61554001225FF /* FullTextSearcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FullTextSearcher.swift; sourceTree = "<group>"; };
451A13B01E13DED2000A50FD /* AppNotifications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; name = AppNotifications.swift; path = UserInterface/Notifications/AppNotifications.swift; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
452037CF1EE84975004E4CDF /* DebugUISessionState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUISessionState.h; sourceTree = "<group>"; };
452037D01EE84975004E4CDF /* DebugUISessionState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugUISessionState.m; sourceTree = "<group>"; };
@ -1223,6 +1224,7 @@
4CC0B59B20EC5F2E00CF6EE0 /* ConversationConfigurationSyncOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationConfigurationSyncOperation.swift; sourceTree = "<group>"; };
4CC1ECF8211A47CD00CC13BE /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
4CC1ECFA211A553000CC13BE /* AppUpdateNag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUpdateNag.swift; sourceTree = "<group>"; };
4CC613352227A00400E21A3A /* ConversationSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationSearch.swift; sourceTree = "<group>"; };
4CEB78C72178EBAB00F315D2 /* OWSSessionResetJobRecord.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OWSSessionResetJobRecord.h; sourceTree = "<group>"; };
4CEB78C82178EBAB00F315D2 /* OWSSessionResetJobRecord.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OWSSessionResetJobRecord.m; sourceTree = "<group>"; };
4CFB4E9B220BC56D00ECB4DE /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = translations/nb.lproj/Localizable.strings; sourceTree = "<group>"; };
@ -1577,7 +1579,7 @@
4C948FF62146EB4800349F0D /* BlockListCache.swift */,
343D3D991E9283F100165CA4 /* BlockListUIUtils.h */,
343D3D9A1E9283F100165CA4 /* BlockListUIUtils.m */,
451777C71FD61554001225FF /* ConversationSearcher.swift */,
451777C71FD61554001225FF /* FullTextSearcher.swift */,
3466087120E550F300AFFE73 /* ConversationStyle.swift */,
34480B4D1FD0A7A300BC14EF /* DebugLogger.h */,
34480B4E1FD0A7A300BC14EF /* DebugLogger.m */,
@ -2074,6 +2076,7 @@
4C090A1A210FD9C7001FD7F9 /* HapticFeedback.swift */,
34B3F8331E8DF1700035BE1A /* ViewControllers */,
76EB052B18170B33006006FC /* Views */,
4CC613352227A00400E21A3A /* ConversationSearch.swift */,
);
name = UserInterface;
sourceTree = "<group>";
@ -3382,7 +3385,7 @@
452C7CA72037628B003D51A5 /* Weak.swift in Sources */,
34D5872F208E2C4200D2255A /* OWS109OutgoingMessageState.m in Sources */,
34AC09F8211B39B100997B47 /* CountryCodeViewController.m in Sources */,
451F8A341FD710C3005CB9DA /* ConversationSearcher.swift in Sources */,
451F8A341FD710C3005CB9DA /* FullTextSearcher.swift in Sources */,
346129FE1FD5F31400532771 /* OWS106EnsureProfileComplete.swift in Sources */,
34AC0A10211B39EA00997B47 /* TappableView.swift in Sources */,
346129F91FD5F31400532771 /* OWS104CreateRecipientIdentities.m in Sources */,
@ -3541,6 +3544,7 @@
340FC8BC204DAC8D007AEB0F /* FingerprintViewController.m in Sources */,
450DF2051E0D74AC003D14BE /* Platform.swift in Sources */,
34A4C61E221613D00042EF2E /* OnboardingVerificationViewController.swift in Sources */,
4CC613362227A00400E21A3A /* ConversationSearch.swift in Sources */,
340FC8B2204DAC8D007AEB0F /* AdvancedSettingsTableViewController.m in Sources */,
452B999020A34B6B006F2F9E /* AddContactShareToExistingContactViewController.swift in Sources */,
346129991FD1E4DA00532771 /* SignalApp.m in Sources */,

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "chevron-down-24@1x.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "chevron-down-24@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "chevron-down-24@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 B

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "chevron-up-24@1x.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "chevron-up-24@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "chevron-up-24@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 B

View File

@ -0,0 +1,289 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
import Foundation
@objc
public protocol ConversationSearchControllerDelegate: UISearchControllerDelegate {
@objc
func conversationSearchController(_ conversationSearchController: ConversationSearchController,
didUpdateSearchResults resultSet: ConversationScreenSearchResultSet?)
@objc
func conversationSearchController(_ conversationSearchController: ConversationSearchController,
didSelectMessageId: String)
}
@objc
public class ConversationSearchController: NSObject {
@objc
public static let kMinimumSearchTextLength: UInt = 2
@objc
public let uiSearchController = UISearchController(searchResultsController: nil)
@objc
public weak var delegate: ConversationSearchControllerDelegate?
let thread: TSThread
let resultsBar: SearchResultsBar = SearchResultsBar(frame: .zero)
// MARK: Initializer
@objc
required public init(thread: TSThread) {
self.thread = thread
super.init()
resultsBar.resultsBarDelegate = self
uiSearchController.delegate = self
uiSearchController.searchResultsUpdater = self
uiSearchController.hidesNavigationBarDuringPresentation = false
uiSearchController.dimsBackgroundDuringPresentation = false
uiSearchController.searchBar.inputAccessoryView = resultsBar
applyTheme()
}
func applyTheme() {
OWSSearchBar.applyTheme(to: uiSearchController.searchBar)
}
// MARK: Dependencies
var dbReadConnection: YapDatabaseConnection {
return OWSPrimaryStorage.shared().dbReadConnection
}
}
extension ConversationSearchController: UISearchControllerDelegate {
public func didPresentSearchController(_ searchController: UISearchController) {
Logger.verbose("")
delegate?.didPresentSearchController?(searchController)
}
public func didDismissSearchController(_ searchController: UISearchController) {
Logger.verbose("")
delegate?.didDismissSearchController?(searchController)
}
}
extension ConversationSearchController: UISearchResultsUpdating {
var dbSearcher: FullTextSearcher {
return FullTextSearcher.shared
}
public func updateSearchResults(for searchController: UISearchController) {
Logger.verbose("searchBar.text: \( searchController.searchBar.text ?? "<blank>")")
guard let searchText = searchController.searchBar.text?.stripped else {
self.resultsBar.updateResults(resultSet: nil)
self.delegate?.conversationSearchController(self, didUpdateSearchResults: nil)
return
}
BenchManager.startEvent(title: "Conversation Search", eventId: searchText)
guard searchText.count >= ConversationSearchController.kMinimumSearchTextLength else {
self.resultsBar.updateResults(resultSet: nil)
self.delegate?.conversationSearchController(self, didUpdateSearchResults: nil)
return
}
var resultSet: ConversationScreenSearchResultSet?
self.dbReadConnection.asyncRead({ [weak self] transaction in
guard let self = self else {
return
}
resultSet = self.dbSearcher.searchWithinConversation(thread: self.thread, searchText: searchText, transaction: transaction)
}, completionBlock: { [weak self] in
guard let self = self else {
return
}
self.resultsBar.updateResults(resultSet: resultSet)
self.delegate?.conversationSearchController(self, didUpdateSearchResults: resultSet)
})
}
}
extension ConversationSearchController: SearchResultsBarDelegate {
func searchResultsBar(_ searchResultsBar: SearchResultsBar,
setCurrentIndex currentIndex: Int,
resultSet: ConversationScreenSearchResultSet) {
guard let searchResult = resultSet.messages[safe: currentIndex] else {
owsFailDebug("messageId was unexpectedly nil")
return
}
BenchEventStart(title: "Conversation Search Nav", eventId: "Conversation Search Nav: \(searchResult.messageId)")
self.delegate?.conversationSearchController(self, didSelectMessageId: searchResult.messageId)
}
}
protocol SearchResultsBarDelegate: AnyObject {
func searchResultsBar(_ searchResultsBar: SearchResultsBar,
setCurrentIndex currentIndex: Int,
resultSet: ConversationScreenSearchResultSet)
}
class SearchResultsBar: UIToolbar {
weak var resultsBarDelegate: SearchResultsBarDelegate?
var showLessRecentButton: UIBarButtonItem!
var showMoreRecentButton: UIBarButtonItem!
let labelItem: UIBarButtonItem
var resultSet: ConversationScreenSearchResultSet?
override init(frame: CGRect) {
labelItem = UIBarButtonItem(title: nil, style: .plain, target: nil, action: nil)
super.init(frame: frame)
let leftExteriorChevronMargin: CGFloat
let leftInteriorChevronMargin: CGFloat
if CurrentAppContext().isRTL {
leftExteriorChevronMargin = 8
leftInteriorChevronMargin = 0
} else {
leftExteriorChevronMargin = 0
leftInteriorChevronMargin = 8
}
let upChevron = #imageLiteral(resourceName: "ic_chevron_up").withRenderingMode(.alwaysTemplate)
showLessRecentButton = UIBarButtonItem(image: upChevron, style: .plain, target: self, action: #selector(didTapShowLessRecent))
showLessRecentButton.imageInsets = UIEdgeInsets(top: 2, left: leftExteriorChevronMargin, bottom: 2, right: leftInteriorChevronMargin)
showLessRecentButton.tintColor = UIColor.ows_systemPrimaryButton
let downChevron = #imageLiteral(resourceName: "ic_chevron_down").withRenderingMode(.alwaysTemplate)
showMoreRecentButton = UIBarButtonItem(image: downChevron, style: .plain, target: self, action: #selector(didTapShowMoreRecent))
showMoreRecentButton.imageInsets = UIEdgeInsets(top: 2, left: leftInteriorChevronMargin, bottom: 2, right: leftExteriorChevronMargin)
showMoreRecentButton.tintColor = UIColor.ows_systemPrimaryButton
let spacer1 = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let spacer2 = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
self.items = [showLessRecentButton, showMoreRecentButton, spacer1, labelItem, spacer2]
self.isTranslucent = false
self.isOpaque = true
self.barTintColor = Theme.toolbarBackgroundColor
self.autoresizingMask = .flexibleHeight
self.translatesAutoresizingMaskIntoConstraints = false
}
required init?(coder aDecoder: NSCoder) {
notImplemented()
}
@objc
public func didTapShowLessRecent() {
Logger.debug("")
guard let resultSet = resultSet else {
owsFailDebug("resultSet was unexpectedly nil")
return
}
guard let currentIndex = currentIndex else {
owsFailDebug("currentIndex was unexpectedly nil")
return
}
guard currentIndex + 1 < resultSet.messages.count else {
owsFailDebug("showLessRecent button should be disabled")
return
}
let newIndex = currentIndex + 1
self.currentIndex = newIndex
updateBarItems()
resultsBarDelegate?.searchResultsBar(self, setCurrentIndex: newIndex, resultSet: resultSet)
}
@objc
public func didTapShowMoreRecent() {
Logger.debug("")
guard let resultSet = resultSet else {
owsFailDebug("resultSet was unexpectedly nil")
return
}
guard let currentIndex = currentIndex else {
owsFailDebug("currentIndex was unexpectedly nil")
return
}
guard currentIndex > 0 else {
owsFailDebug("showMoreRecent button should be disabled")
return
}
let newIndex = currentIndex - 1
self.currentIndex = newIndex
updateBarItems()
resultsBarDelegate?.searchResultsBar(self, setCurrentIndex: newIndex, resultSet: resultSet)
}
var currentIndex: Int?
// MARK:
func updateResults(resultSet: ConversationScreenSearchResultSet?) {
if let resultSet = resultSet {
if resultSet.messages.count > 0 {
currentIndex = min(currentIndex ?? 0, resultSet.messages.count - 1)
} else {
currentIndex = nil
}
} else {
currentIndex = nil
}
self.resultSet = resultSet
updateBarItems()
if let currentIndex = currentIndex, let resultSet = resultSet {
resultsBarDelegate?.searchResultsBar(self, setCurrentIndex: currentIndex, resultSet: resultSet)
}
}
func updateBarItems() {
guard let resultSet = resultSet else {
labelItem.title = nil
showMoreRecentButton.isEnabled = false
showLessRecentButton.isEnabled = false
return
}
switch resultSet.messages.count {
case 0:
labelItem.title = NSLocalizedString("CONVERSATION_SEARCH_NO_RESULTS", comment: "keyboard toolbar label when no messages match the search string")
case 1:
labelItem.title = NSLocalizedString("CONVERSATION_SEARCH_ONE_RESULT", comment: "keyboard toolbar label when exactly 1 message matches the search string")
default:
let format = NSLocalizedString("CONVERSATION_SEARCH_RESULTS_FORMAT",
comment: "keyboard toolbar label when more than 1 message matches the search string. Embeds {{number/position of the 'currently viewed' result}} and the {{total number of results}}")
guard let currentIndex = currentIndex else {
owsFailDebug("currentIndex was unexpectedly nil")
return
}
labelItem.title = String(format: format, currentIndex + 1, resultSet.messages.count)
}
if let currentIndex = currentIndex {
showMoreRecentButton.isEnabled = currentIndex > 0
showLessRecentButton.isEnabled = currentIndex + 1 < resultSet.messages.count
} else {
showMoreRecentButton.isEnabled = false
showLessRecentButton.isEnabled = false
}
}
}

View File

@ -59,6 +59,8 @@ extern const UIDataDetectorTypes kOWSAllowedDataDetectorTypes;
- (void)didTapShowAddToContactUIForContactShare:(ContactShareViewModel *)contactShare
NS_SWIFT_NAME(didTapShowAddToContactUI(forContactShare:));
@property (nonatomic, readonly, nullable) NSString *lastSearchedText;
@end
#pragma mark -

View File

@ -691,6 +691,7 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
}
[self.class loadForTextDisplay:self.bodyTextView
text:self.displayableBodyText.displayText
searchText:self.delegate.lastSearchedText
textColor:self.bodyTextColor
font:self.textMessageFont
shouldIgnoreEvents:shouldIgnoreEvents];
@ -698,6 +699,7 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
+ (void)loadForTextDisplay:(OWSMessageTextView *)textView
text:(NSString *)text
searchText:(nullable NSString *)searchText
textColor:(UIColor *)textColor
font:(UIFont *)font
shouldIgnoreEvents:(BOOL)shouldIgnoreEvents
@ -713,8 +715,29 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
};
textView.shouldIgnoreEvents = shouldIgnoreEvents;
NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc]
initWithString:text
attributes:@{ NSFontAttributeName : font, NSForegroundColorAttributeName : textColor }];
if (searchText.length >= ConversationSearchController.kMinimumSearchTextLength) {
NSError *error;
NSRegularExpression *regex = [[NSRegularExpression alloc] initWithPattern:searchText
options:NSRegularExpressionCaseInsensitive
error:&error];
OWSAssertDebug(error == nil);
for (NSTextCheckingResult *match in
[regex matchesInString:text options:NSMatchingWithoutAnchoringBounds range:NSMakeRange(0, text.length)]) {
OWSAssertDebug(match.range.length >= ConversationSearchController.kMinimumSearchTextLength);
[attributedText addAttribute:NSBackgroundColorAttributeName value:UIColor.yellowColor range:match.range];
[attributedText addAttribute:NSForegroundColorAttributeName value:UIColor.ows_blackColor range:match.range];
}
}
// For perf, set text last. Otherwise changing font/color is more expensive.
textView.text = text;
// We use attributedText even when we're not highlighting searched text to esnure any lingering
// attributes are reset.
textView.attributedText = attributedText;
}
- (BOOL)shouldShowSenderName
@ -1259,6 +1282,7 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
[self.bodyTextView removeFromSuperview];
self.bodyTextView.text = nil;
self.bodyTextView.attributedText = nil;
self.bodyTextView.hidden = YES;
self.bubbleView.bubbleColor = nil;

View File

@ -211,7 +211,7 @@ public class ConversationMessageMapping: NSObject {
// Tries to ensure that the load window includes a given item.
// On success, returns the index path of that item.
// On failure, returns nil.
@objc
@objc(ensureLoadWindowContainsUniqueId:transaction:)
public func ensureLoadWindowContains(uniqueId: String,
transaction: YapDatabaseReadTransaction) -> IndexPath? {
if let oldIndex = loadedUniqueIds().firstIndex(of: uniqueId) {

View File

@ -141,6 +141,7 @@ typedef enum : NSUInteger {
ConversationViewLayoutDelegate,
ConversationViewCellDelegate,
ConversationInputTextViewDelegate,
ConversationSearchControllerDelegate,
LongTextViewDelegate,
MessageActionsDelegate,
MessageDetailViewDelegate,
@ -195,6 +196,8 @@ typedef enum : NSUInteger {
@property (nonatomic) BOOL userHasScrolled;
@property (nonatomic, nullable) NSDate *lastMessageSentDate;
@property (nonatomic, nullable) UIBarButtonItem *customBackButton;
@property (nonatomic) BOOL showLoadMoreHeader;
@property (nonatomic) UILabel *loadMoreHeader;
@property (nonatomic) uint64_t lastVisibleSortId;
@ -227,6 +230,10 @@ typedef enum : NSUInteger {
@property (nonatomic) ScrollContinuity scrollContinuity;
@property (nonatomic, nullable) NSTimer *autoLoadMoreTimer;
@property (nonatomic, readonly) ConversationSearchController *searchController;
@property (nonatomic, nullable) NSString *lastSearchedText;
@property (nonatomic) BOOL isShowingSearchUI;
@end
#pragma mark -
@ -509,6 +516,9 @@ typedef enum : NSUInteger {
[self updateConversationSnapshot];
_searchController = [[ConversationSearchController alloc] initWithThread:thread];
_searchController.delegate = self;
[self updateShouldObserveVMUpdates];
self.reloadTimer = [NSTimer weakScheduledTimerWithTimeInterval:1.f
@ -1391,6 +1401,7 @@ typedef enum : NSUInteger {
- (void)createBackButton
{
UIBarButtonItem *backItem = [self createOWSBackButton];
self.customBackButton = backItem;
if (backItem.customView) {
// This method gets called multiple times, so it's important we re-layout the unread badge
// with respect to the new backItem.
@ -1425,11 +1436,23 @@ typedef enum : NSUInteger {
- (void)updateBarButtonItems
{
self.navigationItem.hidesBackButton = NO;
if (self.customBackButton) {
self.navigationItem.leftBarButtonItem = self.customBackButton;
}
if (self.userLeftGroup) {
self.navigationItem.rightBarButtonItems = @[];
return;
}
if (self.isShowingSearchUI) {
self.navigationItem.rightBarButtonItems = @[];
self.navigationItem.leftBarButtonItem = nil;
self.navigationItem.hidesBackButton = YES;
return;
}
const CGFloat kBarButtonSize = 44;
NSMutableArray<UIBarButtonItem *> *barButtons = [NSMutableArray new];
if ([self canCall]) {
@ -3944,19 +3967,107 @@ typedef enum : NSUInteger {
[self updateGroupModelTo:groupModel successCompletion:nil];
}
- (void)popAllConversationSettingsViews
- (void)popAllConversationSettingsViewsWithCompletion:(void (^_Nullable)(void))completionBlock
{
if (self.presentedViewController) {
[self.presentedViewController
dismissViewControllerAnimated:YES
completion:^{
[self.navigationController popToViewController:self animated:YES];
}];
[self.presentedViewController dismissViewControllerAnimated:YES
completion:^{
[self.navigationController
popToViewController:self
animated:YES
completion:completionBlock];
}];
} else {
[self.navigationController popToViewController:self animated:YES];
[self.navigationController popToViewController:self animated:YES completion:completionBlock];
}
}
#pragma mark - Conversation Search
- (void)conversationSettingsDidRequestConversationSearch:(OWSConversationSettingsViewController *)conversationSettingsViewController
{
[self showSearchUI];
[self popAllConversationSettingsViewsWithCompletion:^{
// This delay is unfortunate, but without it, self.searchController.uiSearchController.searchBar
// isn't yet ready to become first responder. Presumably we're still mid transition.
// A hardcorded constant like this isn't great because it's either too slow, making our users
// wait, or too fast, and fails to wait long enough to be ready to become first responder.
// Luckily in this case the stakes aren't catastrophic. In the case that we're too aggressive
// the user will just have to manually tap into the search field before typing.
// Leaving this assert in as proof that we're not ready to become first responder yet.
// If this assert fails, *great* maybe we can get rid of this delay.
OWSAssertDebug(![self.searchController.uiSearchController.searchBar canBecomeFirstResponder]);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.searchController.uiSearchController.searchBar becomeFirstResponder];
});
}];
}
- (void)showSearchUI
{
self.isShowingSearchUI = YES;
self.navigationItem.titleView = self.searchController.uiSearchController.searchBar;
[self updateBarButtonItems];
}
- (void)hideSearchUI
{
self.isShowingSearchUI = NO;
self.navigationItem.titleView = self.headerView;
[self updateBarButtonItems];
// restore first responder to VC
[self becomeFirstResponder];
}
#pragma mark ConversationSearchControllerDelegate
- (void)didDismissSearchController:(UISearchController *)searchController
{
OWSLogVerbose(@"");
OWSAssertIsOnMainThread();
[self hideSearchUI];
}
- (void)conversationSearchController:(ConversationSearchController *)conversationSearchController
didUpdateSearchResults:(nullable ConversationScreenSearchResultSet *)conversationScreenSearchResultSet
{
OWSAssertIsOnMainThread();
OWSLogInfo(@"conversationScreenSearchResultSet: %@", conversationScreenSearchResultSet.debugDescription);
self.lastSearchedText = conversationScreenSearchResultSet.searchText;
[UIView performWithoutAnimation:^{
[self.collectionView reloadItemsAtIndexPaths:self.collectionView.indexPathsForVisibleItems];
}];
if (conversationScreenSearchResultSet) {
[BenchManager completeEventWithEventId:self.lastSearchedText];
}
}
- (void)conversationSearchController:(ConversationSearchController *)conversationSearchController
didSelectMessageId:(NSString *)messageId
{
OWSLogDebug(@"messageId: %@", messageId);
[self scrollToInteractionId:messageId];
[BenchManager completeEventWithEventId:[NSString stringWithFormat:@"Conversation Search Nav: %@", messageId]];
}
- (void)scrollToInteractionId:(NSString *)interactionId
{
NSIndexPath *_Nullable indexPath = [self.conversationViewModel ensureLoadWindowContainsInteractionId:interactionId];
if (!indexPath) {
OWSFailDebug(@"unable to find indexPath");
return;
}
[self.collectionView scrollToItemAtIndexPath:indexPath
atScrollPosition:UICollectionViewScrollPositionCenteredVertically
animated:YES];
}
#pragma mark - ConversationViewLayoutDelegate
- (NSArray<id<ConversationViewLayoutItem>> *)layoutItems

View File

@ -109,6 +109,7 @@ typedef NS_ENUM(NSUInteger, ConversationUpdateItemType) {
- (BOOL)canLoadMoreItems;
- (nullable NSIndexPath *)ensureLoadWindowContainsQuotedReply:(OWSQuotedReplyModel *)quotedReply;
- (nullable NSIndexPath *)ensureLoadWindowContainsInteractionId:(NSString *)interactionId;
- (void)appendUnsavedOutgoingTextMessage:(TSOutgoingMessage *)outgoingMessage;

View File

@ -1595,8 +1595,32 @@ static const int kYapDatabaseRangeMaxLength = 25000;
return;
}
indexPath = [self.messageMapping ensureLoadWindowContainsWithUniqueId:quotedInteraction.uniqueId
transaction:transaction];
indexPath =
[self.messageMapping ensureLoadWindowContainsUniqueId:quotedInteraction.uniqueId transaction:transaction];
}];
self.collapseCutoffDate = [NSDate new];
[self ensureDynamicInteractionsAndUpdateIfNecessary:NO];
if (![self reloadViewItems]) {
OWSFailDebug(@"failed to reload view items in resetMapping.");
}
[self.delegate conversationViewModelDidUpdate:ConversationUpdate.reloadUpdate];
[self.delegate conversationViewModelRangeDidChange];
return indexPath;
}
- (nullable NSIndexPath *)ensureLoadWindowContainsInteractionId:(NSString *)interactionId
{
OWSAssertIsOnMainThread();
OWSAssertDebug(interactionId);
__block NSIndexPath *_Nullable indexPath = nil;
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
indexPath = [self.messageMapping ensureLoadWindowContainsUniqueId:interactionId transaction:transaction];
}];
self.collapseCutoffDate = [NSDate new];

View File

@ -37,8 +37,8 @@ class ConversationSearchViewController: UITableViewController, BlockListCacheDel
return OWSPrimaryStorage.shared().uiDatabaseConnection
}
var searcher: ConversationSearcher {
return ConversationSearcher.shared
var searcher: FullTextSearcher {
return FullTextSearcher.shared
}
private var contactsManager: OWSContactsManager {

View File

@ -763,6 +763,10 @@ class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDele
UIPasteboard.general.string = messageTimestamp
}
var lastSearchedText: String? {
return nil
}
// MediaGalleryDataSourceDelegate
func mediaGalleryDataSource(_ mediaGalleryDataSource: MediaGalleryDataSource, willDelete items: [MediaGalleryItem], initiatedBy: AnyObject) {

View File

@ -45,7 +45,7 @@ NS_ASSUME_NONNULL_BEGIN
MFMessageComposeViewControllerDelegate>
@property (nonatomic, readonly) ContactsViewHelper *contactsViewHelper;
@property (nonatomic, readonly) ConversationSearcher *conversationSearcher;
@property (nonatomic, readonly) FullTextSearcher *fullTextSearcher;
@property (nonatomic, readonly) UIView *noSignalContactsView;
@ -75,9 +75,9 @@ NS_ASSUME_NONNULL_BEGIN
#pragma mark - Dependencies
- (ConversationSearcher *)conversationSearcher
- (FullTextSearcher *)fullTextSearcher
{
return ConversationSearcher.shared;
return FullTextSearcher.shared;
}
- (YapDatabaseConnection *)uiDatabaseConnection
@ -903,9 +903,9 @@ NS_ASSUME_NONNULL_BEGIN
[self.uiDatabaseConnection
asyncReadWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
self.searchResults = [self.conversationSearcher searchForComposeScreenWithSearchText:searchText
transaction:transaction
contactsManager:self.contactsManager];
self.searchResults = [self.fullTextSearcher searchForComposeScreenWithSearchText:searchText
transaction:transaction
contactsManager:self.contactsManager];
}
completionBlock:^{
__typeof(self) strongSelf = weakSelf;

View File

@ -1,16 +1,13 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import "OWSConversationSettingsViewDelegate.h"
#import <SignalMessaging/OWSViewController.h>
NS_ASSUME_NONNULL_BEGIN
@interface NewGroupViewController : OWSViewController
@property (nonatomic, weak) id<OWSConversationSettingsViewDelegate> delegate;
@end
NS_ASSUME_NONNULL_END

View File

@ -321,13 +321,6 @@ const CGFloat kIconViewLength = 24;
mainSection.customHeaderView = [self mainSectionHeader];
mainSection.customHeaderHeight = @(100.f);
[mainSection addItem:[OWSTableItem itemWithCustomCellBlock:^{
return [weakSelf disclosureCellWithName:MediaStrings.allMedia iconName:@"actionsheet_camera_roll_black"];
}
actionBlock:^{
[weakSelf showMediaGallery];
}]];
if ([self.thread isKindOfClass:[TSContactThread class]] && self.contactsManager.supportsContactEditing
&& !self.hasExistingContact) {
[mainSection addItem:[OWSTableItem itemWithCustomCellBlock:^{
@ -354,6 +347,28 @@ const CGFloat kIconViewLength = 24;
}]];
}
[mainSection addItem:[OWSTableItem
itemWithCustomCellBlock:^{
return [weakSelf disclosureCellWithName:MediaStrings.allMedia
iconName:@"actionsheet_camera_roll_black"];
}
actionBlock:^{
[weakSelf showMediaGallery];
}]];
// TODO icon
[mainSection addItem:[OWSTableItem
itemWithCustomCellBlock:^{
NSString *title = NSLocalizedString(@"CONVERSATION_SETTINGS_SEARCH",
@"Table cell label in conversation settings which returns the user to the "
@"conversation with 'search mode' activated");
return
[weakSelf disclosureCellWithName:title iconName:@"actionsheet_camera_roll_black"];
}
actionBlock:^{
[weakSelf tappedConversationSearch];
}]];
if (!isNoteToSelf && !self.isGroupThread && self.thread.hasSafetyNumbers) {
[mainSection addItem:[OWSTableItem itemWithCustomCellBlock:^{
return [weakSelf
@ -1331,6 +1346,11 @@ const CGFloat kIconViewLength = 24;
[mediaGallery pushTileViewFromNavController:(OWSNavigationController *)self.navigationController];
}
- (void)tappedConversationSearch
{
[self.conversationSettingsViewDelegate conversationSettingsDidRequestConversationSearch:self];
}
#pragma mark - Notifications
- (void)identityStateDidChange:(NSNotification *)notification

View File

@ -1,17 +1,19 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
@class OWSConversationSettingsViewController;
@class TSGroupModel;
@protocol OWSConversationSettingsViewDelegate <NSObject>
- (void)conversationColorWasUpdated;
- (void)groupWasUpdated:(TSGroupModel *)groupModel;
- (void)conversationSettingsDidRequestConversationSearch:(OWSConversationSettingsViewController *)conversationSettingsViewController;
- (void)popAllConversationSettingsViews;
- (void)popAllConversationSettingsViewsWithCompletion:(void (^_Nullable)(void))completionBlock;
@end

View File

@ -431,7 +431,8 @@ NS_ASSUME_NONNULL_BEGIN
[self updateGroup];
[self.conversationSettingsViewDelegate popAllConversationSettingsViews];
[self.conversationSettingsViewDelegate
popAllConversationSettingsViewsWithCompletion:nil];
}]];
[controller addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"ALERT_DONT_SAVE",
@"The label for the 'don't save' button in action sheets.")
@ -448,7 +449,7 @@ NS_ASSUME_NONNULL_BEGIN
[self updateGroup];
[self.conversationSettingsViewDelegate popAllConversationSettingsViews];
[self.conversationSettingsViewDelegate popAllConversationSettingsViewsWithCompletion:nil];
}
- (void)groupNameDidChange:(id)sender

View File

@ -8,7 +8,7 @@ import XCTest
// TODO: We might be able to merge this with OWSFakeContactsManager.
@objc
class ConversationSearcherContactsManager: NSObject, ContactsManagerProtocol {
class FullTextSearcherContactsManager: NSObject, ContactsManagerProtocol {
func displayName(forPhoneIdentifier recipientId: String?, transaction: YapDatabaseReadTransaction) -> String {
return self.displayName(forPhoneIdentifier: recipientId)
}
@ -57,11 +57,11 @@ class ConversationSearcherContactsManager: NSObject, ContactsManagerProtocol {
private let bobRecipientId = "+49030183000"
private let aliceRecipientId = "+12345678900"
class ConversationSearcherTest: SignalBaseTest {
class FullTextSearcherTest: SignalBaseTest {
// MARK: - Dependencies
var searcher: ConversationSearcher {
return ConversationSearcher.shared
var searcher: FullTextSearcher {
return FullTextSearcher.shared
}
var dbConnection: YapDatabaseConnection {
@ -80,7 +80,7 @@ class ConversationSearcherTest: SignalBaseTest {
FullTextSearchFinder.ensureDatabaseExtensionRegistered(storage: OWSPrimaryStorage.shared())
// Replace this singleton.
SSKEnvironment.shared.contactsManager = ConversationSearcherContactsManager()
SSKEnvironment.shared.contactsManager = FullTextSearcherContactsManager()
self.dbConnection.readWrite { transaction in
let bookModel = TSGroupModel(title: "Book Club", memberIds: [aliceRecipientId, bobRecipientId], image: nil, groupId: Randomness.generateRandomBytes(kGroupIdLength))

View File

@ -569,6 +569,15 @@
/* Title for the 'conversation delete confirmation' alert. */
"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "Delete Conversation?";
/* keyboard toolbar label when no messages match the search string */
"CONVERSATION_SEARCH_NO_RESULTS" = "No matches";
/* keyboard toolbar label when exactly 1 message matches the search string */
"CONVERSATION_SEARCH_ONE_RESULT" = "1 match";
/* keyboard toolbar label when more than 1 message matches the search string. Embeds {{number/position of the 'currently viewed' result}} and the {{total number of results}} */
"CONVERSATION_SEARCH_RESULTS_FORMAT" = "%d of %d matches";
/* title for conversation settings screen */
"CONVERSATION_SETTINGS" = "Conversation Settings";
@ -620,6 +629,9 @@
/* Label for 'new contact' button in conversation settings view. */
"CONVERSATION_SETTINGS_NEW_CONTACT" = "Create New Contact";
/* Table cell label in conversation settings which returns the user to the conversation with 'search mode' activated */
"CONVERSATION_SETTINGS_SEARCH" = "Search Conversation";
/* Label for button that opens conversation settings. */
"CONVERSATION_SETTINGS_TAP_TO_CHANGE" = "Tap to Change";

View File

@ -33,7 +33,7 @@ NS_ASSUME_NONNULL_BEGIN
NewNonContactConversationViewControllerDelegate>
@property (nonatomic, readonly) ContactsViewHelper *contactsViewHelper;
@property (nonatomic, readonly) ConversationSearcher *conversationSearcher;
@property (nonatomic, readonly) FullTextSearcher *fullTextSearcher;
@property (nonatomic, readonly) ThreadViewHelper *threadViewHelper;
@property (nonatomic, readonly) YapDatabaseConnection *uiDatabaseConnection;
@ -59,7 +59,7 @@ NS_ASSUME_NONNULL_BEGIN
self.view.backgroundColor = Theme.backgroundColor;
_contactsViewHelper = [[ContactsViewHelper alloc] initWithDelegate:self];
_conversationSearcher = ConversationSearcher.shared;
_fullTextSearcher = FullTextSearcher.shared;
_threadViewHelper = [ThreadViewHelper new];
_threadViewHelper.delegate = self;
@ -341,7 +341,7 @@ NS_ASSUME_NONNULL_BEGIN
{
NSString *searchTerm = [[self.searchBar text] ows_stripped];
return [self.conversationSearcher filterThreads:self.threadViewHelper.threads withSearchText:searchTerm];
return [self.fullTextSearcher filterThreads:self.threadViewHelper.threads withSearchText:searchTerm];
}
- (NSArray<SignalAccount *> *)filteredSignalAccountsWithSearchText

View File

@ -32,7 +32,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic) BOOL shouldNotifyDelegateOfUpdatedContacts;
@property (nonatomic) BOOL hasUpdatedContactsAtLeastOnce;
@property (nonatomic) OWSProfileManager *profileManager;
@property (nonatomic, readonly) ConversationSearcher *conversationSearcher;
@property (nonatomic, readonly) FullTextSearcher *fullTextSearcher;
@end
@ -54,7 +54,7 @@ NS_ASSUME_NONNULL_BEGIN
_blockListCache = [OWSBlockListCache new];
[_blockListCache startObservingAndSyncStateWithDelegate:self];
_conversationSearcher = ConversationSearcher.shared;
_fullTextSearcher = FullTextSearcher.shared;
_contactsManager = Environment.shared.contactsManager;
_profileManager = [OWSProfileManager sharedManager];
@ -210,8 +210,7 @@ NS_ASSUME_NONNULL_BEGIN
NSMutableArray<SignalAccount *> *signalAccountsToSearch = [self.signalAccounts mutableCopy];
SignalAccount *selfAccount = [[SignalAccount alloc] initWithRecipientId:self.localNumber];
[signalAccountsToSearch addObject:selfAccount];
return [self.conversationSearcher filterSignalAccounts:signalAccountsToSearch
withSearchText:searchText];
return [self.fullTextSearcher filterSignalAccounts:signalAccountsToSearch withSearchText:searchText];
}
- (BOOL)doesContact:(Contact *)contact matchSearchTerm:(NSString *)searchTerm

View File

@ -1,11 +1,13 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
@interface OWSSearchBar : UISearchBar
+ (void)applyThemeToSearchBar:(UISearchBar *)searchBar;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,5 +1,5 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import "OWSSearchBar.h"
@ -54,36 +54,41 @@ NS_ASSUME_NONNULL_BEGIN
}
- (void)ows_applyTheme
{
[self.class applyThemeToSearchBar:self];
}
+ (void)applyThemeToSearchBar:(UISearchBar *)searchBar
{
OWSAssertIsOnMainThread();
UIColor *foregroundColor = Theme.placeholderColor;
self.barTintColor = Theme.backgroundColor;
self.barStyle = Theme.barStyle;
searchBar.barTintColor = Theme.backgroundColor;
searchBar.barStyle = Theme.barStyle;
// Hide searchBar border.
// Alternatively we could hide the border by using `UISearchBarStyleMinimal`, but that causes an issue when toggling
// from light -> dark -> light theme wherein the textField background color appears darker than it should
// (regardless of our re-setting textfield.backgroundColor below).
self.backgroundImage = [UIImage new];
searchBar.backgroundImage = [UIImage new];
if (Theme.isDarkThemeEnabled) {
UIImage *clearImage = [UIImage imageNamed:@"searchbar_clear"];
[self setImage:[clearImage asTintedImageWithColor:foregroundColor]
[searchBar setImage:[clearImage asTintedImageWithColor:foregroundColor]
forSearchBarIcon:UISearchBarIconClear
state:UIControlStateNormal];
UIImage *searchImage = [UIImage imageNamed:@"searchbar_search"];
[self setImage:[searchImage asTintedImageWithColor:foregroundColor]
[searchBar setImage:[searchImage asTintedImageWithColor:foregroundColor]
forSearchBarIcon:UISearchBarIconSearch
state:UIControlStateNormal];
} else {
[self setImage:nil forSearchBarIcon:UISearchBarIconClear state:UIControlStateNormal];
[searchBar setImage:nil forSearchBarIcon:UISearchBarIconClear state:UIControlStateNormal];
[self setImage:nil forSearchBarIcon:UISearchBarIconSearch state:UIControlStateNormal];
[searchBar setImage:nil forSearchBarIcon:UISearchBarIconSearch state:UIControlStateNormal];
}
[self traverseViewHierarchyWithVisitor:^(UIView *view) {
[searchBar traverseViewHierarchyWithVisitor:^(UIView *view) {
if ([view isKindOfClass:[UITextField class]]) {
UITextField *textField = (UITextField *)view;
textField.backgroundColor = Theme.searchFieldBackgroundColor;

View File

@ -16,7 +16,7 @@ public extension UIEdgeInsets {
@objc
public extension UINavigationController {
@objc
public func pushViewController(viewController: UIViewController,
public func pushViewController(_ viewController: UIViewController,
animated: Bool,
completion: (() -> Void)?) {
CATransaction.begin()
@ -35,7 +35,7 @@ public extension UINavigationController {
}
@objc
public func popToViewController(viewController: UIViewController,
public func popToViewController(_ viewController: UIViewController,
animated: Bool,
completion: (() -> Void)?) {
CATransaction.begin()

View File

@ -17,10 +17,10 @@ import Foundation
/// }
/// }
public func BenchAsync(title: String, block: (@escaping () -> Void) -> Void) {
let startTime = CFAbsoluteTimeGetCurrent()
let startTime = CACurrentMediaTime()
block {
let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
let timeElapsed = CACurrentMediaTime() - startTime
let formattedTime = String(format: "%0.2fms", timeElapsed * 1000)
Logger.debug("[Bench] title: \(title), duration: \(formattedTime)")
}
@ -52,19 +52,20 @@ public func Bench(title: String, block: () throws -> Void) throws {
/// crosses multiple classes, you can use the BenchEvent tools
///
/// // in one class
/// let message = getMessage()
/// BenchEventStart(title: "message sending", eventId: message.id)
/// beginTheWork()
///
/// ...
/// ...
///
/// // in another class
/// BenchEventComplete(title: "message sending", eventId: message.id)
/// doTheLastThing()
/// BenchEventComplete(eventId: message.id)
///
/// Or in objc
///
/// [BenchManager startEventWithTitle:"message sending" eventId:message.id]
/// ...
/// [BenchManager startEventWithTitle:"message sending" eventId:message.id]
/// [BenchManager completeEventWithEventId:eventId:message.id]
public func BenchEventStart(title: String, eventId: BenchmarkEventId) {
BenchAsync(title: title) { finish in
runningEvents[eventId] = Event(title: title, eventId: eventId, completion: finish)

View File

@ -162,7 +162,57 @@ public class ComposeScreenSearchResultSet: NSObject {
}
@objc
public class ConversationSearcher: NSObject {
public class MessageSearchResult: NSObject, Comparable {
public let messageId: String
public let sortId: UInt64
init(messageId: String, sortId: UInt64) {
self.messageId = messageId
self.sortId = sortId
}
// MARK: - Comparable
public static func < (lhs: MessageSearchResult, rhs: MessageSearchResult) -> Bool {
return lhs.sortId < rhs.sortId
}
}
@objc
public class ConversationScreenSearchResultSet: NSObject {
@objc
public let searchText: String
@objc
public let messages: [MessageSearchResult]
@objc
public lazy var messageSortIds: [UInt64] = {
return messages.map { $0.sortId }
}()
// MARK: Static members
public static let empty: ConversationScreenSearchResultSet = ConversationScreenSearchResultSet(searchText: "", messages: [])
// MARK: Init
public init(searchText: String, messages: [MessageSearchResult]) {
self.searchText = searchText
self.messages = messages
}
// MARK: - CustomDebugStringConvertible
override public var debugDescription: String {
return "ConversationScreenSearchResultSet(searchText: \(searchText), messages: [\(messages.count) matches])"
}
}
@objc
public class FullTextSearcher: NSObject {
// MARK: - Dependencies
@ -175,7 +225,7 @@ public class ConversationSearcher: NSObject {
private let finder: FullTextSearchFinder
@objc
public static let shared: ConversationSearcher = ConversationSearcher()
public static let shared: FullTextSearcher = FullTextSearcher()
override private init() {
finder = FullTextSearchFinder()
super.init()
@ -279,6 +329,39 @@ public class ConversationSearcher: NSObject {
return HomeScreenSearchResultSet(searchText: searchText, conversations: conversations, contacts: otherContacts, messages: messages)
}
public func searchWithinConversation(thread: TSThread,
searchText: String,
transaction: YapDatabaseReadTransaction) -> ConversationScreenSearchResultSet {
var messages: [MessageSearchResult] = []
guard let threadId = thread.uniqueId else {
owsFailDebug("threadId was unexpectedly nil")
return ConversationScreenSearchResultSet.empty
}
self.finder.enumerateObjects(searchText: searchText, transaction: transaction) { (match: Any, snippet: String?) in
if let message = match as? TSMessage {
guard message.uniqueThreadId == threadId else {
return
}
guard let messageId = message.uniqueId else {
owsFailDebug("messageId was unexpectedly nil")
return
}
let searchResult = MessageSearchResult(messageId: messageId, sortId: message.sortId)
messages.append(searchResult)
}
}
// We want most recent first
messages.sort(by: >)
return ConversationScreenSearchResultSet(searchText: searchText, messages: messages)
}
@objc(filterThreads:withSearchText:)
public func filterThreads(_ threads: [TSThread], searchText: String) -> [TSThread] {
guard searchText.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 else {