Contact picking

// FREEBIE
This commit is contained in:
Michael Kirk 2018-05-01 16:38:54 -04:00
parent 0e858bc090
commit 5c0c01dea2
7 changed files with 200 additions and 194 deletions

View File

@ -248,7 +248,6 @@
451166C01FD86B98000739BA /* AccountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451166BF1FD86B98000739BA /* AccountManager.swift */; };
451573962061B49500803601 /* GradientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451573952061B49500803601 /* GradientView.swift */; };
451686AB1F520CDA00AC3D4B /* MultiDeviceProfileKeyUpdateJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451686AA1F520CDA00AC3D4B /* MultiDeviceProfileKeyUpdateJob.swift */; };
4517642A1DE939FD00EDB8B9 /* ContactCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 451764281DE939FD00EDB8B9 /* ContactCell.xib */; };
4517642B1DE939FD00EDB8B9 /* ContactCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451764291DE939FD00EDB8B9 /* ContactCell.swift */; };
45194F8F1FD71FF500333B2C /* ThreadUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = 346129BE1FD2068600532771 /* ThreadUtil.m */; };
45194F901FD7200000333B2C /* ThreadUtil.h in Headers */ = {isa = PBXBuildFile; fileRef = 346129BD1FD2068600532771 /* ThreadUtil.h */; settings = {ATTRIBUTES = (Public, ); }; };
@ -900,7 +899,6 @@
451166BF1FD86B98000739BA /* AccountManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountManager.swift; sourceTree = "<group>"; };
451573952061B49500803601 /* GradientView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = GradientView.swift; path = SignalMessaging/Views/GradientView.swift; sourceTree = SOURCE_ROOT; };
451686AA1F520CDA00AC3D4B /* MultiDeviceProfileKeyUpdateJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultiDeviceProfileKeyUpdateJob.swift; sourceTree = "<group>"; };
451764281DE939FD00EDB8B9 /* ContactCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ContactCell.xib; sourceTree = "<group>"; };
451764291DE939FD00EDB8B9 /* ContactCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactCell.swift; sourceTree = "<group>"; };
451777C71FD61554001225FF /* ConversationSearcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationSearcher.swift; sourceTree = "<group>"; };
451A13B01E13DED2000A50FD /* CallNotificationsAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; name = CallNotificationsAdapter.swift; path = ../UserInterface/Notifications/CallNotificationsAdapter.swift; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
@ -2085,7 +2083,6 @@
452EA09D1EA7ABE00078744B /* AttachmentPointerView.swift */,
34E3E5671EC4B19400495BAC /* AudioProgressView.swift */,
451764291DE939FD00EDB8B9 /* ContactCell.swift */,
451764281DE939FD00EDB8B9 /* ContactCell.xib */,
4523149F1F7E9E18003A428C /* DirectionalPanGestureRecognizer.swift */,
45A663C41F92EC760027B59E /* GroupTableViewCell.swift */,
45E5A6981F61E6DD001E4A8A /* MarqueeLabel.swift */,
@ -2698,7 +2695,6 @@
B66DBF4A19D5BBC8006EA940 /* Images.xcassets in Resources */,
34CF0788203E6B78005C4D61 /* ringback_tone_ansi.caf in Resources */,
34C3C78F2040A4F70000134C /* sonarping.mp3 in Resources */,
4517642A1DE939FD00EDB8B9 /* ContactCell.xib in Resources */,
AD83FF431A73426500B5C81A /* audio_play_button@2x.png in Resources */,
45CB2FA81CB7146C00E1B343 /* Launch Screen.storyboard in Resources */,
34B3F8781E8DF1700035BE1A /* ContactsPicker.xib in Resources */,

View File

