From cd440b839f26d16f9b4e266b293f7b24eddb6e8c Mon Sep 17 00:00:00 2001
From: Michael Kirk <michael.code@endoftheworl.de>
Date: Mon, 4 Dec 2017 13:52:16 -0500
Subject: [PATCH] Consolidate search logic

// FREEBIE
---
 Signal.xcodeproj/project.pbxproj              |  8 --
 .../src/ViewControllers/ContactsViewHelper.m  | 35 +-------
 .../NewContactThreadViewController.m          | 27 ++----
 .../SelectThreadViewController.m              | 10 ++-
 Signal/src/ViewControllers/ThreadViewHelper.h |  6 --
 Signal/src/ViewControllers/ThreadViewHelper.m | 74 +---------------
 Signal/src/contact/OWSContactsSearcher.h      | 16 ----
 Signal/src/contact/OWSContactsSearcher.m      | 40 ---------
 Signal/src/util/Searcher.swift                | 86 +++++++++++++++++++
 9 files changed, 102 insertions(+), 200 deletions(-)
 delete mode 100644 Signal/src/contact/OWSContactsSearcher.h
 delete mode 100644 Signal/src/contact/OWSContactsSearcher.m

diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj
index 69bb72832..ef904fa3e 100644
--- a/Signal.xcodeproj/project.pbxproj
+++ b/Signal.xcodeproj/project.pbxproj
@@ -230,8 +230,6 @@
 		4574A5D61DD6704700C6B692 /* CallService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4574A5D51DD6704700C6B692 /* CallService.swift */; };
 		4579431E1E7C8CE9008ED0C0 /* Pastelog.m in Sources */ = {isa = PBXBuildFile; fileRef = 4579431D1E7C8CE9008ED0C0 /* Pastelog.m */; };
 		45794E861E00620000066731 /* CallUIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45794E851E00620000066731 /* CallUIAdapter.swift */; };
-		45843D1F1D2236B30013E85A /* OWSContactsSearcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 45843D1E1D2236B30013E85A /* OWSContactsSearcher.m */; };
-		45843D201D2236B30013E85A /* OWSContactsSearcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 45843D1E1D2236B30013E85A /* OWSContactsSearcher.m */; };
 		45847E871E4283C30080EAB3 /* Intents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 45847E861E4283C30080EAB3 /* Intents.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
 		45855F371D9498A40084F340 /* OWSContactAvatarBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 45855F361D9498A40084F340 /* OWSContactAvatarBuilder.m */; };
 		45855F381D9498A40084F340 /* OWSContactAvatarBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 45855F361D9498A40084F340 /* OWSContactAvatarBuilder.m */; };
@@ -815,8 +813,6 @@
 		4579431C1E7C8CE9008ED0C0 /* Pastelog.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Pastelog.h; sourceTree = "<group>"; };
 		4579431D1E7C8CE9008ED0C0 /* Pastelog.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Pastelog.m; sourceTree = "<group>"; };
 		45794E851E00620000066731 /* CallUIAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CallUIAdapter.swift; path = UserInterface/CallUIAdapter.swift; sourceTree = "<group>"; };
-		45843D1D1D2236B30013E85A /* OWSContactsSearcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSContactsSearcher.h; sourceTree = "<group>"; };
-		45843D1E1D2236B30013E85A /* OWSContactsSearcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSContactsSearcher.m; sourceTree = "<group>"; };
 		45847E861E4283C30080EAB3 /* Intents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Intents.framework; path = System/Library/Frameworks/Intents.framework; sourceTree = SDKROOT; };
 		45855F351D9498A40084F340 /* OWSContactAvatarBuilder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSContactAvatarBuilder.h; sourceTree = "<group>"; };
 		45855F361D9498A40084F340 /* OWSContactAvatarBuilder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSContactAvatarBuilder.m; sourceTree = "<group>"; };
@@ -1642,8 +1638,6 @@
 			children = (
 				76EB040818170B33006006FC /* OWSContactsManager.h */,
 				76EB040918170B33006006FC /* OWSContactsManager.m */,
-				45843D1D1D2236B30013E85A /* OWSContactsSearcher.h */,
-				45843D1E1D2236B30013E85A /* OWSContactsSearcher.m */,
 				4542F0931EB9372700C7EE92 /* SystemContactsFetcher.swift */,
 			);
 			path = contact;
