//
// 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
}
// MARK: V i e w L i f e c y l e
override func viewDidLoad ( ) {
super . viewDidLoad ( )
tableView . rowHeight = UITableViewAutomaticDimension
tableView . estimatedRowHeight = 60
tableView . register ( EmptySearchResultCell . self , forCellReuseIdentifier : EmptySearchResultCell . reuseIdentifier )
tableView . register ( ConversationSearchResultCell . self , forCellReuseIdentifier : ConversationSearchResultCell . reuseIdentifier )
tableView . register ( MessageSearchResultCell . self , forCellReuseIdentifier : MessageSearchResultCell . reuseIdentifier )
tableView . register ( ContactSearchResultCell . self , forCellReuseIdentifier : ContactSearchResultCell . 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 : ConversationSearchResultCell . reuseIdentifier ) as ? ConversationSearchResultCell 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 ( searchResult : searchResult )
return cell
case . contacts :
guard let cell = tableView . dequeueReusableCell ( withIdentifier : ContactSearchResultCell . reuseIdentifier ) as ? ContactSearchResultCell 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 ( searchResult : searchResult )
return cell
case . messages :
guard let cell = tableView . dequeueReusableCell ( withIdentifier : MessageSearchResultCell . reuseIdentifier ) as ? MessageSearchResultCell 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 ( )
}
cell . configure ( searchResult : searchResult )
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 ConversationSearchResultCell : UITableViewCell {
static let reuseIdentifier = " ConversationSearchResultCell "
let nameLabel : UILabel
let snippetLabel : UILabel
let avatarView : AvatarImageView
let avatarWidth : UInt = 40
override init ( style : UITableViewCellStyle , reuseIdentifier : String ? ) {
self . nameLabel = UILabel ( )
self . snippetLabel = UILabel ( )
self . avatarView = AvatarImageView ( )
avatarView . autoSetDimensions ( to : CGSize ( width : CGFloat ( avatarWidth ) , height : CGFloat ( avatarWidth ) ) )
super . init ( style : style , reuseIdentifier : reuseIdentifier )
nameLabel . font = UIFont . ows_dynamicTypeBody . ows_mediumWeight ( )
snippetLabel . font = UIFont . ows_dynamicTypeFootnote
let textRows = UIStackView ( arrangedSubviews : [ nameLabel , snippetLabel ] )
textRows . axis = . vertical
let columns = UIStackView ( arrangedSubviews : [ avatarView , textRows ] )
columns . axis = . horizontal
columns . spacing = 8
contentView . addSubview ( columns )
columns . autoPinEdgesToSuperviewMargins ( )
}
required init ? ( coder aDecoder : NSCoder ) {
fatalError ( " init(coder:) has not been implemented " )
}
var contactsManager : OWSContactsManager {
return Environment . current ( ) . contactsManager
}
func configure ( searchResult : ConversationSearchResult ) {
self . avatarView . image = OWSAvatarBuilder . buildImage ( thread : searchResult . thread . threadRecord , diameter : avatarWidth , contactsManager : self . contactsManager )
self . nameLabel . text = searchResult . thread . name
self . snippetLabel . text = searchResult . snippet
}
}
class MessageSearchResultCell : UITableViewCell {
static let reuseIdentifier = " MessageSearchResultCell "
let nameLabel : UILabel
let snippetLabel : UILabel
override init ( style : UITableViewCellStyle , reuseIdentifier : String ? ) {
self . nameLabel = UILabel ( )
self . snippetLabel = UILabel ( )
super . init ( style : style , reuseIdentifier : reuseIdentifier )
nameLabel . font = UIFont . ows_dynamicTypeBody . ows_mediumWeight ( )
snippetLabel . font = UIFont . ows_dynamicTypeFootnote
let textRows = UIStackView ( arrangedSubviews : [ nameLabel , snippetLabel ] )
textRows . axis = . vertical
contentView . addSubview ( textRows )
textRows . autoPinEdgesToSuperviewMargins ( )
}
required init ? ( coder aDecoder : NSCoder ) {
fatalError ( " init(coder:) has not been implemented " )
}
func configure ( searchResult : ConversationSearchResult ) {
self . nameLabel . text = searchResult . thread . name
guard let snippet = searchResult . snippet else {
self . snippetLabel . text = nil
return
}
guard let encodedString = snippet . data ( using : . utf8 ) else {
self . snippetLabel . text = nil
return
}
// B o l d s n i p p e t t e x t
do {
// F I X M E - T h e s n i p p e t m a r k s u p t h e m a t c h e d s e a r c h t e x t w i t h < b > t a g s .
// W e c a n p a r s e t h i s i n t o a n a t t r i b u t e d s t r i n g , b u t i t a l s o t a k e s o n a n u n d e s i r a b l e f o n t .
// W e w a n t t o a p p l y o u r o w n f o n t w i t h o u t c l o b b e r i n g b o l d i n t h e p r o c e s s - m a y b e b y e n u m e r a t i n g a n d i n s p e c t i n g t h e a t t r i b u t e s ? O r m a y b e w e c a n p a s s i n a b a s e f o n t ?
let attributedSnippet = try NSMutableAttributedString ( data : encodedString ,
options : [ NSAttributedString . DocumentReadingOptionKey . documentType : NSAttributedString . DocumentType . html ] ,
documentAttributes : nil )
attributedSnippet . addAttribute ( NSAttributedStringKey . font , value : self . snippetLabel . font , range : NSRange ( location : 0 , length : attributedSnippet . length ) )
self . snippetLabel . attributedText = attributedSnippet
} catch {
owsFail ( " failed to generate snippet: \( error ) " )
}
}
}
class ContactSearchResultCell : UITableViewCell {
static let reuseIdentifier = " ContactSearchResultCell "
let nameLabel : UILabel
let snippetLabel : UILabel
let avatarView : AvatarImageView
let avatarWidth : UInt = 40
override init ( style : UITableViewCellStyle , reuseIdentifier : String ? ) {
self . nameLabel = UILabel ( )
self . snippetLabel = UILabel ( )
self . avatarView = AvatarImageView ( )
avatarView . autoSetDimensions ( to : CGSize ( width : CGFloat ( avatarWidth ) , height : CGFloat ( avatarWidth ) ) )
super . init ( style : style , reuseIdentifier : reuseIdentifier )
nameLabel . font = UIFont . ows_dynamicTypeBody . ows_mediumWeight ( )
snippetLabel . font = UIFont . ows_dynamicTypeFootnote
let textRows = UIStackView ( arrangedSubviews : [ nameLabel , snippetLabel ] )
textRows . axis = . vertical
let columns = UIStackView ( arrangedSubviews : [ avatarView , textRows ] )
columns . axis = . horizontal
columns . spacing = 8
contentView . addSubview ( columns )
columns . autoPinEdgesToSuperviewMargins ( )
}
required init ? ( coder aDecoder : NSCoder ) {
fatalError ( " init(coder:) has not been implemented " )
}
var contactsManager : OWSContactsManager {
return Environment . current ( ) . contactsManager
}
func configure ( searchResult : ContactSearchResult ) {
let avatarBuilder = OWSContactAvatarBuilder . init ( signalId : searchResult . recipientId , diameter : avatarWidth , contactsManager : contactsManager )
self . avatarView . image = avatarBuilder . build ( )
self . nameLabel . text = self . contactsManager . displayName ( forPhoneIdentifier : searchResult . recipientId )
self . snippetLabel . text = searchResult . recipientId
}
}
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
}
}