@ -11,50 +11,52 @@ import UIKit
import Contacts
import SignalServiceKit
public protocol ContactsPickerDelegate {
func contactsPicker(_: ContactsPicker, didContactFetchFailed error: NSError)
func contactsPicker(_: ContactsPicker, didCancel error: NSError)
@objc
public protocol ContactsPickerDelegate: class {
func contactsPicker(_: ContactsPicker, contactFetchDidFail error: NSError)
func contactsPickerDidCancel(_: ContactsPicker)
func contactsPicker(_: ContactsPicker, didSelectContact contact: Contact)
func contactsPicker(_: ContactsPicker, didSelectMultipleContacts contacts: [Contact])
func contactsPicker(_: ContactsPicker, shouldSelectContact contact: Contact) -> Bool
}
public extension ContactsPickerDelegate {
func contactsPicker(_: ContactsPicker, didContactFetchFailed error: NSError) { }
func contactsPicker(_: ContactsPicker, didCancel error: NSError) { }
func contactsPicker(_: ContactsPicker, didSelectContact contact: Contact) { }
func contactsPicker(_: ContactsPicker, didSelectMultipleContacts contacts: [Contact]) { }
func contactsPicker(_: ContactsPicker, shouldSelectContact contact: Contact) -> Bool { return true }
}
public enum SubtitleCellValue {
case phoneNumber
case email
@objc
public enum SubtitleCellValue: Int {
case phoneNumber, email, none
}
open class ContactsPicker: OWSViewController, UITableViewDelegate, UITableViewDataSource, UISearchBarDelegate {
@objc
public class ContactsPicker: OWSViewController, UITableViewDelegate, UITableViewDataSource, UISearchBarDelegate {
@IBOutlet var tableView: UITableView!
@IBOutlet var searchBar: UISearchBar!
// MARK: - Properties
let TAG = "[ContactsPicker]"
let contactCellReuseIdentifier = "contactCellReuseIdentifier"
let contactsManager: OWSContactsManager
let collation = UILocalizedIndexedCollation.current()
let contactStore = CNContactStore()
private let TAG = "[ContactsPicker]"
private let contactCellReuseIdentifier = "contactCellReuseIdentifier"
private var contactsManager: OWSContactsManager {
return Environment.current().contactsManager
}
private let collation = UILocalizedIndexedCollation.current()
private let contactStore = CNContactStore()
// Data Source State
lazy var sections = [[CNContact]]()
lazy var filteredSections = [[CNContact]]()
lazy var selectedContacts = [Contact]()
private lazy var sections = [[CNContact]]()
private lazy var filteredSections = [[CNContact]]()
private lazy var selectedContacts = [Contact]()
// Configuration
open var contactsPickerDelegate: ContactsPickerDelegate?
var subtitleCellValue = SubtitleCellValue.phoneNumber
var multiSelectEnabled = false
let allowedContactKeys: [CNKeyDescriptor] = [
public weak var contactsPickerDelegate: ContactsPickerDelegate?
private let subtitleCellValue: SubtitleCellValue
private let multiSelectEnabled: Bool
private let allowedContactKeys: [CNKeyDescriptor] = [
CNContactFormatter.descriptorForRequiredKeys(for: .fullName),
CNContactThumbnailImageDataKey as CNKeyDescriptor,
CNContactPhoneNumbersKey as CNKeyDescriptor,
@ -65,10 +67,9 @@ open class ContactsPicker: OWSViewController, UITableViewDelegate, UITableViewDa
override open func viewDidLoad() {
super.viewDidLoad()
title = NSLocalizedString("INVITE_FRIENDS_PICKER_TITLE", comment: "Navbar title")
searchBar.placeholder = NSLocalizedString("INVITE_FRIENDS_PICKER_SEARCHBAR_PLACEHOLDER", comment: "Search")
// Prevent content form going under the navigation bar
// Prevent content from going under the navigation bar
self.edgesForExtendedLayout = []
// Auto size cells for dynamic type
@ -77,6 +78,8 @@ open class ContactsPicker: OWSViewController, UITableViewDelegate, UITableViewDa
tableView.allowsMultipleSelection = multiSelectEnabled
tableView.separatorInset = UIEdgeInsets(top: 0, left: ContactCell.kSeparatorHInset, bottom: 0, right: 16)
registerContactCell()
initializeBarButtons()
reloadContacts()
@ -85,12 +88,13 @@ open class ContactsPicker: OWSViewController, UITableViewDelegate, UITableViewDa
NotificationCenter.default.addObserver(self, selector: #selector(self.didChangePreferredContentSize), name: NSNotification.Name.UIContentSizeCategoryDidChange, object: nil)
}
func didChangePreferredContentSize() {
@objc
public func didChangePreferredContentSize() {
self.tableView.reloadData()
}
func initializeBarButtons() {
let cancelButton = UIBarButtonItem(barButtonSystemItem: .stop, target: self, action: #selector(onTouchCancelButton))
private func initializeBarButtons() {
let cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(onTouchCancelButton))
self.navigationItem.leftBarButtonItem = cancelButton
if multiSelectEnabled {
@ -99,48 +103,33 @@ open class ContactsPicker: OWSViewController, UITableViewDelegate, UITableViewDa
}
}
fileprivate func registerContactCell() {
tableView.register(ContactCell.nib, forCellReuseIdentifier: contactCellReuseIdentifier)
private func registerContactCell() {
tableView.register(ContactCell.self, forCellReuseIdentifier: contactCellReuseIdentifier)
}
// MARK: - Initializers
init() {
contactsManager = Environment.current().contactsManager
@objc
required public init(delegate: ContactsPickerDelegate?, multiSelection: Bool, subtitleCellType: SubtitleCellValue) {
multiSelectEnabled = multiSelection
subtitleCellValue = subtitleCellType
super.init(nibName: nil, bundle: nil)
contactsPickerDelegate = delegate
}
required public init?(coder aDecoder: NSCoder) {
contactsManager = Environment.current().contactsManager
super.init(coder: aDecoder)
}
convenience public init(delegate: ContactsPickerDelegate?) {
self.init(delegate: delegate, multiSelection: false)
}
convenience public init(delegate: ContactsPickerDelegate?, multiSelection: Bool) {
self.init()
multiSelectEnabled = multiSelection
contactsPickerDelegate = delegate
}
convenience public init(delegate: ContactsPickerDelegate?, multiSelection: Bool, subtitleCellType: SubtitleCellValue) {
self.init()
multiSelectEnabled = multiSelection
contactsPickerDelegate = delegate
subtitleCellValue = subtitleCellType
fatalError("init(coder:) has not been implemented")
}
// MARK: - Contact Operations
open func reloadContacts() {
private func reloadContacts() {
getContacts( onError: { error in
Logger.error("\(self.TAG) failed to reload contacts with error:\(error)")
})
}
func getContacts(onError errorHandler: @escaping (_ error: Error) -> Void) {
private func getContacts(onError errorHandler: @escaping (_ error: Error) -> Void) {
switch CNContactStore.authorizationStatus(for: CNEntityType.contacts) {
case CNAuthorizationStatus.denied, CNAuthorizationStatus.restricted:
let title = NSLocalizedString("INVITE_FLOW_REQUIRES_CONTACT_ACCESS_TITLE", comment: "Alert title when contacts disabled while trying to invite contacts to signal")
@ -152,7 +141,7 @@ open class ContactsPicker: OWSViewController, UITableViewDelegate, UITableViewDa
let cancelAction = UIAlertAction(title: dismissText, style: .cancel, handler: { _ in
let error = NSError(domain: "contactsPickerErrorDomain", code: 1, userInfo: [NSLocalizedDescriptionKey: "No Contacts Access"])
self.contactsPickerDelegate?.contactsPicker(self, didContactFetchFailed: error)
self.contactsPickerDelegate?.contactsPicker(self, contactFetchDidFail: error)
errorHandler(error)
self.dismiss(animated: true, completion: nil)
})
@ -229,7 +218,7 @@ open class ContactsPicker: OWSViewController, UITableViewDelegate, UITableViewDa
let cnContact = dataSource[indexPath.section][indexPath.row]
let contact = Contact(systemContact: cnContact)
cell.updateContactsinUI(contact, subtitleType: subtitleCellValue, contactsManager: self.contactsManager)
cell.configure(contact: contact, subtitleType: subtitleCellValue, contactsManager: self.contactsManager)
let isSelected = selectedContacts.contains(where: { $0.uniqueId == contact.uniqueId })
cell.isSelected = isSelected
@ -302,7 +291,7 @@ open class ContactsPicker: OWSViewController, UITableViewDelegate, UITableViewDa
// MARK: - Button Actions
func onTouchCancelButton() {
contactsPickerDelegate?.contactsPicker(self, didCancel: NSError(domain: "contactsPickerErrorDomain", code: 2, userInfo: [ NSLocalizedDescriptionKey: "User Canceled Selection"]))
contactsPickerDelegate?.contactsPickerDidCancel(self)
dismiss(animated: true, completion: nil)
}

View File

@ -118,9 +118,10 @@ typedef enum : NSUInteger {
@interface ConversationViewController () <AttachmentApprovalViewControllerDelegate,
AVAudioPlayerDelegate,
ContactsViewHelperDelegate,
ContactEditingDelegate,
CNContactViewControllerDelegate,
ContactEditingDelegate,
ContactsPickerDelegate,
ContactsViewHelperDelegate,
DisappearingTimerConfigurationViewDelegate,
OWSConversationSettingsViewDelegate,
ConversationHeaderViewDelegate,
@ -1562,7 +1563,7 @@ typedef enum : NSUInteger {
if (self.dynamicInteractions.unreadIndicatorPosition) {
NSUInteger unreadIndicatorPosition
= (NSUInteger)[self.dynamicInteractions.unreadIndicatorPosition longValue];
// If there is an unread indicator, increase the initial load window
// to include it.
OWSAssert(unreadIndicatorPosition > 0);
@ -1585,7 +1586,7 @@ typedef enum : NSUInteger {
rangeLength = MIN(rangeLength, kYapDatabaseRangeMaxLength);
self.lastRangeLength = rangeLength;
YapDatabaseViewRangeOptions *rangeOptions =
[YapDatabaseViewRangeOptions flexibleRangeWithLength:rangeLength offset:0 from:YapDatabaseViewEnd];
@ -2495,6 +2496,20 @@ typedef enum : NSUInteger {
#endif
}
#pragma mark - Attachment Picking: Contacts
- (void)chooseContactForSending
{
ContactsPicker *contactsPicker =
[[ContactsPicker alloc] initWithDelegate:self multiSelection:NO subtitleCellType:SubtitleCellValueNone];
contactsPicker.title
= NSLocalizedString(@"CONTACT_PICKER_TITLE", @"navbar title for contact picker when sharing a contact");
UINavigationController *navigationController =
[[UINavigationController alloc] initWithRootViewController:contactsPicker];
[self dismissKeyBoard];
[self presentViewController:navigationController animated:YES completion:nil];
}
#pragma mark - Attachment Picking: Documents
- (void)showAttachmentDocumentPickerMenu
@ -2677,13 +2692,13 @@ typedef enum : NSUInteger {
DDLogWarn(@"%@ camera permission denied.", self.logTag);
return;
}
UIImagePickerController *picker = [UIImagePickerController new];
picker.sourceType = UIImagePickerControllerSourceTypeCamera;
picker.mediaTypes = @[ (__bridge NSString *)kUTTypeImage, (__bridge NSString *)kUTTypeMovie ];
picker.allowsEditing = NO;
picker.delegate = self;
[self dismissKeyBoard];
[self presentViewController:picker animated:YES completion:[UIUtil modalCompletionBlock]];
}];
@ -2714,12 +2729,12 @@ typedef enum : NSUInteger {
DDLogWarn(@"%@ Media Library permission denied.", self.logTag);
return;
}
UIImagePickerController *picker = [UIImagePickerController new];
picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
picker.delegate = self;
picker.mediaTypes = @[ (__bridge NSString *)kUTTypeImage, (__bridge NSString *)kUTTypeMovie ];
[self dismissKeyBoard];
[self presentViewController:picker animated:YES completion:[UIUtil modalCompletionBlock]];
}];
@ -2954,8 +2969,7 @@ typedef enum : NSUInteger {
}];
}
#pragma mark Storage access
#pragma mark - Storage access
- (YapDatabaseConnection *)uiDatabaseConnection
{
@ -3207,14 +3221,14 @@ typedef enum : NSUInteger {
BOOL shouldAnimateUpdates = [self shouldAnimateRowUpdates:rowChanges oldViewItemCount:oldViewItemCount];
void (^batchUpdatesCompletion)(BOOL) = ^(BOOL finished) {
OWSAssertIsOnMainThread();
if (!finished) {
DDLogInfo(@"%@ performBatchUpdates did not finish", self.logTag);
}
[self updateLastVisibleTimestamp];
if (scrollToBottom) {
[self scrollToBottomAnimated:shouldAnimateScrollToBottom && shouldAnimateUpdates];
}
@ -3334,13 +3348,13 @@ typedef enum : NSUInteger {
if (!strongSelf) {
return;
}
if (strongSelf.voiceMessageUUID != voiceMessageUUID) {
// This voice message recording has been cancelled
// before recording could begin.
return;
}
if (granted) {
[strongSelf startRecordingVoiceMemo];
} else {
@ -3576,6 +3590,18 @@ typedef enum : NSUInteger {
[gifAction setValue:gifImage forKey:@"image"];
[actionSheetController addAction:gifAction];
UIAlertAction *chooseContactAction = [UIAlertAction
actionWithTitle:NSLocalizedString(@"ATTACHMENT_MENU_CONTACT_BUTTON", @"attachment menu option to send contact")
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *_Nonnull action) {
[self chooseContactForSending];
}];
// TODO - proper image
UIImage *chooseContactImage = [UIImage imageNamed:@"actionsheet_camera_black"];
OWSAssert(takeMediaImage);
[chooseContactAction setValue:chooseContactImage forKey:@"image"];
[actionSheetController addAction:chooseContactAction];
[self dismissKeyBoard];
[self presentViewController:actionSheetController animated:true completion:nil];
}
@ -4066,7 +4092,7 @@ typedef enum : NSUInteger {
[self updateGroupModelTo:groupModel
successCompletion:^{
DDLogInfo(@"Group updated, removing group creation error.");
[message remove];
}];
}
@ -4327,9 +4353,8 @@ typedef enum : NSUInteger {
// first item at or after the "view horizon". See the comments below which explain
// the "view horizon".
ConversationViewItem *_Nullable lastViewItem = self.viewItems.lastObject;
BOOL hasAddedNewItems = (lastViewItem &&
previousLastTimestamp &&
lastViewItem.interaction.timestamp > previousLastTimestamp.unsignedLongLongValue);
BOOL hasAddedNewItems = (lastViewItem && previousLastTimestamp
&& lastViewItem.interaction.timestamp > previousLastTimestamp.unsignedLongLongValue);
DDLogInfo(@"%@ hasAddedNewItems: %d", self.logTag, hasAddedNewItems);
if (hasAddedNewItems) {
@ -4848,6 +4873,37 @@ interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransiti
conversationViewCell.isCellVisible = NO;
}
#pragma mark - ContactsPickerDelegate
- (void)contactsPickerDidCancel:(ContactsPicker *)contactsPicker
{
DDLogDebug(@"%@ in %s", self.logTag, __PRETTY_FUNCTION__);
}
- (void)contactsPicker:(ContactsPicker *)contactsPicker contactFetchDidFail:(NSError *)error
{
DDLogDebug(@"%@ in %s with error %@", self.logTag, __PRETTY_FUNCTION__, error);
}
- (void)contactsPicker:(ContactsPicker *)contactsPicker didSelectContact:(Contact *)contact
{
DDLogDebug(@"%@ in %s with contact: %@", self.logTag, __PRETTY_FUNCTION__, contact);
// TODO actually build contact message.
self.inputToolbar.messageText = contact.fullName;
}
- (void)contactsPicker:(ContactsPicker *)contactsPicker didSelectMultipleContacts:(NSArray<Contact *> *)contacts
{
OWSFail(@"%@ in %s with contacts: %@", self.logTag, __PRETTY_FUNCTION__, contacts);
}
- (BOOL)contactsPicker:(ContactsPicker *)contactsPicker shouldSelectContact:(Contact *)contact
{
// Any reason to preclude contacts?
return YES;
}
@end
NS_ASSUME_NONNULL_END

View File

@ -122,6 +122,18 @@ class InviteFlow: NSObject, MFMessageComposeViewControllerDelegate, MFMailCompos
return true
}
func contactsPicker(_: ContactsPicker, contactFetchDidFail error: NSError) {
Logger.error("\(self.logTag) in \(#function) with error: \(error)")
}
func contactsPickerDidCancel(_: ContactsPicker) {
Logger.debug("\(self.logTag) in \(#function)")
}
func contactsPicker(_: ContactsPicker, didSelectContact contact: Contact) {
owsFail("\(logTag) in \(#function) InviteFlow only supports multi-select")
}
// MARK: SMS
func messageAction() -> UIAlertAction? {
@ -135,6 +147,7 @@ class InviteFlow: NSObject, MFMessageComposeViewControllerDelegate, MFMailCompos
Logger.debug("\(self.TAG) Chose message.")
self.channel = .message
let picker = ContactsPicker(delegate: self, multiSelection: true, subtitleCellType: .phoneNumber)
picker.title = NSLocalizedString("INVITE_FRIENDS_PICKER_TITLE", comment: "Navbar title")
let navigationController = UINavigationController(rootViewController: picker)
self.presentingViewController.present(navigationController, animated: true)
}
@ -197,6 +210,7 @@ class InviteFlow: NSObject, MFMessageComposeViewControllerDelegate, MFMailCompos
self.channel = .mail
let picker = ContactsPicker(delegate: self, multiSelection: true, subtitleCellType: .email)
picker.title = NSLocalizedString("INVITE_FRIENDS_PICKER_TITLE", comment: "Navbar title")
let navigationController = UINavigationController(rootViewController: picker)
self.presentingViewController.present(navigationController, animated: true)
}

View File

@ -8,29 +8,53 @@ import SignalServiceKit
class ContactCell: UITableViewCell {
static let nib = UINib(nibName: "ContactCell", bundle: nil)
public static let kSeparatorHInset: CGFloat = CGFloat(kAvatarDiameter) + 16 + 8
@IBOutlet weak var contactTextLabel: UILabel!
@IBOutlet weak var contactDetailTextLabel: UILabel!
@IBOutlet weak var contactImageView: UIImageView!
@IBOutlet weak var contactContainerView: UIView!
static let kAvatarSpacing: CGFloat = 6
static let kAvatarDiameter: UInt = 40
let contactImageView: AvatarImageView
let textStackView: UIStackView
let titleLabel: UILabel
var subtitleLabel: UILabel
var contact: Contact?
override func awakeFromNib() {
super.awakeFromNib()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
self.contactImageView = AvatarImageView()
self.textStackView = UIStackView()
self.titleLabel = UILabel()
self.titleLabel.font = UIFont.ows_dynamicTypeBody
self.subtitleLabel = UILabel()
self.subtitleLabel.font = UIFont.ows_dynamicTypeSubheadline
super.init(style: style, reuseIdentifier: reuseIdentifier)
// Initialization code
selectionStyle = UITableViewCellSelectionStyle.none
contactContainerView.layer.masksToBounds = true
contactContainerView.layer.cornerRadius = contactContainerView.frame.size.width/2
textStackView.axis = .vertical
textStackView.addArrangedSubview(titleLabel)
contactImageView.autoSetDimensions(to: CGSize(width: CGFloat(ContactCell.kAvatarDiameter), height: CGFloat(ContactCell.kAvatarDiameter)))
let contentColumns: UIStackView = UIStackView(arrangedSubviews: [contactImageView, textStackView])
contentColumns.axis = .horizontal
contentColumns.spacing = ContactCell.kAvatarSpacing
contentColumns.alignment = .center
self.contentView.addSubview(contentColumns)
contentColumns.autoPinEdgesToSuperviewMargins()
NotificationCenter.default.addObserver(self, selector: #selector(self.didChangePreferredContentSize), name: NSNotification.Name.UIContentSizeCategoryDidChange, object: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
accessoryType = .none
self.subtitleLabel.removeFromSuperview()
}
override func setSelected(_ selected: Bool, animated: Bool) {
@ -39,20 +63,19 @@ class ContactCell: UITableViewCell {
}
func didChangePreferredContentSize() {
contactTextLabel.font = UIFont.preferredFont(forTextStyle: .body)
contactDetailTextLabel.font = UIFont.preferredFont(forTextStyle: .subheadline)
self.titleLabel.font = UIFont.ows_dynamicTypeBody
self.subtitleLabel.font = UIFont.ows_dynamicTypeSubheadline
}
func updateContactsinUI(_ contact: Contact, subtitleType: SubtitleCellValue, contactsManager: OWSContactsManager) {
func configure(contact: Contact, subtitleType: SubtitleCellValue, contactsManager: OWSContactsManager) {
self.contact = contact
if contactTextLabel != nil {
contactTextLabel.attributedText = contact.cnContact?.formattedFullName(font: contactTextLabel.font)
}
titleLabel.attributedText = contact.cnContact?.formattedFullName(font: titleLabel.font)
updateSubtitle(subtitleType: subtitleType, contact: contact)
updateSubtitleBasedonType(subtitleType, contact: contact)
if contact.image == nil {
if let contactImage = contact.image {
contactImageView.image = contactImage
} else {
let contactIdForDeterminingBackgroundColor: String
if let signalId = contact.parsedPhoneNumbers.first?.toE164() {
contactIdForDeterminingBackgroundColor = signalId
@ -60,32 +83,35 @@ class ContactCell: UITableViewCell {
contactIdForDeterminingBackgroundColor = contact.fullName
}
let kAvatarWidth: UInt = 40
let avatarBuilder = OWSContactAvatarBuilder(nonSignalName: contact.fullName,
colorSeed: contactIdForDeterminingBackgroundColor,
diameter: kAvatarWidth,
diameter: ContactCell.kAvatarDiameter,
contactsManager: contactsManager)
self.contactImageView?.image = avatarBuilder.buildDefaultImage()
} else {
self.contactImageView?.image = contact.image
contactImageView.image = avatarBuilder.buildDefaultImage()
}
}
func updateSubtitleBasedonType(_ subtitleType: SubtitleCellValue, contact: Contact) {
func updateSubtitle(subtitleType: SubtitleCellValue, contact: Contact) {
switch subtitleType {
case .none:
assert(self.subtitleLabel.superview == nil)
break
case .phoneNumber:
self.textStackView.addArrangedSubview(self.subtitleLabel)
case SubtitleCellValue.phoneNumber:
if contact.userTextPhoneNumbers.count > 0 {
self.contactDetailTextLabel.text = "\(contact.userTextPhoneNumbers[0])"
if let firstPhoneNumber = contact.userTextPhoneNumbers.first {
self.subtitleLabel.text = firstPhoneNumber
} else {
self.contactDetailTextLabel.text = NSLocalizedString("CONTACT_PICKER_NO_PHONE_NUMBERS_AVAILABLE", comment: "table cell subtitle when contact card has no known phone number")
self.subtitleLabel.text = NSLocalizedString("CONTACT_PICKER_NO_PHONE_NUMBERS_AVAILABLE", comment: "table cell subtitle when contact card has no known phone number")
}
case SubtitleCellValue.email:
if contact.emails.count > 0 {
self.contactDetailTextLabel.text = "\(contact.emails[0])"
case .email:
self.textStackView.addArrangedSubview(self.subtitleLabel)
if let firstEmail = contact.emails.first {
self.subtitleLabel.text = firstEmail
} else {
self.contactDetailTextLabel.text = NSLocalizedString("CONTACT_PICKER_NO_EMAILS_AVAILABLE", comment: "table cell subtitle when contact card has no email")
self.subtitleLabel.text = NSLocalizedString("CONTACT_PICKER_NO_EMAILS_AVAILABLE", comment: "table cell subtitle when contact card has no email")
}
}
}

View File

@ -1,81 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="11542" systemVersion="15G1108" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11524"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="60" id="KGk-i7-Jjw" customClass="ContactCell" customModule="Signal" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="320" height="60"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
<rect key="frame" x="0.0" y="0.0" width="320" height="59"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="bKn-WF-9hk">
<rect key="frame" x="0.0" y="0.0" width="320" height="59"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="59" id="z02-WI-o4t"/>
</constraints>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="HUe-8f-NjY">
<rect key="frame" x="60" y="10" width="250" height="21"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<color key="textColor" red="0.33333333333333331" green="0.33333333333333331" blue="0.33333333333333331" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Tqx-Gt-ofy">
<rect key="frame" x="60" y="35" width="234" height="18"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
<color key="textColor" red="0.66666666666666663" green="0.66666666666666663" blue="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Bvt-tZ-NrJ">
<rect key="frame" x="10" y="10" width="40" height="40"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="mxT-DB-6lI">
<rect key="frame" x="0.0" y="0.0" width="40" height="40"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="0.7725490196" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="width" constant="40" id="8v6-UT-2rU"/>
<constraint firstAttribute="height" constant="40" id="pAq-lQ-W6t"/>
</constraints>
</view>
</subviews>
<constraints>
<constraint firstItem="Bvt-tZ-NrJ" firstAttribute="centerY" secondItem="H2p-sc-9uM" secondAttribute="centerY" id="1V9-eI-crl"/>
<constraint firstAttribute="trailing" secondItem="Tqx-Gt-ofy" secondAttribute="trailing" constant="26" id="36W-0D-Qv3"/>
<constraint firstItem="HUe-8f-NjY" firstAttribute="leading" secondItem="Bvt-tZ-NrJ" secondAttribute="trailing" constant="10" id="8Ws-h4-oJp"/>
<constraint firstItem="bKn-WF-9hk" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" id="NDc-ZH-7qQ"/>
<constraint firstItem="Tqx-Gt-ofy" firstAttribute="leading" secondItem="Bvt-tZ-NrJ" secondAttribute="trailing" constant="10" id="PDp-yF-F2b"/>
<constraint firstItem="HUe-8f-NjY" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="10" id="R7t-dd-hcs"/>
<constraint firstItem="Tqx-Gt-ofy" firstAttribute="top" secondItem="HUe-8f-NjY" secondAttribute="bottom" constant="4" id="aNx-bp-Uj5"/>
<constraint firstAttribute="trailing" secondItem="HUe-8f-NjY" secondAttribute="trailing" constant="10" id="cOY-df-aFi"/>
<constraint firstItem="Bvt-tZ-NrJ" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="10" id="m7q-1p-kjc"/>
<constraint firstAttribute="bottom" secondItem="Tqx-Gt-ofy" secondAttribute="bottom" constant="6" id="mEe-il-eMD"/>
<constraint firstAttribute="bottom" secondItem="bKn-WF-9hk" secondAttribute="bottom" id="oHg-Fe-wLe"/>
<constraint firstAttribute="trailing" secondItem="bKn-WF-9hk" secondAttribute="trailing" id="p0G-Qg-21x"/>
<constraint firstItem="bKn-WF-9hk" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" id="q4R-qf-Bvi"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="contactContainerView" destination="Bvt-tZ-NrJ" id="qqg-f5-Xol"/>
<outlet property="contactDetailTextLabel" destination="Tqx-Gt-ofy" id="Jlj-TK-UJl"/>
<outlet property="contactImageView" destination="mxT-DB-6lI" id="aYl-FS-HAU"/>
<outlet property="contactTextLabel" destination="HUe-8f-NjY" id="Smr-wZ-MHr"/>
</connections>
<point key="canvasLocation" x="245" y="321"/>
</tableViewCell>
</objects>
</document>

View File

@ -139,6 +139,9 @@
/* Accessibility label for attaching photos */
"ATTACHMENT_LABEL" = "Attachment";
/* attachment menu option to send contact */
"ATTACHMENT_MENU_CONTACT_BUTTON" = "Contact";
/* Alert title when picking a document fails for an unknown reason */
"ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "Failed to choose document.";
@ -403,6 +406,9 @@
/* table cell subtitle when contact card has no known phone number */
"CONTACT_PICKER_NO_PHONE_NUMBERS_AVAILABLE" = "No phone number available.";
/* navbar title for contact picker when sharing a contact */
"CONTACT_PICKER_TITLE" = "Select Contact";
/* title for conversation settings screen */
"CONVERSATION_SETTINGS" = "Conversation Settings";