From 5c0c01dea2cb13657cecf63513380350749b066d Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Tue, 1 May 2018 16:38:54 -0400 Subject: [PATCH] Contact picking // FREEBIE --- Signal.xcodeproj/project.pbxproj | 4 - .../src/ViewControllers/ContactsPicker.swift | 101 ++++++++---------- .../ConversationViewController.m | 96 +++++++++++++---- Signal/src/ViewControllers/InviteFlow.swift | 14 +++ Signal/src/views/ContactCell.swift | 92 ++++++++++------ Signal/src/views/ContactCell.xib | 81 -------------- .../translations/en.lproj/Localizable.strings | 6 ++ 7 files changed, 200 insertions(+), 194 deletions(-) delete mode 100644 Signal/src/views/ContactCell.xib diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index f0c29f4b5..931c39def 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -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 = ""; }; 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 = ""; }; - 451764281DE939FD00EDB8B9 /* ContactCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ContactCell.xib; sourceTree = ""; }; 451764291DE939FD00EDB8B9 /* ContactCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactCell.swift; sourceTree = ""; }; 451777C71FD61554001225FF /* ConversationSearcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationSearcher.swift; sourceTree = ""; }; 451A13B01E13DED2000A50FD /* CallNotificationsAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; name = CallNotificationsAdapter.swift; path = ../UserInterface/Notifications/CallNotificationsAdapter.swift; sourceTree = ""; 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 */, diff --git a/Signal/src/ViewControllers/ContactsPicker.swift b/Signal/src/ViewControllers/ContactsPicker.swift index 65f9fef59..bc6af9e35 100644 --- a/Signal/src/ViewControllers/ContactsPicker.swift +++ b/Signal/src/ViewControllers/ContactsPicker.swift @@ -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) } diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 2ab1497a9..54d6c97bb 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -118,9 +118,10 @@ typedef enum : NSUInteger { @interface ConversationViewController () 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 *)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 diff --git a/Signal/src/ViewControllers/InviteFlow.swift b/Signal/src/ViewControllers/InviteFlow.swift index 8f2b469f6..075fa0d66 100644 --- a/Signal/src/ViewControllers/InviteFlow.swift +++ b/Signal/src/ViewControllers/InviteFlow.swift @@ -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) } diff --git a/Signal/src/views/ContactCell.swift b/Signal/src/views/ContactCell.swift index 5875a230a..aa520ef95 100644 --- a/Signal/src/views/ContactCell.swift +++ b/Signal/src/views/ContactCell.swift @@ -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") } } } diff --git a/Signal/src/views/ContactCell.xib b/Signal/src/views/ContactCell.xib deleted file mode 100644 index 0c7e8401d..000000000 --- a/Signal/src/views/ContactCell.xib +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index b6f2583e7..596023376 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -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";