@@ -2770,7 +2764,6 @@
 				D221A09A169C9E5E00537ABF /* main.m in Sources */,
 				345671011E89A5F1006EE662 /* ThreadUtil.m in Sources */,
 				4585C4601ED4FD0400896AEA /* OWS104CreateRecipientIdentities.m in Sources */,
-				45843D1F1D2236B30013E85A /* OWSContactsSearcher.m in Sources */,
 				B6258B331C29E2E60014138E /* NotificationsManager.m in Sources */,
 				34B3F87B1E8DF1700035BE1A /* ExperienceUpgradesPageViewController.swift in Sources */,
 				34533F181EA8D2070006114F /* OWSAudioAttachmentPlayer.m in Sources */,
@@ -2928,7 +2921,6 @@
 				456F6E231E24133500FD2210 /* Platform.swift in Sources */,
 				4539B5871F79348F007141FF /* PushRegistrationManager.swift in Sources */,
 				4504493A1F45EE7D002D1ADA /* NSString+OWS.m in Sources */,
-				45843D201D2236B30013E85A /* OWSContactsSearcher.m in Sources */,
 				45AE48521E0732D6004D96C2 /* TurnServerInfo.swift in Sources */,
 				45360B901F9527DA00FA666C /* SearcherTest.swift in Sources */,
 				B660F7561C29988E00687D6E /* PushManager.m in Sources */,
diff --git a/Signal/src/ViewControllers/ContactsViewHelper.m b/Signal/src/ViewControllers/ContactsViewHelper.m
index f1bfdb0f4..50dafced6 100644
--- a/Signal/src/ViewControllers/ContactsViewHelper.m
+++ b/Signal/src/ViewControllers/ContactsViewHelper.m
@@ -31,7 +31,7 @@ NS_ASSUME_NONNULL_BEGIN
 @property (nonatomic) BOOL shouldNotifyDelegateOfUpdatedContacts;
 @property (nonatomic) BOOL hasUpdatedContactsAtLeastOnce;
 @property (nonatomic) OWSProfileManager *profileManager;
-@property (nonatomic, readonly) AnySearcher *signalAccountSearcher;
+@property (nonatomic, readonly) ConversationSearcher *conversationSearcher;
 
 @end
 
@@ -51,6 +51,7 @@ NS_ASSUME_NONNULL_BEGIN
 
     _blockingManager = [OWSBlockingManager sharedManager];
     _blockedPhoneNumbers = [_blockingManager blockedPhoneNumbers];
+    _conversationSearcher = ConversationSearcher.shared;
 
     _contactsManager = [Environment getCurrent].contactsManager;
     _profileManager = [OWSProfileManager sharedManager];
@@ -60,8 +61,6 @@ NS_ASSUME_NONNULL_BEGIN
     [self updateContacts];
     self.shouldNotifyDelegateOfUpdatedContacts = NO;
 
-    _signalAccountSearcher = [self buildSignalAccountSearcher];
-
     [self observeNotifications];
 
     return self;
@@ -102,24 +101,6 @@ NS_ASSUME_NONNULL_BEGIN
 
 #pragma mark - Contacts
 
-- (AnySearcher *)buildSignalAccountSearcher
-{
-    return [[AnySearcher alloc] initWithIndexer:^NSString *_Nonnull(id _Nonnull obj) {
-        if (![obj isKindOfClass:[SignalAccount class]]) {
-            OWSFail(@"unexpected item in searcher");
-            return @"";
-        }
-
-        SignalAccount *signalAccount = (SignalAccount *)obj;
-
-        NSString *recipientId = signalAccount.recipientId;
-        NSString *contactName = [self.contactsManager displayNameForPhoneIdentifier:recipientId];
-        NSString *profileName = [self.contactsManager profileNameForRecipientId:recipientId];
-
-        return [NSString stringWithFormat:@"%@ %@ %@", recipientId, contactName, profileName];
-    }];
-}
-
 - (nullable SignalAccount *)signalAccountForRecipientId:(NSString *)recipientId
 {
     OWSAssert([NSThread isMainThread]);
@@ -206,17 +187,7 @@ NS_ASSUME_NONNULL_BEGIN
 
 - (NSArray<SignalAccount *> *)signalAccountsMatchingSearchString:(NSString *)searchText
 {
-    NSArray<NSString *> *searchTerms = [self searchTermsForSearchString:searchText];
-
-    if (searchTerms.count < 1) {
-        return self.signalAccounts;
-    }
-
-    return [self.signalAccounts
-        filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(SignalAccount *signalAccount,
-                                        NSDictionary<NSString *, id> *_Nullable bindings) {
-            return [self.signalAccountSearcher item:signalAccount doesMatchQuery:searchText];
-        }]];
+    return [self.conversationSearcher filterSignalAccounts:self.signalAccounts withSearchText:searchText];
 }
 
 - (BOOL)doesContact:(Contact *)contact matchSearchTerm:(NSString *)searchTerm
