2018-06-08 03:51:42 +02:00
//
// 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
2018-06-21 15:59:01 +02:00
@objc
protocol ConversationSearchViewDelegate : class {
func conversationSearchViewWillBeginDragging ( )
}
2018-06-08 03:51:42 +02:00
@objc
2018-06-09 12:52:03 +02:00
class ConversationSearchViewController : UITableViewController {
2018-06-08 03:51:42 +02:00
2018-06-21 15:59:01 +02:00
@objc
public weak var delegate : ConversationSearchViewDelegate ?
2018-06-13 17:37:01 +02:00
@objc
public var searchText = " " {
didSet {
2018-08-22 19:44:22 +02:00
AssertIsOnMainThread ( )
2018-06-13 17:37:01 +02:00
2018-06-13 17:46:23 +02:00
// U s e a s l i g h t d e l a y t o d e b o u n c e u p d a t e s .
2018-06-13 19:02:20 +02:00
refreshSearchResults ( )
2018-06-13 17:37:01 +02:00
}
}
2018-06-08 06:14:46 +02:00
var searchResultSet : SearchResultSet = SearchResultSet . empty
2018-06-08 06:00:49 +02:00
var uiDatabaseConnection : YapDatabaseConnection {
return OWSPrimaryStorage . shared ( ) . uiDatabaseConnection
}
var searcher : ConversationSearcher {
return ConversationSearcher . shared
}
2018-06-08 03:51:42 +02:00
2018-06-12 21:12:14 +02:00
private var contactsManager : OWSContactsManager {
return Environment . current ( ) . contactsManager
}
2018-06-08 03:51:42 +02:00
enum SearchSection : Int {
2018-06-11 19:33:21 +02:00
case noResults
case conversations
case contacts
case messages
2018-06-08 03:51:42 +02:00
}
2018-06-12 17:27:32 +02:00
var blockedPhoneNumberSet = Set < String > ( )
2018-07-23 21:03:07 +02:00
private var hasThemeChanged = false
2018-06-13 17:37:01 +02:00
// MARK: V i e w L i f e c y c l e
2018-06-08 03:51:42 +02:00
2018-06-13 15:54:18 +02:00
override func viewDidLoad ( ) {
super . viewDidLoad ( )
2018-06-12 17:27:32 +02:00
let blockingManager = OWSBlockingManager . shared ( )
blockedPhoneNumberSet = Set ( blockingManager . blockedPhoneNumbers ( ) )
2018-06-08 03:51:42 +02:00
2018-06-09 07:00:07 +02:00
tableView . rowHeight = UITableViewAutomaticDimension
2018-06-15 17:08:01 +02:00
tableView . estimatedRowHeight = 60
2018-08-22 22:30:12 +02:00
tableView . separatorColor = Theme . cellSeparatorColor
2018-06-09 07:00:07 +02:00
2018-06-11 20:24:29 +02:00
tableView . register ( EmptySearchResultCell . self , forCellReuseIdentifier : EmptySearchResultCell . reuseIdentifier )
2018-06-12 17:27:32 +02:00
tableView . register ( HomeViewCell . self , forCellReuseIdentifier : HomeViewCell . cellReuseIdentifier ( ) )
tableView . register ( ContactTableViewCell . self , forCellReuseIdentifier : ContactTableViewCell . reuseIdentifier ( ) )
2018-06-13 17:37:01 +02:00
NotificationCenter . default . addObserver ( self ,
selector : #selector ( yapDatabaseModified ) ,
name : NSNotification . Name . YapDatabaseModified ,
object : OWSPrimaryStorage . shared ( ) . dbNotificationObject )
2018-07-23 21:03:07 +02:00
NotificationCenter . default . addObserver ( self ,
selector : #selector ( themeDidChange ) ,
name : NSNotification . Name . ThemeDidChange ,
object : nil )
applyTheme ( )
}
override func viewDidAppear ( _ animated : Bool ) {
super . viewDidAppear ( animated )
guard hasThemeChanged else {
return
}
hasThemeChanged = false
applyTheme ( )
self . tableView . reloadData ( )
}
deinit {
NotificationCenter . default . removeObserver ( self )
2018-06-13 17:37:01 +02:00
}
@objc internal func yapDatabaseModified ( notification : NSNotification ) {
2018-08-22 19:44:22 +02:00
AssertIsOnMainThread ( )
2018-06-13 17:37:01 +02:00
2018-06-13 19:02:20 +02:00
refreshSearchResults ( )
2018-06-08 03:51:42 +02:00
}
2018-07-23 21:03:07 +02:00
@objc internal func themeDidChange ( notification : NSNotification ) {
2018-08-22 19:44:22 +02:00
AssertIsOnMainThread ( )
2018-07-23 21:03:07 +02:00
applyTheme ( )
self . tableView . reloadData ( )
hasThemeChanged = true
}
private func applyTheme ( ) {
2018-08-22 19:44:22 +02:00
AssertIsOnMainThread ( )
2018-07-23 21:03:07 +02:00
self . view . backgroundColor = Theme . backgroundColor
self . tableView . backgroundColor = Theme . backgroundColor
}
2018-06-11 18:15:46 +02:00
// 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 )
2018-06-11 19:33:21 +02:00
2018-06-11 18:15:46 +02:00
guard let searchSection = SearchSection ( rawValue : indexPath . section ) else {
owsFail ( " \( logTag ) unknown section selected. " )
return
}
2018-06-11 19:33:21 +02:00
2018-06-11 18:15:46 +02:00
switch searchSection {
2018-06-11 19:33:21 +02:00
case . noResults :
owsFail ( " \( logTag ) shouldn't be able to tap 'no results' section " )
2018-06-11 18:15:46 +02:00
case . conversations :
2018-06-11 19:51:15 +02:00
let sectionResults = searchResultSet . conversations
guard let searchResult = sectionResults [ safe : indexPath . row ] else {
owsFail ( " \( logTag ) unknown row selected. " )
return
}
2018-06-11 19:33:21 +02:00
2018-06-11 19:51:15 +02:00
let thread = searchResult . thread
SignalApp . shared ( ) . presentConversation ( for : thread . threadRecord , action : . compose )
2018-06-11 19:33:21 +02:00
2018-06-11 18:15:46 +02:00
case . contacts :
2018-06-11 19:51:15 +02:00
let sectionResults = searchResultSet . contacts
guard let searchResult = sectionResults [ safe : indexPath . row ] else {
owsFail ( " \( logTag ) unknown row selected. " )
return
}
2018-06-11 19:33:21 +02:00
2018-06-11 19:51:15 +02:00
SignalApp . shared ( ) . presentConversation ( forRecipientId : searchResult . recipientId , action : . compose )
2018-06-11 19:33:21 +02:00
2018-06-11 18:15:46 +02:00
case . messages :
2018-06-11 19:51:15 +02:00
let sectionResults = searchResultSet . messages
guard let searchResult = sectionResults [ safe : indexPath . row ] else {
owsFail ( " \( logTag ) unknown row selected. " )
return
}
2018-06-11 19:33:21 +02:00
2018-06-11 19:51:15 +02:00
let thread = searchResult . thread
2018-06-11 21:31:54 +02:00
SignalApp . shared ( ) . presentConversation ( for : thread . threadRecord ,
action : . compose ,
focusMessageId : searchResult . messageId )
2018-06-11 18:15:46 +02:00
}
}
2018-06-09 07:00:07 +02:00
// MARK: U I T a b l e V i e w D a t a S o u r c e
2018-06-08 03:51:42 +02:00
2018-06-08 06:00:49 +02:00
override func tableView ( _ tableView : UITableView , numberOfRowsInSection section : Int ) -> Int {
2018-06-08 03:51:42 +02:00
guard let searchSection = SearchSection ( rawValue : section ) else {
owsFail ( " unknown section: \( section ) " )
return 0
}
switch searchSection {
2018-06-11 19:33:21 +02:00
case . noResults :
2018-06-11 20:24:29 +02:00
return searchResultSet . isEmpty ? 1 : 0
2018-06-08 03:51:42 +02:00
case . conversations :
2018-06-08 06:14:46 +02:00
return searchResultSet . conversations . count
2018-06-08 03:51:42 +02:00
case . contacts :
2018-06-08 06:14:46 +02:00
return searchResultSet . contacts . count
2018-06-08 03:51:42 +02:00
case . messages :
2018-06-08 06:14:46 +02:00
return searchResultSet . messages . count
2018-06-08 03:51:42 +02:00
}
}
2018-06-08 06:00:49 +02:00
override func tableView ( _ tableView : UITableView , cellForRowAt indexPath : IndexPath ) -> UITableViewCell {
guard let searchSection = SearchSection ( rawValue : indexPath . section ) else {
return UITableViewCell ( )
}
switch searchSection {
2018-06-11 19:33:21 +02:00
case . noResults :
2018-06-11 20:24:29 +02:00
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 ( )
}
2018-07-23 21:03:07 +02:00
OWSTableItem . configureCell ( cell )
2018-06-11 20:24:29 +02:00
let searchText = self . searchResultSet . searchText
cell . configure ( searchText : searchText )
return cell
2018-06-08 06:00:49 +02:00
case . conversations :
2018-06-12 17:27:32 +02:00
guard let cell = tableView . dequeueReusableCell ( withIdentifier : HomeViewCell . cellReuseIdentifier ( ) ) as ? HomeViewCell else {
2018-06-11 20:24:29 +02:00
owsFail ( " cell was unexpectedly nil " )
2018-06-08 06:00:49 +02:00
return UITableViewCell ( )
}
2018-06-08 06:14:46 +02:00
guard let searchResult = self . searchResultSet . conversations [ safe : indexPath . row ] else {
2018-06-11 20:24:29 +02:00
owsFail ( " searchResult was unexpectedly nil " )
2018-06-08 06:00:49 +02:00
return UITableViewCell ( )
}
2018-06-12 17:27:32 +02:00
cell . configure ( withThread : searchResult . thread , contactsManager : contactsManager , blockedPhoneNumber : self . blockedPhoneNumberSet )
2018-06-08 06:00:49 +02:00
return cell
case . contacts :
2018-06-12 17:27:32 +02:00
guard let cell = tableView . dequeueReusableCell ( withIdentifier : ContactTableViewCell . reuseIdentifier ( ) ) as ? ContactTableViewCell else {
2018-06-11 20:24:29 +02:00
owsFail ( " cell was unexpectedly nil " )
2018-06-11 18:20:49 +02:00
return UITableViewCell ( )
}
guard let searchResult = self . searchResultSet . contacts [ safe : indexPath . row ] else {
2018-06-11 20:24:29 +02:00
owsFail ( " searchResult was unexpectedly nil " )
2018-06-11 18:20:49 +02:00
return UITableViewCell ( )
}
2018-07-27 20:19:11 +02:00
cell . configure ( withRecipientId : searchResult . signalAccount . recipientId , contactsManager : contactsManager )
2018-06-11 18:20:49 +02:00
return cell
2018-06-08 06:00:49 +02:00
case . messages :
2018-06-12 17:27:32 +02:00
guard let cell = tableView . dequeueReusableCell ( withIdentifier : HomeViewCell . cellReuseIdentifier ( ) ) as ? HomeViewCell else {
2018-06-11 20:24:29 +02:00
owsFail ( " cell was unexpectedly nil " )
2018-06-08 19:23:17 +02:00
return UITableViewCell ( )
}
guard let searchResult = self . searchResultSet . messages [ safe : indexPath . row ] else {
2018-06-11 20:24:29 +02:00
owsFail ( " searchResult was unexpectedly nil " )
2018-06-08 06:00:49 +02:00
return UITableViewCell ( )
2018-06-08 19:23:17 +02:00
}
2018-06-12 17:27:32 +02:00
var overrideSnippet = NSAttributedString ( )
2018-06-13 16:23:12 +02:00
var overrideDate : Date ?
if searchResult . messageId != nil {
if let messageDate = searchResult . messageDate {
overrideDate = messageDate
2018-06-13 15:54:18 +02:00
} else {
2018-08-15 16:24:29 +02:00
owsFail ( " \( ConversationSearchViewController . logTag ( ) ) message search result is missing message timestamp " )
2018-06-12 17:27:32 +02:00
}
2018-06-13 15:54:18 +02:00
// N o t e t h a t w e o n l y u s e t h e s n i p p e t f o r m e s s a g e r e s u l t s ,
// n o t c o n v e r s a t i o n r e s u l t s . H o m e V i e w C e l l w i l l g e n e r a t e
// a s n i p p e t f o r c o n v e r s a t i o n s t h a t r e f l e c t s t h e l a t e s t
// c o n t e n t s .
if let messageSnippet = searchResult . snippet {
2018-08-07 23:12:16 +02:00
overrideSnippet = NSAttributedString ( string : messageSnippet ,
attributes : [
2018-08-16 23:27:20 +02:00
NSAttributedStringKey . foregroundColor : Theme . secondaryColor
2018-08-07 23:12:16 +02:00
] )
2018-06-13 15:54:18 +02:00
} else {
2018-08-15 16:24:29 +02:00
owsFail ( " \( ConversationSearchViewController . logTag ( ) ) message search result is missing message snippet " )
2018-06-12 17:27:32 +02:00
}
}
cell . configure ( withThread : searchResult . thread ,
contactsManager : contactsManager ,
blockedPhoneNumber : self . blockedPhoneNumberSet ,
2018-06-13 16:23:12 +02:00
overrideSnippet : overrideSnippet ,
overrideDate : overrideDate )
2018-06-12 17:27:32 +02:00
2018-06-08 19:23:17 +02:00
return cell
2018-06-08 06:00:49 +02:00
}
}
override func numberOfSections ( in tableView : UITableView ) -> Int {
2018-06-11 20:24:29 +02:00
return 4
2018-06-08 06:00:49 +02:00
}
2018-08-16 23:27:20 +02:00
override func tableView ( _ tableView : UITableView , heightForHeaderInSection section : Int ) -> CGFloat {
2018-08-22 22:30:12 +02:00
guard nil != self . tableView ( tableView , titleForHeaderInSection : section ) else {
2018-08-16 23:27:20 +02:00
return 0
}
2018-08-22 22:30:12 +02:00
return UITableViewAutomaticDimension
2018-08-16 23:27:20 +02:00
}
override func tableView ( _ tableView : UITableView , viewForHeaderInSection section : Int ) -> UIView ? {
guard let title = self . tableView ( tableView , titleForHeaderInSection : section ) else {
return nil
}
let label = UILabel ( )
label . textColor = Theme . secondaryColor
label . text = title
label . font = UIFont . ows_dynamicTypeBody . ows_mediumWeight ( )
label . tag = section
let hMargin : CGFloat = 15
let vMargin : CGFloat = 4
let wrapper = UIView ( )
wrapper . backgroundColor = Theme . offBackgroundColor
wrapper . addSubview ( label )
label . autoPinWidthToSuperview ( withMargin : hMargin )
label . autoPinHeightToSuperview ( withMargin : vMargin )
return wrapper
}
2018-06-08 06:00:49 +02:00
override func tableView ( _ tableView : UITableView , titleForHeaderInSection section : Int ) -> String ? {
guard let searchSection = SearchSection ( rawValue : section ) else {
owsFail ( " unknown section: \( section ) " )
return nil
}
switch searchSection {
2018-06-11 19:33:21 +02:00
case . noResults :
return nil
2018-06-08 06:00:49 +02:00
case . conversations :
2018-06-08 06:14:46 +02:00
if searchResultSet . conversations . count > 0 {
2018-06-08 06:00:49 +02:00
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 :
2018-06-08 06:14:46 +02:00
if searchResultSet . contacts . count > 0 {
2018-06-08 06:00:49 +02:00
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 :
2018-06-08 06:14:46 +02:00
if searchResultSet . messages . count > 0 {
2018-06-08 06:00:49 +02:00
return NSLocalizedString ( " SEARCH_SECTION_MESSAGES " , comment : " section header for search results that match a message in a conversation " )
} else {
return nil
}
}
}
2018-06-13 17:46:23 +02:00
// MARK: U p d a t e S e a r c h R e s u l t s
var refreshTimer : Timer ?
2018-06-13 19:02:20 +02:00
private func refreshSearchResults ( ) {
2018-08-22 19:44:22 +02:00
AssertIsOnMainThread ( )
2018-06-13 17:46:23 +02:00
2018-06-13 19:02:20 +02:00
guard ! searchResultSet . isEmpty else {
// T o a v o i d i n c o r r e c t l y s h o w i n g t h e " n o r e s u l t s " s t a t e ,
// a l w a y s s e a r c h i m m e d i a t e l y i f t h e c u r r e n t r e s u l t s e t i s e m p t y .
refreshTimer ? . invalidate ( )
refreshTimer = nil
updateSearchResults ( searchText : searchText )
return
}
2018-06-13 17:46:23 +02:00
if refreshTimer != nil {
// D o n ' t s t a r t a n e w r e f r e s h t i m e r i f t h e r e ' s a l r e a d y o n e a c t i v e .
return
}
refreshTimer ? . invalidate ( )
2018-06-13 19:02:20 +02:00
refreshTimer = WeakTimer . scheduledTimer ( timeInterval : 0.1 , target : self , userInfo : nil , repeats : false ) { [ weak self ] _ in
2018-06-13 17:46:23 +02:00
guard let strongSelf = self else {
return
}
strongSelf . updateSearchResults ( searchText : strongSelf . searchText )
strongSelf . refreshTimer = nil
}
}
2018-06-08 03:51:42 +02:00
2018-06-13 17:37:01 +02:00
private func updateSearchResults ( searchText : String ) {
2018-06-08 03:51:42 +02:00
guard searchText . stripped . count > 0 else {
2018-06-08 06:14:46 +02:00
self . searchResultSet = SearchResultSet . empty
2018-06-12 21:12:14 +02:00
self . tableView . reloadData ( )
2018-06-08 03:51:42 +02:00
return
}
2018-08-18 23:54:13 +02:00
var searchResults : SearchResultSet ?
2018-08-22 19:17:56 +02:00
self . uiDatabaseConnection . asyncRead ( { [ weak self ] transaction in
guard let strongSelf = self else { return }
searchResults = strongSelf . searcher . results ( searchText : searchText , transaction : transaction , contactsManager : strongSelf . contactsManager )
2018-08-18 23:54:13 +02:00
} ,
2018-08-22 19:17:56 +02:00
completionBlock : { [ weak self ] in
2018-08-22 19:44:22 +02:00
AssertIsOnMainThread ( )
2018-08-22 19:17:56 +02:00
guard let strongSelf = self else { return }
2018-08-18 23:54:13 +02:00
guard let results = searchResults else {
2018-08-22 19:17:56 +02:00
owsFail ( " \( strongSelf . logTag ) in \( #function ) searchResults was unexpectedly nil " )
2018-08-18 23:54:13 +02:00
return
}
2018-08-22 19:17:56 +02:00
strongSelf . searchResultSet = results
strongSelf . tableView . reloadData ( )
2018-08-18 23:54:13 +02:00
} )
2018-06-08 03:51:42 +02:00
}
2018-06-21 15:59:01 +02:00
// MARK: - U I S c r o l l V i e w D e l e g a t e
override func scrollViewWillBeginDragging ( _ scrollView : UIScrollView ) {
delegate ? . conversationSearchViewWillBeginDragging ( )
}
2018-06-09 07:00:07 +02:00
}
2018-06-08 03:51:42 +02:00
2018-06-11 20:24:29 +02:00
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 . 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
2018-08-22 20:21:19 +02:00
messageLabel . textColor = Theme . primaryColor
messageLabel . font = UIFont . ows_dynamicTypeBody
2018-06-11 20:24:29 +02:00
}
}