//
// 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
}
enum SearchSection : Int {
case conversations = 0
case contacts = 1
case messages = 2
}
// 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 ( ConversationSearchResultCell . self , forCellReuseIdentifier : ConversationSearchResultCell . reuseIdentifier )
tableView . register ( MessageSearchResultCell . self , forCellReuseIdentifier : MessageSearchResultCell . reuseIdentifier )
}
// 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 . 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 . conversations :
guard let cell = tableView . dequeueReusableCell ( withIdentifier : ConversationSearchResultCell . reuseIdentifier ) as ? ConversationSearchResultCell else {
return UITableViewCell ( )
}
guard let searchResult = self . searchResultSet . conversations [ safe : indexPath . row ] else {
return UITableViewCell ( )
}
cell . configure ( searchResult : searchResult )
return cell
case . contacts :
// T O D O
return UITableViewCell ( )
case . messages :
guard let cell = tableView . dequeueReusableCell ( withIdentifier : MessageSearchResultCell . reuseIdentifier ) as ? MessageSearchResultCell else {
return UITableViewCell ( )
}
guard let searchResult = self . searchResultSet . messages [ safe : indexPath . row ] else {
return UITableViewCell ( )
}
cell . configure ( searchResult : searchResult )
return cell
}
}
override func numberOfSections ( in tableView : UITableView ) -> Int {
return 3
}
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 . 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
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 )
}
// TODO: m o r e p e r f o 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 : SearchResult ) {
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 : SearchResult ) {
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 ) " )
}
}
}