diff --git a/Signal/src/ViewControllers/NewContactThreadViewController.m b/Signal/src/ViewControllers/NewContactThreadViewController.m
index 9b65ec0a6..b02aa5977 100644
--- a/Signal/src/ViewControllers/NewContactThreadViewController.m
+++ b/Signal/src/ViewControllers/NewContactThreadViewController.m
@@ -8,7 +8,6 @@
 #import "Environment.h"
 #import "NewGroupViewController.h"
 #import "NewNonContactConversationViewController.h"
-#import "OWSContactsSearcher.h"
 #import "OWSTableViewController.h"
 #import "Signal-Swift.h"
 #import "UIColor+OWS.h"
@@ -45,6 +44,7 @@ NS_ASSUME_NONNULL_BEGIN
     MFMessageComposeViewControllerDelegate>
 
 @property (nonatomic, readonly) ContactsViewHelper *contactsViewHelper;
+@property (nonatomic, readonly) ConversationSearcher *conversationSearcher;
 
 @property (nonatomic, readonly) UIView *noSignalContactsView;
 
@@ -77,6 +77,7 @@ NS_ASSUME_NONNULL_BEGIN
 
     self.view.backgroundColor = UIColor.whiteColor;
     _contactsViewHelper = [[ContactsViewHelper alloc] initWithDelegate:self];
+    _conversationSearcher = [ConversationSearcher shared];
     _nonContactAccountSet = [NSMutableSet set];
     _collation = [UILocalizedIndexedCollation currentCollation];
 
@@ -630,35 +631,17 @@ NS_ASSUME_NONNULL_BEGIN
 
 - (NSArray<TSGroupThread *> *)filteredGroupThreads
 {
-    AnySearcher *searcher = [[AnySearcher alloc] initWithIndexer:^NSString * _Nonnull(id _Nonnull obj) {
-        if (![obj isKindOfClass:[TSGroupThread class]]) {
-            OWSFail(@"unexpected item in searcher");
-            return @"";
-        }
-        TSGroupThread *groupThread = (TSGroupThread *)obj;
-        NSString *groupName = groupThread.groupModel.groupName;
-        NSMutableString *groupMemberNames = [NSMutableString new];
-        for (NSString *recipientId in groupThread.groupModel.groupMemberIds) {
-            NSString *contactName = [self.contactsViewHelper.contactsManager displayNameForPhoneIdentifier:recipientId];
-            [groupMemberNames appendFormat:@" %@", contactName];
-        }
-        
-        return [NSString stringWithFormat:@"%@ %@", groupName, groupMemberNames];
-    }];
-    
-    NSMutableArray<TSGroupThread *> *matchingThreads = [NSMutableArray new];
+    NSMutableArray<TSGroupThread *> *groupThreads = [NSMutableArray new];
     [TSGroupThread enumerateCollectionObjectsUsingBlock:^(id obj, BOOL *stop) {
         if (![obj isKindOfClass:[TSGroupThread class]]) {
             // group and contact threads are in the same collection.
             return;
         }
         TSGroupThread *groupThread = (TSGroupThread *)obj;
-        if ([searcher item:groupThread doesMatchQuery:self.searchBar.text]) {
-            [matchingThreads addObject:groupThread];
-        }
+        [groupThreads addObject:groupThread];
     }];
 
-    return [matchingThreads copy];
+    return [self.conversationSearcher filterGroupThreads:groupThreads withSearchText:self.searchBar.text];
 }
 
 #pragma mark - No Contacts Mode
diff --git a/Signal/src/ViewControllers/SelectThreadViewController.m b/Signal/src/ViewControllers/SelectThreadViewController.m
index c39e374f9..dfbc5a158 100644
--- a/Signal/src/ViewControllers/SelectThreadViewController.m
+++ b/Signal/src/ViewControllers/SelectThreadViewController.m
@@ -9,8 +9,8 @@
 #import "Environment.h"
 #import "NSString+OWS.h"
 #import "OWSContactsManager.h"
-#import "OWSContactsSearcher.h"
 #import "OWSTableViewController.h"
