2017-05-01 20:28:37 +02:00
//
2019-01-08 17:18:05 +01:00
// C o p y r i g h t ( c ) 2 0 1 9 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 .
2017-05-01 20:28:37 +02:00
//
2016-11-23 17:07:38 +01:00
// O r i g i n a l l y b a s e d o n E P C o n t a c t s
//
// C r e a t e d b y P r a b a h a r a n E l a n g o v a n o n 1 2 / 1 0 / 1 5 .
2017-05-01 20:28:37 +02:00
// P a r t s C o p y r i g h t © 2 0 1 5 P r a b a h a r a n E l a n g o v a n . A l l r i g h t s r e s e r v e d
2016-11-23 17:07:38 +01:00
import UIKit
import Contacts
2017-11-28 00:17:46 +01:00
import SignalServiceKit
2016-11-23 17:07:38 +01:00
2018-05-01 22:38:54 +02:00
@objc
public protocol ContactsPickerDelegate : class {
func contactsPicker ( _ : ContactsPicker , contactFetchDidFail error : NSError )
func contactsPickerDidCancel ( _ : ContactsPicker )
2016-11-23 17:07:38 +01:00
func contactsPicker ( _ : ContactsPicker , didSelectContact contact : Contact )
func contactsPicker ( _ : ContactsPicker , didSelectMultipleContacts contacts : [ Contact ] )
func contactsPicker ( _ : ContactsPicker , shouldSelectContact contact : Contact ) -> Bool
}
2018-05-01 22:38:54 +02:00
@objc
public enum SubtitleCellValue : Int {
case phoneNumber , email , none
2016-11-23 17:07:38 +01:00
}
2018-05-01 22:38:54 +02:00
@objc
2018-08-21 22:56:21 +02:00
public class ContactsPicker : OWSViewController , UITableViewDelegate , UITableViewDataSource , UISearchBarDelegate {
2016-12-12 13:45:16 +01:00
2018-08-03 20:35:06 +02:00
var tableView : UITableView !
2018-08-21 22:56:21 +02:00
var searchBar : UISearchBar !
2016-11-23 17:07:38 +01:00
// MARK: - P r o p e r t i e s
2018-05-01 22:38:54 +02:00
private let contactCellReuseIdentifier = " contactCellReuseIdentifier "
private var contactsManager : OWSContactsManager {
2018-08-31 19:22:19 +02:00
return Environment . shared . contactsManager
2018-05-01 22:38:54 +02:00
}
2018-05-08 21:58:32 +02:00
// H A C K : T h o u g h w e d o n ' t h a v e a n i n p u t a c c e s s o r y v i e w , t h e V C w e a r e p r e s e n t e d a b o v e ( C o n v e r s a t i o n V C ) d o e s .
// I f t h e a p p i s b a c k g r o u n d e d a n d t h e n f o r e g r o u n d e d , w h e n O W S W i n d o w M a n a g e r c a l l s m a i n W i n d o w . m a k e K e y A n d V i s i b l e
// t h e C o n v e r s a t i o n V C ' s i n p u t A c c e s s o r y V i e w w i l l a p p e a r * a b o v e * u s u n l e s s w e ' d p r e v i o u s l y b e c o m e f i r s t r e s p o n d e r .
override public var canBecomeFirstResponder : Bool {
2018-08-23 16:37:34 +02:00
Logger . debug ( " " )
2018-05-08 21:58:32 +02:00
return true
}
override public func becomeFirstResponder ( ) -> Bool {
2018-08-23 16:37:34 +02:00
Logger . debug ( " " )
2018-05-08 21:58:32 +02:00
return super . becomeFirstResponder ( )
}
override public func resignFirstResponder ( ) -> Bool {
2018-08-23 16:37:34 +02:00
Logger . debug ( " " )
2018-05-08 21:58:32 +02:00
return super . resignFirstResponder ( )
}
2018-05-01 22:38:54 +02:00
private let collation = UILocalizedIndexedCollation . current ( )
2018-05-25 18:51:49 +02:00
public var collationForTests : UILocalizedIndexedCollation {
get {
return collation
}
}
2018-05-01 22:38:54 +02:00
private let contactStore = CNContactStore ( )
2016-11-23 17:07:38 +01:00
// D a t a S o u r c e S t a t e
2018-05-01 22:38:54 +02:00
private lazy var sections = [ [ CNContact ] ] ( )
private lazy var filteredSections = [ [ CNContact ] ] ( )
private lazy var selectedContacts = [ Contact ] ( )
2016-11-23 17:07:38 +01:00
// C o n f i g u r a t i o n
2018-05-25 23:28:36 +02:00
@objc
2018-05-01 22:38:54 +02:00
public weak var contactsPickerDelegate : ContactsPickerDelegate ?
2018-05-09 17:54:10 +02:00
private let subtitleCellType : SubtitleCellValue
private let allowsMultipleSelection : Bool
private let allowedContactKeys : [ CNKeyDescriptor ] = ContactsFrameworkContactStoreAdaptee . allowedContactKeys
// MARK: - I n i t i a l i z e r s
@objc
required public init ( allowsMultipleSelection : Bool , subtitleCellType : SubtitleCellValue ) {
self . allowsMultipleSelection = allowsMultipleSelection
self . subtitleCellType = subtitleCellType
2018-08-03 20:35:06 +02:00
super . init ( nibName : nil , bundle : nil )
2018-05-09 17:54:10 +02:00
}
required public init ? ( coder aDecoder : NSCoder ) {
2018-08-27 16:21:03 +02:00
notImplemented ( )
2018-05-09 17:54:10 +02:00
}
2016-11-23 17:07:38 +01:00
// MARK: - L i f e c y c l e M e t h o d s
2018-08-03 20:35:06 +02:00
override public func loadView ( ) {
self . view = UIView ( )
let tableView = UITableView ( )
self . tableView = tableView
2018-08-22 22:30:12 +02:00
self . tableView . separatorColor = Theme . cellSeparatorColor
2018-08-03 20:35:06 +02:00
view . addSubview ( tableView )
2019-01-08 17:18:05 +01:00
tableView . autoPinEdge ( toSuperviewEdge : . top )
tableView . autoPinEdge ( toSuperviewEdge : . bottom )
tableView . autoPinEdge ( toSuperviewSafeArea : . leading )
tableView . autoPinEdge ( toSuperviewSafeArea : . trailing )
2018-08-03 20:35:06 +02:00
tableView . delegate = self
tableView . dataSource = self
2018-08-15 23:09:59 +02:00
let searchBar = OWSSearchBar ( )
2018-08-03 20:35:06 +02:00
self . searchBar = searchBar
searchBar . delegate = self
searchBar . sizeToFit ( )
tableView . tableHeaderView = searchBar
}
2016-11-23 17:07:38 +01:00
override open func viewDidLoad ( ) {
super . viewDidLoad ( )
2018-07-23 21:51:12 +02:00
self . view . backgroundColor = Theme . backgroundColor
self . tableView . backgroundColor = Theme . backgroundColor
2016-12-12 13:45:16 +01:00
searchBar . placeholder = NSLocalizedString ( " INVITE_FRIENDS_PICKER_SEARCHBAR_PLACEHOLDER " , comment : " Search " )
2016-11-23 17:07:38 +01:00
// A u t o s i z e c e l l s f o r d y n a m i c t y p e
tableView . estimatedRowHeight = 60.0
2019-03-30 14:22:31 +01:00
tableView . rowHeight = UITableView . automaticDimension
2018-06-15 17:08:01 +02:00
tableView . estimatedRowHeight = 60
2016-11-23 17:07:38 +01:00
2018-05-09 17:54:10 +02:00
tableView . allowsMultipleSelection = allowsMultipleSelection
2016-11-23 17:07:38 +01:00
2018-05-01 22:38:54 +02:00
tableView . separatorInset = UIEdgeInsets ( top : 0 , left : ContactCell . kSeparatorHInset , bottom : 0 , right : 16 )
2016-11-23 17:07:38 +01:00
registerContactCell ( )
initializeBarButtons ( )
reloadContacts ( )
2016-12-12 13:45:16 +01:00
updateSearchResults ( searchText : " " )
2016-11-23 17:07:38 +01:00
2019-03-30 14:22:31 +01:00
NotificationCenter . default . addObserver ( self , selector : #selector ( self . didChangePreferredContentSize ) , name : UIContentSizeCategory . didChangeNotification , object : nil )
2016-11-23 17:07:38 +01:00
}
2018-05-01 22:38:54 +02:00
@objc
public func didChangePreferredContentSize ( ) {
2016-11-23 17:07:38 +01:00
self . tableView . reloadData ( )
}
2017-05-01 20:28:37 +02:00
2018-05-01 22:38:54 +02:00
private func initializeBarButtons ( ) {
let cancelButton = UIBarButtonItem ( barButtonSystemItem : . cancel , target : self , action : #selector ( onTouchCancelButton ) )
2016-11-23 17:07:38 +01:00
self . navigationItem . leftBarButtonItem = cancelButton
2017-05-01 20:28:37 +02:00
2018-05-09 17:54:10 +02:00
if allowsMultipleSelection {
2017-03-23 18:46:41 +01:00
let doneButton = UIBarButtonItem ( barButtonSystemItem : . done , target : self , action : #selector ( onTouchDoneButton ) )
2016-11-23 17:07:38 +01:00
self . navigationItem . rightBarButtonItem = doneButton
}
}
2017-05-01 20:28:37 +02:00
2018-05-01 22:38:54 +02:00
private func registerContactCell ( ) {
tableView . register ( ContactCell . self , forCellReuseIdentifier : contactCellReuseIdentifier )
2016-11-23 17:07:38 +01:00
}
// MARK: - C o n t a c t O p e r a t i o n s
2017-05-01 20:28:37 +02:00
2018-05-01 22:38:54 +02:00
private func reloadContacts ( ) {
2016-11-23 17:07:38 +01:00
getContacts ( onError : { error in
2018-08-23 16:37:34 +02:00
Logger . error ( " failed to reload contacts with error: \( error ) " )
2016-11-23 17:07:38 +01:00
} )
}
2018-05-01 22:38:54 +02:00
private func getContacts ( onError errorHandler : @ escaping ( _ error : Error ) -> Void ) {
2016-11-23 17:07:38 +01:00
switch CNContactStore . authorizationStatus ( for : CNEntityType . contacts ) {
case CNAuthorizationStatus . denied , CNAuthorizationStatus . restricted :
2017-05-05 23:34:37 +02:00
let title = NSLocalizedString ( " INVITE_FLOW_REQUIRES_CONTACT_ACCESS_TITLE " , comment : " Alert title when contacts disabled while trying to invite contacts to signal " )
let body = NSLocalizedString ( " INVITE_FLOW_REQUIRES_CONTACT_ACCESS_BODY " , comment : " Alert body when contacts disabled while trying to invite contacts to signal " )
2017-05-01 20:28:37 +02:00
2019-03-30 14:22:31 +01:00
let alert = UIAlertController ( title : title , message : body , preferredStyle : UIAlertController . Style . alert )
2016-11-23 17:07:38 +01:00
2017-09-01 15:30:16 +02:00
let dismissText = CommonStrings . cancelButton
2016-11-23 17:07:38 +01:00
2017-05-05 23:34:37 +02:00
let cancelAction = UIAlertAction ( title : dismissText , style : . cancel , handler : { _ in
2016-11-23 17:07:38 +01:00
let error = NSError ( domain : " contactsPickerErrorDomain " , code : 1 , userInfo : [ NSLocalizedDescriptionKey : " No Contacts Access " ] )
2018-05-01 22:38:54 +02:00
self . contactsPickerDelegate ? . contactsPicker ( self , contactFetchDidFail : error )
2016-11-23 17:07:38 +01:00
errorHandler ( error )
} )
2017-05-05 23:34:37 +02:00
alert . addAction ( cancelAction )
2018-01-17 21:41:08 +01:00
let settingsText = CommonStrings . openSettingsButton
2017-05-05 23:34:37 +02:00
let openSettingsAction = UIAlertAction ( title : settingsText , style : . default , handler : { ( _ ) in
UIApplication . shared . openSystemSettings ( )
} )
alert . addAction ( openSettingsAction )
2019-03-21 15:55:04 +01:00
self . presentAlert ( alert )
2017-05-01 20:28:37 +02:00
2016-11-23 17:07:38 +01:00
case CNAuthorizationStatus . notDetermined :
// T h i s c a s e m e a n s t h e u s e r i s p r o m p t e d f o r t h e f i r s t t i m e f o r a l l o w i n g c o n t a c t s
contactStore . requestAccess ( for : CNEntityType . contacts ) { ( granted , error ) -> Void in
// A t t h i s p o i n t a n a l e r t i s p r o v i d e d t o t h e u s e r t o p r o v i d e a c c e s s t o c o n t a c t s . T h i s w i l l g e t i n v o k e d i f a u s e r r e s p o n d s t o t h e a l e r t
if granted {
self . getContacts ( onError : errorHandler )
} else {
errorHandler ( error ! )
}
}
2017-05-01 20:28:37 +02:00
2016-11-23 17:07:38 +01:00
case CNAuthorizationStatus . authorized :
// A u t h o r i z a t i o n g r a n t e d b y u s e r f o r t h i s a p p .
var contacts = [ CNContact ] ( )
do {
let contactFetchRequest = CNContactFetchRequest ( keysToFetch : allowedContactKeys )
2018-05-31 22:49:29 +02:00
contactFetchRequest . sortOrder = . userDefault
2017-05-01 20:28:37 +02:00
try contactStore . enumerateContacts ( with : contactFetchRequest ) { ( contact , _ ) -> Void in
2016-11-23 17:07:38 +01:00
contacts . append ( contact )
}
self . sections = collatedContacts ( contacts )
} catch let error as NSError {
2018-08-23 16:37:34 +02:00
Logger . error ( " Failed to fetch contacts with error: \( error ) " )
2016-11-23 17:07:38 +01:00
}
}
}
func collatedContacts ( _ contacts : [ CNContact ] ) -> [ [ CNContact ] ] {
2018-05-31 22:49:29 +02:00
let selector : Selector = #selector ( getter : CNContact . nameForCollating )
2016-11-23 17:07:38 +01:00
var collated = Array ( repeating : [ CNContact ] ( ) , count : collation . sectionTitles . count )
for contact in contacts {
2018-05-31 22:49:29 +02:00
let sectionNumber = collation . section ( for : contact , collationStringSelector : selector )
2016-11-23 17:07:38 +01:00
collated [ sectionNumber ] . append ( contact )
}
return collated
}
// MARK: - T a b l e V i e w D a t a S o u r c e
2017-05-01 20:28:37 +02:00
2016-12-12 13:45:16 +01:00
open func numberOfSections ( in tableView : UITableView ) -> Int {
2016-11-23 17:07:38 +01:00
return self . collation . sectionTitles . count
}
2017-05-01 20:28:37 +02:00
2016-12-12 13:45:16 +01:00
open func tableView ( _ tableView : UITableView , numberOfRowsInSection section : Int ) -> Int {
let dataSource = filteredSections
2016-11-23 17:07:38 +01:00
2017-05-05 23:34:37 +02:00
guard section < dataSource . count else {
return 0
}
2016-11-23 17:07:38 +01:00
return dataSource [ section ] . count
}
// MARK: - T a b l e V i e w D e l e g a t e s
2016-12-12 13:45:16 +01:00
open func tableView ( _ tableView : UITableView , cellForRowAt indexPath : IndexPath ) -> UITableViewCell {
2018-05-09 17:54:10 +02:00
guard let cell = tableView . dequeueReusableCell ( withIdentifier : contactCellReuseIdentifier , for : indexPath ) as ? ContactCell else {
2018-08-27 16:27:48 +02:00
owsFailDebug ( " cell had unexpected type " )
2018-05-09 17:54:10 +02:00
return UITableViewCell ( )
}
2016-11-23 17:07:38 +01:00
2016-12-12 13:45:16 +01:00
let dataSource = filteredSections
2016-11-23 17:07:38 +01:00
let cnContact = dataSource [ indexPath . section ] [ indexPath . row ]
2017-05-01 20:28:37 +02:00
let contact = Contact ( systemContact : cnContact )
2016-11-23 17:07:38 +01:00
2018-05-09 17:54:10 +02:00
cell . configure ( contact : contact , subtitleType : subtitleCellType , showsWhenSelected : self . allowsMultipleSelection , contactsManager : self . contactsManager )
2016-11-23 17:07:38 +01:00
let isSelected = selectedContacts . contains ( where : { $0 . uniqueId = = contact . uniqueId } )
cell . isSelected = isSelected
// M a k e s u r e w e p r e s e r v e s e l e c t i o n a c r o s s t a b l e V i e w . r e l o a d D a t a w h i c h h a p p e n s w h e n t o g g l i n g b e t w e e n
// s e a r c h c o n t r o l l e r
if ( isSelected ) {
self . tableView . selectRow ( at : indexPath , animated : false , scrollPosition : . none )
} else {
self . tableView . deselectRow ( at : indexPath , animated : false )
}
return cell
}
2016-12-12 13:45:16 +01:00
open func tableView ( _ tableView : UITableView , didDeselectRowAt indexPath : IndexPath ) {
2016-11-23 17:07:38 +01:00
let cell = tableView . cellForRow ( at : indexPath ) as ! ContactCell
let deselectedContact = cell . contact !
2017-05-01 20:28:37 +02:00
selectedContacts = selectedContacts . filter {
2016-11-23 17:07:38 +01:00
return $0 . uniqueId != deselectedContact . uniqueId
}
}
2016-12-12 13:45:16 +01:00
open func tableView ( _ tableView : UITableView , didSelectRowAt indexPath : IndexPath ) {
2018-08-23 16:37:34 +02:00
Logger . verbose ( " " )
2018-05-04 23:13:42 +02:00
2016-11-23 17:07:38 +01:00
let cell = tableView . cellForRow ( at : indexPath ) as ! ContactCell
let selectedContact = cell . contact !
guard ( contactsPickerDelegate = = nil || contactsPickerDelegate ! . contactsPicker ( self , shouldSelectContact : selectedContact ) ) else {
self . tableView . deselectRow ( at : indexPath , animated : false )
return
}
selectedContacts . append ( selectedContact )
2018-05-09 17:54:10 +02:00
if ! allowsMultipleSelection {
// S i n g l e s e l e c t i o n c o d e
self . contactsPickerDelegate ? . contactsPicker ( self , didSelectContact : selectedContact )
2016-11-23 17:07:38 +01:00
}
}
2017-05-01 20:28:37 +02:00
2016-12-12 13:45:16 +01:00
open func tableView ( _ tableView : UITableView , sectionForSectionIndexTitle title : String , at index : Int ) -> Int {
2016-11-23 17:07:38 +01:00
return collation . section ( forSectionIndexTitle : index )
}
2017-05-01 20:28:37 +02:00
2016-12-12 13:45:16 +01:00
open func sectionIndexTitles ( for tableView : UITableView ) -> [ String ] ? {
2016-11-23 17:07:38 +01:00
return collation . sectionIndexTitles
}
2016-12-12 13:45:16 +01:00
open func tableView ( _ tableView : UITableView , titleForHeaderInSection section : Int ) -> String ? {
let dataSource = filteredSections
2016-11-23 17:07:38 +01:00
2017-05-05 23:34:37 +02:00
guard section < dataSource . count else {
2017-05-05 23:14:12 +02:00
return nil
}
2017-05-05 23:34:37 +02:00
2017-10-14 19:20:46 +02:00
// D o n ' t s h o w e m p t y s e c t i o n s
2016-11-23 17:07:38 +01:00
if dataSource [ section ] . count > 0 {
2017-05-05 23:34:37 +02:00
guard section < collation . sectionTitles . count else {
2017-05-05 23:14:12 +02:00
return nil
}
2017-05-05 23:34:37 +02:00
2016-11-23 17:07:38 +01:00
return collation . sectionTitles [ section ]
} else {
return nil
}
}
2017-05-01 20:28:37 +02:00
2016-11-23 17:07:38 +01:00
// MARK: - B u t t o n A c t i o n s
2017-05-01 20:28:37 +02:00
2018-05-25 18:54:25 +02:00
@objc func onTouchCancelButton ( ) {
2018-05-01 22:38:54 +02:00
contactsPickerDelegate ? . contactsPickerDidCancel ( self )
2016-11-23 17:07:38 +01:00
}
2017-05-01 20:28:37 +02:00
2018-05-25 18:54:25 +02:00
@objc func onTouchDoneButton ( ) {
2016-11-23 17:07:38 +01:00
contactsPickerDelegate ? . contactsPicker ( self , didSelectMultipleContacts : selectedContacts )
}
2017-05-01 20:28:37 +02:00
2016-11-23 17:07:38 +01:00
// MARK: - S e a r c h A c t i o n s
2018-08-21 22:56:21 +02:00
open func searchBar ( _ searchBar : UISearchBar , textDidChange searchText : String ) {
2016-12-12 13:45:16 +01:00
updateSearchResults ( searchText : searchText )
}
2017-05-01 20:28:37 +02:00
2016-12-12 13:45:16 +01:00
open func updateSearchResults ( searchText : String ) {
let predicate : NSPredicate
2018-07-30 05:13:00 +02:00
if searchText . isEmpty {
2016-12-12 13:45:16 +01:00
filteredSections = sections
} else {
do {
predicate = CNContact . predicateForContacts ( matchingName : searchText )
2017-05-01 20:28:37 +02:00
let filteredContacts = try contactStore . unifiedContacts ( matching : predicate , keysToFetch : allowedContactKeys )
2016-11-23 17:07:38 +01:00
filteredSections = collatedContacts ( filteredContacts )
2016-12-12 13:45:16 +01:00
} catch let error as NSError {
2018-08-23 16:37:34 +02:00
Logger . error ( " updating search results failed with error: \( error ) " )
2016-11-23 17:07:38 +01:00
}
}
2016-12-12 13:45:16 +01:00
self . tableView . reloadData ( )
2016-11-23 17:07:38 +01:00
}
}
let ContactSortOrder = computeSortOrder ( )
func computeSortOrder ( ) -> CNContactSortOrder {
let comparator = CNContact . comparator ( forNameSortOrder : . userDefault )
let contact0 = CNMutableContact ( )
contact0 . givenName = " A "
contact0 . familyName = " Z "
let contact1 = CNMutableContact ( )
contact1 . givenName = " Z "
contact1 . familyName = " A "
let result = comparator ( contact0 , contact1 )
if result = = . orderedAscending {
return . givenName
} else {
return . familyName
}
}
fileprivate extension CNContact {
/* *
* Sorting Key used by collation
*/
@objc var nameForCollating : String {
get {
2016-12-03 15:14:19 +01:00
if self . familyName . isEmpty && self . givenName . isEmpty {
2018-05-31 22:49:29 +02:00
return self . emailAddresses . first ? . value as String ? ? ? " "
2016-12-03 15:14:19 +01:00
}
2017-05-01 20:28:37 +02:00
2016-11-23 17:07:38 +01:00
let compositeName : String
if ContactSortOrder = = . familyName {
compositeName = " \( self . familyName ) \( self . givenName ) "
} else {
compositeName = " \( self . givenName ) \( self . familyName ) "
}
2018-05-31 22:49:29 +02:00
return compositeName . trimmingCharacters ( in : CharacterSet . whitespacesAndNewlines )
2016-11-23 17:07:38 +01:00
}
}
}