//
// C o p y r i g h t ( c ) 2 0 1 8 O p e n W h i s p e r S y s t e m s . A l l r i g h t s r e s e r v e d .
//
import Foundation
@objc
class ConversationSearchViewController : UITableViewController {
var searchResultSet : SearchResultSet = SearchResultSet . empty
var uiDatabaseConnection : YapDatabaseConnection {
// T O D O d o w e w a n t t o r e s p o n d t o Y a p D B M o d i f i e d ? M i g h t b e h a r d w h e n t h e r e ' s l o t s o f s e a r c h r e s u l t s , f o r o n l y m a r g i n a l v a l u e
return OWSPrimaryStorage . shared ( ) . uiDatabaseConnection
}
var searcher : ConversationSearcher {
return ConversationSearcher . shared
}
private var contactsManager : OWSContactsManager {
return Environment . current ( ) . contactsManager
}
enum SearchSection : Int {
case noResults
case conversations
case contacts
case messages
}
var contactsManager : OWSContactsManager {
return Environment . current ( ) . contactsManager
}
var blockedPhoneNumberSet = Set < String > ( )
// MARK: V i e w L i f e c y l e
override public func loadView ( ) {
super . loadView ( )
let blockingManager = OWSBlockingManager . shared ( )
blockedPhoneNumberSet = Set ( blockingManager . blockedPhoneNumbers ( ) )
}
override func viewDidLoad ( ) {
super . viewDidLoad ( )
tableView . rowHeight = UITableViewAutomaticDimension
tableView . estimatedRowHeight = 60
tableView . register ( EmptySearchResultCell . self , forCellReuseIdentifier : EmptySearchResultCell . reuseIdentifier )
tableView . register ( HomeViewCell . self , forCellReuseIdentifier : HomeViewCell . cellReuseIdentifier ( ) )
tableView . register ( ContactTableViewCell . self , forCellReuseIdentifier : ContactTableViewCell . reuseIdentifier ( ) )
}
// MARK: U I T a b l e V i e w D e l e g a t e
override func tableView ( _ tableView : UITableView , didSelectRowAt indexPath : IndexPath ) {
tableView . deselectRow ( at : indexPath , animated : false )
guard let searchSection = SearchSection ( rawValue : indexPath . section ) else {
owsFail ( " \( logTag ) unknown section selected. " )
return
}
switch searchSection {
case . noResults :
owsFail ( " \( logTag ) shouldn't be able to tap 'no results' section " )
case . conversations :
let sectionResults = searchResultSet . conversations
guard let searchResult = sectionResults [ safe : indexPath . row ] else {
owsFail ( " \( logTag ) unknown row selected. " )
return
}
let thread = searchResult . thread
SignalApp . shared ( ) . presentConversation ( for : thread . threadRecord , action : . compose )
case . contacts :
let sectionResults = searchResultSet . contacts
guard let searchResult = sectionResults [ safe : indexPath . row ] else {
owsFail ( " \( logTag ) unknown row selected. " )
return
}
SignalApp . shared ( ) . presentConversation ( forRecipientId : searchResult . recipientId , action : . compose )
case . messages :
let sectionResults = searchResultSet . messages
guard let searchResult = sectionResults [ safe : indexPath . row ] else {
owsFail ( " \( logTag ) unknown row selected. " )
return
}
let thread = searchResult . thread
SignalApp . shared ( ) . presentConversation ( for : thread . threadRecord ,
action : . compose ,
focusMessageId : searchResult . messageId )
}
}
// MARK: U I T a b l e V i e w D a t a S o u r c e
override func tableView ( _ tableView : UITableView , numberOfRowsInSection section : Int ) -> Int {
guard let searchSection = SearchSection ( rawValue : section ) else {
owsFail ( " unknown section: \( section ) " )
return 0
}
switch searchSection {
case . noResults :
return searchResultSet . isEmpty ? 1 : 0
case . conversations :
return searchResultSet . conversations . count
case . contacts :
return searchResultSet . contacts . count
case . messages :
return searchResultSet . messages . count
}
}
override func tableView ( _ tableView : UITableView , cellForRowAt indexPath : IndexPath ) -> UITableViewCell {
guard let searchSection = SearchSection ( rawValue : indexPath . section ) else {
return UITableViewCell ( )
}
switch searchSection {
case . noResults :
guard let cell = tableView . dequeueReusableCell ( withIdentifier : EmptySearchResultCell . reuseIdentifier ) as ? EmptySearchResultCell else {
owsFail ( " cell was unexpectedly nil " )
return UITableViewCell ( )
}
guard indexPath . row = = 0 else {
owsFail ( " searchResult was unexpected index " )
return UITableViewCell ( )
}
let searchText = self . searchResultSet . searchText
cell . configure ( searchText : searchText )
return cell
case . conversations :
guard let cell = tableView . dequeueReusableCell ( withIdentifier : HomeViewCell . cellReuseIdentifier ( ) ) as ? HomeViewCell else {
owsFail ( " cell was unexpectedly nil " )
return UITableViewCell ( )
}
guard let searchResult = self . searchResultSet . conversations [ safe : indexPath . row ] else {
owsFail ( " searchResult was unexpectedly nil " )
return UITableViewCell ( )
}
cell . configure ( withThread : searchResult . thread , contactsManager : contactsManager , blockedPhoneNumber : self . blockedPhoneNumberSet )
return cell
case . contacts :
guard let cell = tableView . dequeueReusableCell ( withIdentifier : ContactTableViewCell . reuseIdentifier ( ) ) as ? ContactTableViewCell else {
owsFail ( " cell was unexpectedly nil " )
return UITableViewCell ( )
}
guard let searchResult = self . searchResultSet . contacts [ safe : indexPath . row ] else {
owsFail ( " searchResult was unexpectedly nil " )
return UITableViewCell ( )
}
cell . configure ( with : searchResult . signalAccount , contactsManager : contactsManager )
return cell
case . messages :
guard let cell = tableView . dequeueReusableCell ( withIdentifier : HomeViewCell . cellReuseIdentifier ( ) ) as ? HomeViewCell else {
owsFail ( " cell was unexpectedly nil " )
return UITableViewCell ( )
}
guard let searchResult = self . searchResultSet . messages [ safe : indexPath . row ] else {
owsFail ( " searchResult was unexpectedly nil " )
return UITableViewCell ( )
}
var overrideSnippet = NSAttributedString ( )
var overrideTimestamp : NSNumber ?
self . uiDatabaseConnection . read { transaction in
guard let messageId = searchResult . messageId else {
owsFail ( " \( ConversationSearchViewController . logTag ) message search result is missing message id " )
return
}
guard let message = TSInteraction . fetch ( uniqueId : messageId , transaction : transaction ) else {
// T h i s c o u l d h a p p e n i f t h e m e s s a g e h a s d i s a p p e a r e d , e t c .
Logger . error ( " \( ConversationSearchViewController . logTag ) could not load message " )
return
}
overrideTimestamp = NSNumber ( value : message . timestamp )
guard let rawSnippet = searchResult . snippet else {
owsFail ( " \( ConversationSearchViewController . logTag ) message search result is missing snippet " )
return
}
// Y D B u s e s b o l d t a g s t o h i g h l i g h t m a t c h e s w i t h i n t h e s n i p p e t .
let filteredSnippet = rawSnippet
. replacingOccurrences ( of : " <b> " , with : " " )
. replacingOccurrences ( of : " </b> " , with : " " )
. replacingOccurrences ( of : " <B> " , with : " " )
. replacingOccurrences ( of : " </B> " , with : " " )
overrideSnippet = NSAttributedString ( string : filteredSnippet )
}
cell . configure ( withThread : searchResult . thread ,
contactsManager : contactsManager ,
blockedPhoneNumber : self . blockedPhoneNumberSet ,
overrideSnippet : overrideSnippet ,
overrideTimestamp : overrideTimestamp )
return cell
}
}
override func numberOfSections ( in tableView : UITableView ) -> Int {
return 4
}
override func tableView ( _ tableView : UITableView , titleForHeaderInSection section : Int ) -> String ? {
guard let searchSection = SearchSection ( rawValue : section ) else {
owsFail ( " unknown section: \( section ) " )
return nil
}
switch searchSection {
case . noResults :
return nil
case . conversations :
if searchResultSet . conversations . count > 0 {
return NSLocalizedString ( " SEARCH_SECTION_CONVERSATIONS " , comment : " section header for search results that match existing conversations (either group or contact conversations) " )
} else {
return nil
}
case . contacts :
if searchResultSet . contacts . count > 0 {
return NSLocalizedString ( " SEARCH_SECTION_CONTACTS " , comment : " section header for search results that match a contact who doesn't have an existing conversation " )
} else {
return nil
}
case . messages :
if searchResultSet . messages . count > 0 {
return NSLocalizedString ( " SEARCH_SECTION_MESSAGES " , comment : " section header for search results that match a message in a conversation " )
} else {
return nil
}
}
}
// MARK: U I S e a r c h B a r D e l e g a t e
@objc
public func updateSearchResults ( searchText : String ) {
guard searchText . stripped . count > 0 else {
self . searchResultSet = SearchResultSet . empty
self . tableView . reloadData ( )
return
}
// TODO: a s y n c ?
// TODO: d e b o u n c e ?
self . uiDatabaseConnection . read { transaction in
self . searchResultSet = self . searcher . results ( searchText : searchText , transaction : transaction , contactsManager : self . contactsManager )
}
// TODO: m o r e p e r f o r m a n t w a y t o d o t h i s ?
self . tableView . reloadData ( )
}
}
class EmptySearchResultCell : UITableViewCell {
static let reuseIdentifier = " EmptySearchResultCell "
let messageLabel : UILabel
override init ( style : UITableViewCellStyle , reuseIdentifier : String ? ) {
self . messageLabel = UILabel ( )
super . init ( style : style , reuseIdentifier : reuseIdentifier )
messageLabel . font = UIFont . ows_dynamicTypeBody
messageLabel . textAlignment = . center
messageLabel . numberOfLines = 3
contentView . addSubview ( messageLabel )
messageLabel . autoSetDimension ( . height , toSize : 150 )
messageLabel . autoPinEdge ( toSuperviewMargin : . top , relation : . greaterThanOrEqual )
messageLabel . autoPinEdge ( toSuperviewMargin : . leading , relation : . greaterThanOrEqual )
messageLabel . autoPinEdge ( toSuperviewMargin : . bottom , relation : . greaterThanOrEqual )
messageLabel . autoPinEdge ( toSuperviewMargin : . trailing , relation : . greaterThanOrEqual )
messageLabel . autoVCenterInSuperview ( )
messageLabel . autoHCenterInSuperview ( )
messageLabel . setContentHuggingHigh ( )
messageLabel . setCompressionResistanceHigh ( )
}
required init ? ( coder aDecoder : NSCoder ) {
fatalError ( " init(coder:) has not been implemented " )
}
public func configure ( searchText : String ) {
let format = NSLocalizedString ( " HOME_VIEW_SEARCH_NO_RESULTS_FORMAT " , comment : " Format string when search returns no results. Embeds {{search term}} " )
let messageText : String = NSString ( format : format as NSString , searchText ) as String
self . messageLabel . text = messageText
}
}