+#import "Signal-Swift.h"
 #import "ThreadViewHelper.h"
 #import "UIColor+OWS.h"
 #import "UIFont+OWS.h"
@@ -29,7 +29,7 @@ NS_ASSUME_NONNULL_BEGIN
     UISearchBarDelegate>
 
 @property (nonatomic, readonly) ContactsViewHelper *contactsViewHelper;
-
+@property (nonatomic, readonly) ConversationSearcher *conversationSearcher;
 @property (nonatomic, readonly) ThreadViewHelper *threadViewHelper;
 
 @property (nonatomic, readonly) OWSTableViewController *tableViewController;
@@ -54,6 +54,7 @@ NS_ASSUME_NONNULL_BEGIN
     self.view.backgroundColor = [UIColor whiteColor];
 
     _contactsViewHelper = [[ContactsViewHelper alloc] initWithDelegate:self];
+    _conversationSearcher = ConversationSearcher.shared;
     _threadViewHelper = [ThreadViewHelper new];
     _threadViewHelper.delegate = self;
 
@@ -132,7 +133,7 @@ NS_ASSUME_NONNULL_BEGIN
     ContactsViewHelper *helper = self.contactsViewHelper;
     OWSTableContents *contents = [OWSTableContents new];
 
-    // Threads are listed, most recent first.
+    // Existing threads are listed first, ordered by most recently active
     OWSTableSection *recentChatsSection = [OWSTableSection new];
     recentChatsSection.headerTitle = NSLocalizedString(
         @"SELECT_THREAD_TABLE_RECENT_CHATS_TITLE", @"Table section header for recently active conversations");
@@ -237,7 +238,8 @@ NS_ASSUME_NONNULL_BEGIN
 - (NSArray<TSThread *> *)filteredThreadsWithSearchText
 {
     NSString *searchTerm = [[self.searchBar text] ows_stripped];
-    return [self.threadViewHelper threadsMatchingSearchString:searchTerm];
+
+    return [self.conversationSearcher filterThreads:self.threadViewHelper.threads withSearchText:searchTerm];
 }
 
 - (NSArray<SignalAccount *> *)filteredSignalAccountsWithSearchText
diff --git a/Signal/src/ViewControllers/ThreadViewHelper.h b/Signal/src/ViewControllers/ThreadViewHelper.h
index 917ccf3de..6b73af8c4 100644
--- a/Signal/src/ViewControllers/ThreadViewHelper.h
+++ b/Signal/src/ViewControllers/ThreadViewHelper.h
@@ -4,8 +4,6 @@
 
 NS_ASSUME_NONNULL_BEGIN
 
-@class AnySearcher;
-
 @protocol ThreadViewHelperDelegate <NSObject>
 
 - (void)threadListDidChange;
@@ -25,10 +23,6 @@ NS_ASSUME_NONNULL_BEGIN
 
 @property (nonatomic, weak) id<ThreadViewHelperDelegate> delegate;
 @property (nonatomic, readonly) NSMutableArray<TSThread *> *threads;
-@property (nonatomic, readonly) AnySearcher *groupThreadSearcher;
-@property (nonatomic, readonly) AnySearcher *contactThreadSearcher;
-
-- (NSArray<TSThread *> *)threadsMatchingSearchString:(NSString *)searchString;
 
 @end
 
diff --git a/Signal/src/ViewControllers/ThreadViewHelper.m b/Signal/src/ViewControllers/ThreadViewHelper.m
index 9bae7c91f..3df98fdfe 100644
--- a/Signal/src/ViewControllers/ThreadViewHelper.m
+++ b/Signal/src/ViewControllers/ThreadViewHelper.m
@@ -17,6 +17,7 @@ NS_ASSUME_NONNULL_BEGIN
 
 @property (nonatomic) YapDatabaseConnection *uiDatabaseConnection;
 @property (nonatomic) YapDatabaseViewMappings *threadMappings;
+@property (nonatomic) ConversationSearcher *conversationSearcher;
 
 @end
 
@@ -30,8 +31,7 @@ NS_ASSUME_NONNULL_BEGIN
     }
 
     [self initializeMapping];
-    _groupThreadSearcher = [self buildGroupThreadSearcher];
-    _contactThreadSearcher = [self buildContactThreadSearcher];
+    _conversationSearcher = ConversationSearcher.shared;
 
     return self;
 }
@@ -126,76 +126,6 @@ NS_ASSUME_NONNULL_BEGIN
     _threads = [threads copy];
 }
 
-#pragma mark - Searching
-
-- (OWSContactsManager *)contactsManager
-{
-    return [Environment getCurrent].contactsManager;
-}
-
-- (NSString *)searchIndexStringForRecipientId:(NSString *)recipientId
-{
-    NSString *contactName = [self.contactsManager displayNameForPhoneIdentifier:recipientId];
-    NSString *profileName = [self.contactsManager profileNameForRecipientId:recipientId];
-
-    return [NSString stringWithFormat:@"%@ %@ %@", recipientId, contactName, profileName];
-}
-
-- (AnySearcher *)buildContactThreadSearcher
-{
-    AnySearcher *searcher = [[AnySearcher alloc] initWithIndexer:^NSString *_Nonnull(id _Nonnull obj) {
-        if (![obj isKindOfClass:[TSContactThread class]]) {
-            OWSFail(@"unexpected item in searcher");
-            return @"";
-        }
-        TSContactThread *contactThread = (TSContactThread *)obj;
-
-        NSString *recipientId = contactThread.contactIdentifier;
-        return [self searchIndexStringForRecipientId:recipientId];
-    }];
-
-    return searcher;
-}
-
-- (AnySearcher *)buildGroupThreadSearcher
-{
-    AnySearcher *searcher = [[AnySearcher alloc] initWithIndexer:^NSString *_Nonnull(id _Nonnull obj) {
-        if (![obj isKindOfClass:[TSGroupThread class]]) {
-            OWSFail(@"unexpected item in searcher");
-            return @"";
-        }
-        TSGroupThread *groupThread = (TSGroupThread *)obj;
-        NSString *groupName = groupThread.groupModel.groupName;
-        NSMutableString *groupMemberStrings = [NSMutableString new];
-        for (NSString *recipientId in groupThread.groupModel.groupMemberIds) {
-            NSString *recipientString = [self searchIndexStringForRecipientId:recipientId];
-            [groupMemberStrings appendFormat:@" %@", recipientString];
-        }
-
-        return [NSString stringWithFormat:@"%@ %@", groupName, groupMemberStrings];
-    }];
-
-    return searcher;
-}
-
-- (NSArray<TSThread *> *)threadsMatchingSearchString:(NSString *)searchString
-{
-    if (searchString.length == 0) {
-        return self.threads;
-    }
-
-    NSMutableArray *result = [NSMutableArray new];
-    for (TSThread *thread in self.threads) {
-        AnySearcher *searcher =
-            [thread isKindOfClass:[TSContactThread class]] ? self.contactThreadSearcher : self.groupThreadSearcher;
-        if ([searcher item:thread doesMatchQuery:searchString]) {
-            [result addObject:thread];
-        }
-    }
-    return result;
-}
-
-
 @end
 
 NS_ASSUME_NONNULL_END
diff --git a/Signal/src/contact/OWSContactsSearcher.h b/Signal/src/contact/OWSContactsSearcher.h
deleted file mode 100644
index 09f96e0d2..000000000
--- a/Signal/src/contact/OWSContactsSearcher.h
+++ /dev/null
@@ -1,16 +0,0 @@
-//
-//  OWSContactsSearcher.h
-//  Signal
-//
-//  Created by Michael Kirk on 6/27/16.
-//  Copyright © 2016 Open Whisper Systems. All rights reserved.
-//
-
-#import "Contact.h"
-
-@interface OWSContactsSearcher : NSObject
-
-- (instancetype)initWithContacts:(NSArray<Contact *> *)contacts;
-- (NSArray<Contact *> *)filterWithString:(NSString *)string;
-
-@end
diff --git a/Signal/src/contact/OWSContactsSearcher.m b/Signal/src/contact/OWSContactsSearcher.m
deleted file mode 100644
index 774f1038c..000000000
--- a/Signal/src/contact/OWSContactsSearcher.m
+++ /dev/null
@@ -1,40 +0,0 @@
-//
-//  Copyright (c) 2017 Open Whisper Systems. All rights reserved.
-//
-
-#import "OWSContactsSearcher.h"
-#import "NSString+OWS.h"
-#import <SignalServiceKit/PhoneNumber.h>
-
-@interface OWSContactsSearcher ()
-
-@property (copy) NSArray<Contact *> *contacts;
-
-@end
-
-@implementation OWSContactsSearcher
-
-- (instancetype)initWithContacts:(NSArray<Contact *> *)contacts {
-    self = [super init];
-    if (!self) return self;
-
-    _contacts = contacts;
-    return self;
-}
-
-- (NSArray<Contact *> *)filterWithString:(NSString *)string {
-    NSString *searchTerm = [string ows_stripped];
-
-    if ([searchTerm isEqualToString:@""]) {
-        return self.contacts;
-    }
-
-    NSString *formattedNumber = [PhoneNumber removeFormattingCharacters:searchTerm];
-
-    // TODO: This assumes there's a single search term.
-    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"(fullName contains[c] %@) OR (ANY parsedPhoneNumbers.toE164 contains[c] %@)", searchTerm, formattedNumber];
-
-    return [self.contacts filteredArrayUsingPredicate:predicate];
-}
-
-@end
diff --git a/Signal/src/util/Searcher.swift b/Signal/src/util/Searcher.swift
index aab5fa7fb..7adf75fc7 100644
--- a/Signal/src/util/Searcher.swift
+++ b/Signal/src/util/Searcher.swift
@@ -3,6 +3,91 @@
 //
 
 import Foundation
+import SignalServiceKit
+
+@objc
+class ConversationSearcher: NSObject {
+
+    @objc
+    public static let shared: ConversationSearcher = ConversationSearcher()
+    override private init() {
+        super.init()
+    }
+
+    @objc(filterThreads:withSearchText:)
+    public func filterThreads(_ threads: [TSThread], searchText: String) -> [TSThread] {
+        guard searchText.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 else {
+            return threads
+        }
+
+        return threads.filter { thread in
+            switch thread {
+            case let groupThread as TSGroupThread:
+                return self.groupThreadSearcher.matches(item: groupThread, query: searchText)
+            case let contactThread as TSContactThread:
+                return self.contactThreadSearcher.matches(item: contactThread, query: searchText)
+            default:
+                owsFail("Unexpected thread type: \(thread)")
+                return false
+            }
+        }
+    }
+
+    @objc(filterGroupThreads:withSearchText:)
+    public func filterGroupThreads(_ groupThreads: [TSGroupThread], searchText: String) -> [TSGroupThread] {
+        guard searchText.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 else {
+            return groupThreads
+        }
+
+        return groupThreads.filter { groupThread in
+            return self.groupThreadSearcher.matches(item: groupThread, query: searchText)
+        }
+    }
+
+    @objc(filterSignalAccounts:withSearchText:)
+    public func filterSignalAccounts(_ signalAccounts: [SignalAccount], searchText: String) -> [SignalAccount] {
+        guard searchText.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 else {
+            return signalAccounts
+        }
+
+        return signalAccounts.filter { signalAccount in
+            self.signalAccountSearcher.matches(item: signalAccount, query: searchText)
+        }
+    }
+
+    // MARK: - Helpers
+
+    // MARK: Searchers
+    private lazy var groupThreadSearcher: Searcher<TSGroupThread> = Searcher { (groupThread: TSGroupThread) in
+        let groupName = groupThread.groupModel.groupName
+        let memberStrings = groupThread.groupModel.groupMemberIds.map { recipientId in
+            self.indexingString(recipientId: recipientId)
+            }.joined(separator: " ")
+
+        return "\(memberStrings) \(groupName ?? "")"
+    }
+
+    private lazy var contactThreadSearcher: Searcher<TSContactThread> = Searcher { (contactThread: TSContactThread) in
+        let recipientId = contactThread.contactIdentifier()
+        return self.indexingString(recipientId: recipientId)
+    }
+
+    private lazy var signalAccountSearcher: Searcher<SignalAccount> = Searcher { (signalAccount: SignalAccount) in
+        let recipientId = signalAccount.recipientId
+        return self.indexingString(recipientId: recipientId)
+    }
+
+    private var contactsManager: OWSContactsManager {
+        return Environment.getCurrent().contactsManager
+    }
+
+    private func indexingString(recipientId: String) -> String {
+        let contactName = contactsManager.displayName(forPhoneIdentifier: recipientId)
+        let profileName = contactsManager.profileName(forRecipientId: recipientId)
+
+        return "\(recipientId) \(contactName) \(profileName ?? "")"
+    }
+}
 
 // ObjC compatible searcher
 @objc class AnySearcher: NSObject {
@@ -19,6 +104,7 @@ import Foundation
     }
 }
 
+// A generic searching class, configurable with an indexing block
 class Searcher<T> {
 
     private let indexer: (T) -> String