mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
re-use contact picker for "add to existing"
Required refactor of contact picker to be presented non-modally. TODO: merge emails, address, display names // FREEBIE
This commit is contained in:
parent
609746abec
commit
0c469764f1
10 changed files with 253 additions and 60 deletions
|
@ -294,6 +294,7 @@
|
||||||
4523149E1F7E916B003A428C /* SlideOffAnimatedTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4523149D1F7E916B003A428C /* SlideOffAnimatedTransition.swift */; };
|
4523149E1F7E916B003A428C /* SlideOffAnimatedTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4523149D1F7E916B003A428C /* SlideOffAnimatedTransition.swift */; };
|
||||||
452314A01F7E9E18003A428C /* DirectionalPanGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4523149F1F7E9E18003A428C /* DirectionalPanGestureRecognizer.swift */; };
|
452314A01F7E9E18003A428C /* DirectionalPanGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4523149F1F7E9E18003A428C /* DirectionalPanGestureRecognizer.swift */; };
|
||||||
4523D016206EDC2B00A2AB51 /* LRUCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4523D015206EDC2B00A2AB51 /* LRUCache.swift */; };
|
4523D016206EDC2B00A2AB51 /* LRUCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4523D015206EDC2B00A2AB51 /* LRUCache.swift */; };
|
||||||
|
452B999020A34B6B006F2F9E /* AddContactShareToExistingContactViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452B998F20A34B6B006F2F9E /* AddContactShareToExistingContactViewController.swift */; };
|
||||||
452C468F1E427E200087B011 /* OutboundCallInitiator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452C468E1E427E200087B011 /* OutboundCallInitiator.swift */; };
|
452C468F1E427E200087B011 /* OutboundCallInitiator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452C468E1E427E200087B011 /* OutboundCallInitiator.swift */; };
|
||||||
452C7CA72037628B003D51A5 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F170D51E315310003FC1F2 /* Weak.swift */; };
|
452C7CA72037628B003D51A5 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F170D51E315310003FC1F2 /* Weak.swift */; };
|
||||||
452D1AF12081059C00A67F7F /* StringAdditionsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452D1AF02081059C00A67F7F /* StringAdditionsTest.swift */; };
|
452D1AF12081059C00A67F7F /* StringAdditionsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452D1AF02081059C00A67F7F /* StringAdditionsTest.swift */; };
|
||||||
|
@ -926,6 +927,7 @@
|
||||||
4523149D1F7E916B003A428C /* SlideOffAnimatedTransition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SlideOffAnimatedTransition.swift; path = UserInterface/SlideOffAnimatedTransition.swift; sourceTree = "<group>"; };
|
4523149D1F7E916B003A428C /* SlideOffAnimatedTransition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SlideOffAnimatedTransition.swift; path = UserInterface/SlideOffAnimatedTransition.swift; sourceTree = "<group>"; };
|
||||||
4523149F1F7E9E18003A428C /* DirectionalPanGestureRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectionalPanGestureRecognizer.swift; sourceTree = "<group>"; };
|
4523149F1F7E9E18003A428C /* DirectionalPanGestureRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectionalPanGestureRecognizer.swift; sourceTree = "<group>"; };
|
||||||
4523D015206EDC2B00A2AB51 /* LRUCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LRUCache.swift; sourceTree = "<group>"; };
|
4523D015206EDC2B00A2AB51 /* LRUCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LRUCache.swift; sourceTree = "<group>"; };
|
||||||
|
452B998F20A34B6B006F2F9E /* AddContactShareToExistingContactViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactShareToExistingContactViewController.swift; sourceTree = "<group>"; };
|
||||||
452C468E1E427E200087B011 /* OutboundCallInitiator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutboundCallInitiator.swift; sourceTree = "<group>"; };
|
452C468E1E427E200087B011 /* OutboundCallInitiator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutboundCallInitiator.swift; sourceTree = "<group>"; };
|
||||||
452D1AF02081059C00A67F7F /* StringAdditionsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringAdditionsTest.swift; sourceTree = "<group>"; };
|
452D1AF02081059C00A67F7F /* StringAdditionsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringAdditionsTest.swift; sourceTree = "<group>"; };
|
||||||
452D1AF220810B6F00A67F7F /* String+OWS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+OWS.swift"; sourceTree = "<group>"; };
|
452D1AF220810B6F00A67F7F /* String+OWS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+OWS.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -1681,6 +1683,7 @@
|
||||||
34B3F86E1E8DF1700035BE1A /* SignalsNavigationController.m */,
|
34B3F86E1E8DF1700035BE1A /* SignalsNavigationController.m */,
|
||||||
340FC897204DAC8D007AEB0F /* ThreadSettings */,
|
340FC897204DAC8D007AEB0F /* ThreadSettings */,
|
||||||
34D1F0BE1F8EC1760066283D /* Utils */,
|
34D1F0BE1F8EC1760066283D /* Utils */,
|
||||||
|
452B998F20A34B6B006F2F9E /* AddContactShareToExistingContactViewController.swift */,
|
||||||
);
|
);
|
||||||
path = ViewControllers;
|
path = ViewControllers;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -3238,6 +3241,7 @@
|
||||||
340FC8BC204DAC8D007AEB0F /* FingerprintViewController.m in Sources */,
|
340FC8BC204DAC8D007AEB0F /* FingerprintViewController.m in Sources */,
|
||||||
450DF2051E0D74AC003D14BE /* Platform.swift in Sources */,
|
450DF2051E0D74AC003D14BE /* Platform.swift in Sources */,
|
||||||
340FC8B2204DAC8D007AEB0F /* AdvancedSettingsTableViewController.m in Sources */,
|
340FC8B2204DAC8D007AEB0F /* AdvancedSettingsTableViewController.m in Sources */,
|
||||||
|
452B999020A34B6B006F2F9E /* AddContactShareToExistingContactViewController.swift in Sources */,
|
||||||
346129991FD1E4DA00532771 /* SignalApp.m in Sources */,
|
346129991FD1E4DA00532771 /* SignalApp.m in Sources */,
|
||||||
34BECE301F7ABCF800D7438D /* GifPickerLayout.swift in Sources */,
|
34BECE301F7ABCF800D7438D /* GifPickerLayout.swift in Sources */,
|
||||||
343A65951FC47D5E000477A1 /* DebugUISyncMessages.m in Sources */,
|
343A65951FC47D5E000477A1 /* DebugUISyncMessages.m in Sources */,
|
||||||
|
|
|
@ -0,0 +1,121 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import ContactsUI
|
||||||
|
|
||||||
|
class AddContactShareToExistingContactViewController: ContactsPicker, ContactsPickerDelegate, CNContactViewControllerDelegate {
|
||||||
|
|
||||||
|
// TODO actual protocol?
|
||||||
|
weak var addToExistingContactDelegate: UIViewController?
|
||||||
|
|
||||||
|
let contactShare: ContactShareViewModel
|
||||||
|
|
||||||
|
required init(contactShare: ContactShareViewModel) {
|
||||||
|
self.contactShare = contactShare
|
||||||
|
super.init(allowsMultipleSelection: false, subtitleCellType: .none)
|
||||||
|
|
||||||
|
self.contactsPickerDelegate = self
|
||||||
|
}
|
||||||
|
|
||||||
|
required public init?(coder aDecoder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc required public init(allowsMultipleSelection: Bool, subtitleCellType: SubtitleCellValue) {
|
||||||
|
fatalError("init(allowsMultipleSelection:subtitleCellType:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - ContactsPickerDelegate
|
||||||
|
|
||||||
|
func contactsPicker(_: ContactsPicker, contactFetchDidFail error: NSError) {
|
||||||
|
owsFail("\(logTag) in \(#function) with error: \(error)")
|
||||||
|
|
||||||
|
guard let navigationController = self.navigationController else {
|
||||||
|
owsFail("\(logTag) in \(#function) navigationController was unexpectedly nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
navigationController.popViewController(animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func contactsPickerDidCancel(_: ContactsPicker) {
|
||||||
|
Logger.debug("\(self.logTag) in \(#function)")
|
||||||
|
guard let navigationController = self.navigationController else {
|
||||||
|
owsFail("\(logTag) in \(#function) navigationController was unexpectedly nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
navigationController.popViewController(animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func contactsPicker(_: ContactsPicker, didSelectContact contact: Contact) {
|
||||||
|
Logger.debug("\(self.logTag) in \(#function)")
|
||||||
|
|
||||||
|
guard let mergedContact: CNContact = self.contactShare.cnContact(mergedWithExistingContact: contact) else {
|
||||||
|
// TODO maybe this should not be optional and return a blank contact so we can still save the (not-actually merged) contact
|
||||||
|
owsFail("\(logTag) in \(#function) navigationController was unexpectedly nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not actually a "new" contact, but this brings up the edit form rather than the "Read" form
|
||||||
|
// saving our users a tap in some cases when we already know they want to edit.
|
||||||
|
let contactViewController: CNContactViewController = CNContactViewController(forNewContact: mergedContact)
|
||||||
|
|
||||||
|
// Default title is "New Contact". We could give a more descriptive title, but anything
|
||||||
|
// seems redundant - the context is sufficiently clear.
|
||||||
|
contactViewController.title = ""
|
||||||
|
contactViewController.allowsActions = false
|
||||||
|
contactViewController.allowsEditing = true
|
||||||
|
contactViewController.delegate = self
|
||||||
|
|
||||||
|
guard let navigationController = self.navigationController else {
|
||||||
|
owsFail("\(logTag) in \(#function) navigationController was unexpectedly nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
navigationController.pushViewController(contactViewController, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func contactsPicker(_: ContactsPicker, didSelectMultipleContacts contacts: [Contact]) {
|
||||||
|
Logger.debug("\(self.logTag) in \(#function)")
|
||||||
|
owsFail("\(logTag) only supports single contact select")
|
||||||
|
|
||||||
|
guard let navigationController = self.navigationController else {
|
||||||
|
owsFail("\(logTag) in \(#function) navigationController was unexpectedly nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
navigationController.popViewController(animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func contactsPicker(_: ContactsPicker, shouldSelectContact contact: Contact) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - CNContactViewControllerDelegate
|
||||||
|
|
||||||
|
public func contactViewController(_ viewController: CNContactViewController, didCompleteWith contact: CNContact?) {
|
||||||
|
Logger.debug("\(self.logTag) in \(#function)")
|
||||||
|
|
||||||
|
guard let navigationController = self.navigationController else {
|
||||||
|
owsFail("\(logTag) in \(#function) navigationController was unexpectedly nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// We want to pop *this* view and the still presented CNContactViewController in a single animation.
|
||||||
|
// Note this happens for *cancel* and for *done*. Unfortunately, I don't know of a way to detect the difference
|
||||||
|
// between the two, since both just call this method.
|
||||||
|
guard let myIndex = navigationController.viewControllers.index(of: self) else {
|
||||||
|
owsFail("\(logTag) in \(#function) myIndex was unexpectedly nil")
|
||||||
|
navigationController.popViewController(animated: true)
|
||||||
|
navigationController.popViewController(animated: true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let previousViewControllerIndex = navigationController.viewControllers.index(before: myIndex)
|
||||||
|
let previousViewController = navigationController.viewControllers[previousViewControllerIndex]
|
||||||
|
navigationController.popToViewController(previousViewController, animated: true)
|
||||||
|
}
|
||||||
|
}
|
|
@ -177,6 +177,7 @@ public class ContactShareViewHelper: NSObject, CNContactViewControllerDelegate {
|
||||||
UIUtil.applyDefaultSystemAppearence()
|
UIUtil.applyDefaultSystemAppearence()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MJK TODO fromNavigationController?
|
||||||
private func presentSelectAddToExistingContactView(contactShare: ContactShareViewModel, fromViewController: UIViewController) {
|
private func presentSelectAddToExistingContactView(contactShare: ContactShareViewModel, fromViewController: UIViewController) {
|
||||||
guard contactsManager.supportsContactEditing else {
|
guard contactsManager.supportsContactEditing else {
|
||||||
owsFail("\(logTag) Contact editing not supported")
|
owsFail("\(logTag) Contact editing not supported")
|
||||||
|
@ -187,23 +188,26 @@ public class ContactShareViewHelper: NSObject, CNContactViewControllerDelegate {
|
||||||
ContactsViewHelper.presentMissingContactAccessAlertController(from: fromViewController)
|
ContactsViewHelper.presentMissingContactAccessAlertController(from: fromViewController)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
//
|
||||||
// TODO: Revisit this.
|
// // TODO: Revisit this.
|
||||||
guard let firstPhoneNumber = contactShare.e164PhoneNumbers().first else {
|
// guard let firstPhoneNumber = contactShare.e164PhoneNumbers().first else {
|
||||||
owsFail("\(logTag) Missing phone number.")
|
// owsFail("\(logTag) Missing phone number.")
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// TODO: We need to modify OWSAddToContactViewController to take a OWSContact
|
// // TODO: We need to modify OWSAddToContactViewController to take a OWSContact
|
||||||
// and merge it with an existing CNContact.
|
// // and merge it with an existing CNContact.
|
||||||
let viewController = OWSAddToContactViewController()
|
// let viewController = OWSAddToContactViewController()
|
||||||
viewController.configure(withRecipientId: firstPhoneNumber)
|
// viewController.configure(withRecipientId: firstPhoneNumber)
|
||||||
|
|
||||||
guard let navigationController = fromViewController.navigationController else {
|
guard let navigationController = fromViewController.navigationController else {
|
||||||
owsFail("\(logTag) missing navigationController")
|
owsFail("\(logTag) missing navigationController")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let viewController = AddContactShareToExistingContactViewController(contactShare: contactShare)
|
||||||
|
// viewController.addToExistingContactDelegate = fromViewController
|
||||||
|
|
||||||
navigationController.pushViewController(viewController, animated: true)
|
navigationController.pushViewController(viewController, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,10 +20,6 @@ public protocol ContactsPickerDelegate: class {
|
||||||
func contactsPicker(_: ContactsPicker, shouldSelectContact contact: Contact) -> Bool
|
func contactsPicker(_: ContactsPicker, shouldSelectContact contact: Contact) -> Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension ContactsPickerDelegate {
|
|
||||||
func contactsPicker(_: ContactsPicker, shouldSelectContact contact: Contact) -> Bool { return true }
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
public enum SubtitleCellValue: Int {
|
public enum SubtitleCellValue: Int {
|
||||||
case phoneNumber, email, none
|
case phoneNumber, email, none
|
||||||
|
@ -72,15 +68,22 @@ public class ContactsPicker: OWSViewController, UITableViewDelegate, UITableView
|
||||||
|
|
||||||
// Configuration
|
// Configuration
|
||||||
public weak var contactsPickerDelegate: ContactsPickerDelegate?
|
public weak var contactsPickerDelegate: ContactsPickerDelegate?
|
||||||
private let subtitleCellValue: SubtitleCellValue
|
private let subtitleCellType: SubtitleCellValue
|
||||||
private let multiSelectEnabled: Bool
|
private let allowsMultipleSelection: Bool
|
||||||
private let allowedContactKeys: [CNKeyDescriptor] = [
|
private let allowedContactKeys: [CNKeyDescriptor] = ContactsFrameworkContactStoreAdaptee.allowedContactKeys
|
||||||
CNContactFormatter.descriptorForRequiredKeys(for: .fullName),
|
|
||||||
CNContactThumbnailImageDataKey as CNKeyDescriptor,
|
// MARK: - Initializers
|
||||||
CNContactPhoneNumbersKey as CNKeyDescriptor,
|
|
||||||
CNContactEmailAddressesKey as CNKeyDescriptor,
|
@objc
|
||||||
CNContactPostalAddressesKey as CNKeyDescriptor
|
required public init(allowsMultipleSelection: Bool, subtitleCellType: SubtitleCellValue) {
|
||||||
]
|
self.allowsMultipleSelection = allowsMultipleSelection
|
||||||
|
self.subtitleCellType = subtitleCellType
|
||||||
|
super.init(nibName: "ContactsPicker", bundle: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
required public init?(coder aDecoder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Lifecycle Methods
|
// MARK: - Lifecycle Methods
|
||||||
|
|
||||||
|
@ -95,7 +98,7 @@ public class ContactsPicker: OWSViewController, UITableViewDelegate, UITableView
|
||||||
tableView.estimatedRowHeight = 60.0
|
tableView.estimatedRowHeight = 60.0
|
||||||
tableView.rowHeight = UITableViewAutomaticDimension
|
tableView.rowHeight = UITableViewAutomaticDimension
|
||||||
|
|
||||||
tableView.allowsMultipleSelection = multiSelectEnabled
|
tableView.allowsMultipleSelection = allowsMultipleSelection
|
||||||
|
|
||||||
tableView.separatorInset = UIEdgeInsets(top: 0, left: ContactCell.kSeparatorHInset, bottom: 0, right: 16)
|
tableView.separatorInset = UIEdgeInsets(top: 0, left: ContactCell.kSeparatorHInset, bottom: 0, right: 16)
|
||||||
|
|
||||||
|
@ -116,7 +119,7 @@ public class ContactsPicker: OWSViewController, UITableViewDelegate, UITableView
|
||||||
let cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(onTouchCancelButton))
|
let cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(onTouchCancelButton))
|
||||||
self.navigationItem.leftBarButtonItem = cancelButton
|
self.navigationItem.leftBarButtonItem = cancelButton
|
||||||
|
|
||||||
if multiSelectEnabled {
|
if allowsMultipleSelection {
|
||||||
let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(onTouchDoneButton))
|
let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(onTouchDoneButton))
|
||||||
self.navigationItem.rightBarButtonItem = doneButton
|
self.navigationItem.rightBarButtonItem = doneButton
|
||||||
}
|
}
|
||||||
|
@ -126,20 +129,6 @@ public class ContactsPicker: OWSViewController, UITableViewDelegate, UITableView
|
||||||
tableView.register(ContactCell.self, forCellReuseIdentifier: contactCellReuseIdentifier)
|
tableView.register(ContactCell.self, forCellReuseIdentifier: contactCellReuseIdentifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Initializers
|
|
||||||
|
|
||||||
@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) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Contact Operations
|
// MARK: - Contact Operations
|
||||||
|
|
||||||
private func reloadContacts() {
|
private func reloadContacts() {
|
||||||
|
@ -162,7 +151,6 @@ public class ContactsPicker: OWSViewController, UITableViewDelegate, UITableView
|
||||||
let error = NSError(domain: "contactsPickerErrorDomain", code: 1, userInfo: [NSLocalizedDescriptionKey: "No Contacts Access"])
|
let error = NSError(domain: "contactsPickerErrorDomain", code: 1, userInfo: [NSLocalizedDescriptionKey: "No Contacts Access"])
|
||||||
self.contactsPickerDelegate?.contactsPicker(self, contactFetchDidFail: error)
|
self.contactsPickerDelegate?.contactsPicker(self, contactFetchDidFail: error)
|
||||||
errorHandler(error)
|
errorHandler(error)
|
||||||
self.dismiss(animated: true, completion: nil)
|
|
||||||
})
|
})
|
||||||
alert.addAction(cancelAction)
|
alert.addAction(cancelAction)
|
||||||
|
|
||||||
|
@ -231,13 +219,16 @@ public class ContactsPicker: OWSViewController, UITableViewDelegate, UITableView
|
||||||
// MARK: - Table View Delegates
|
// MARK: - Table View Delegates
|
||||||
|
|
||||||
open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: contactCellReuseIdentifier, for: indexPath) as! ContactCell
|
guard let cell = tableView.dequeueReusableCell(withIdentifier: contactCellReuseIdentifier, for: indexPath) as? ContactCell else {
|
||||||
|
owsFail("\(logTag) in \(#function) cell had unexpected type")
|
||||||
|
return UITableViewCell()
|
||||||
|
}
|
||||||
|
|
||||||
let dataSource = filteredSections
|
let dataSource = filteredSections
|
||||||
let cnContact = dataSource[indexPath.section][indexPath.row]
|
let cnContact = dataSource[indexPath.section][indexPath.row]
|
||||||
let contact = Contact(systemContact: cnContact)
|
let contact = Contact(systemContact: cnContact)
|
||||||
|
|
||||||
cell.configure(contact: contact, subtitleType: subtitleCellValue, contactsManager: self.contactsManager)
|
cell.configure(contact: contact, subtitleType: subtitleCellType, showsWhenSelected: self.allowsMultipleSelection, contactsManager: self.contactsManager)
|
||||||
let isSelected = selectedContacts.contains(where: { $0.uniqueId == contact.uniqueId })
|
let isSelected = selectedContacts.contains(where: { $0.uniqueId == contact.uniqueId })
|
||||||
cell.isSelected = isSelected
|
cell.isSelected = isSelected
|
||||||
|
|
||||||
|
@ -274,13 +265,11 @@ public class ContactsPicker: OWSViewController, UITableViewDelegate, UITableView
|
||||||
|
|
||||||
selectedContacts.append(selectedContact)
|
selectedContacts.append(selectedContact)
|
||||||
|
|
||||||
if !multiSelectEnabled {
|
if !allowsMultipleSelection {
|
||||||
//Single selection code
|
// Single selection code
|
||||||
self.dismiss(animated: true) {
|
|
||||||
self.contactsPickerDelegate?.contactsPicker(self, didSelectContact: selectedContact)
|
self.contactsPickerDelegate?.contactsPicker(self, didSelectContact: selectedContact)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
open func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
|
open func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
|
||||||
return collation.section(forSectionIndexTitle: index)
|
return collation.section(forSectionIndexTitle: index)
|
||||||
|
@ -313,12 +302,10 @@ public class ContactsPicker: OWSViewController, UITableViewDelegate, UITableView
|
||||||
|
|
||||||
func onTouchCancelButton() {
|
func onTouchCancelButton() {
|
||||||
contactsPickerDelegate?.contactsPickerDidCancel(self)
|
contactsPickerDelegate?.contactsPickerDidCancel(self)
|
||||||
dismiss(animated: true, completion: nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func onTouchDoneButton() {
|
func onTouchDoneButton() {
|
||||||
contactsPickerDelegate?.contactsPicker(self, didSelectMultipleContacts: selectedContacts)
|
contactsPickerDelegate?.contactsPicker(self, didSelectMultipleContacts: selectedContacts)
|
||||||
dismiss(animated: true, completion: nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Search Actions
|
// MARK: - Search Actions
|
||||||
|
|
|
@ -2571,7 +2571,8 @@ typedef enum : NSUInteger {
|
||||||
- (void)chooseContactForSending
|
- (void)chooseContactForSending
|
||||||
{
|
{
|
||||||
ContactsPicker *contactsPicker =
|
ContactsPicker *contactsPicker =
|
||||||
[[ContactsPicker alloc] initWithDelegate:self multiSelection:NO subtitleCellType:SubtitleCellValueNone];
|
[[ContactsPicker alloc] initWithAllowsMultipleSelection:NO subtitleCellType:SubtitleCellValueNone];
|
||||||
|
contactsPicker.contactsPickerDelegate = self;
|
||||||
contactsPicker.title
|
contactsPicker.title
|
||||||
= NSLocalizedString(@"CONTACT_PICKER_TITLE", @"navbar title for contact picker when sharing a contact");
|
= NSLocalizedString(@"CONTACT_PICKER_TITLE", @"navbar title for contact picker when sharing a contact");
|
||||||
|
|
||||||
|
@ -4978,11 +4979,13 @@ interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransiti
|
||||||
- (void)contactsPickerDidCancel:(ContactsPicker *)contactsPicker
|
- (void)contactsPickerDidCancel:(ContactsPicker *)contactsPicker
|
||||||
{
|
{
|
||||||
DDLogDebug(@"%@ in %s", self.logTag, __PRETTY_FUNCTION__);
|
DDLogDebug(@"%@ in %s", self.logTag, __PRETTY_FUNCTION__);
|
||||||
|
[self dismissViewControllerAnimated:YES completion:nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)contactsPicker:(ContactsPicker *)contactsPicker contactFetchDidFail:(NSError *)error
|
- (void)contactsPicker:(ContactsPicker *)contactsPicker contactFetchDidFail:(NSError *)error
|
||||||
{
|
{
|
||||||
DDLogDebug(@"%@ in %s with error %@", self.logTag, __PRETTY_FUNCTION__, error);
|
DDLogDebug(@"%@ in %s with error %@", self.logTag, __PRETTY_FUNCTION__, error);
|
||||||
|
[self dismissViewControllerAnimated:YES completion:nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)contactsPicker:(ContactsPicker *)contactsPicker didSelectContact:(Contact *)contact
|
- (void)contactsPicker:(ContactsPicker *)contactsPicker didSelectContact:(Contact *)contact
|
||||||
|
@ -4992,6 +4995,8 @@ interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransiti
|
||||||
|
|
||||||
DDLogDebug(@"%@ in %s with contact: %@", self.logTag, __PRETTY_FUNCTION__, contact);
|
DDLogDebug(@"%@ in %s with contact: %@", self.logTag, __PRETTY_FUNCTION__, contact);
|
||||||
|
|
||||||
|
[self dismissViewControllerAnimated:YES completion:nil];
|
||||||
|
|
||||||
OWSContact *_Nullable contactShareRecord = [OWSContacts contactForSystemContact:contact.cnContact];
|
OWSContact *_Nullable contactShareRecord = [OWSContacts contactForSystemContact:contact.cnContact];
|
||||||
if (!contactShareRecord) {
|
if (!contactShareRecord) {
|
||||||
DDLogError(@"%@ Could not convert system contact.", self.logTag);
|
DDLogError(@"%@ Could not convert system contact.", self.logTag);
|
||||||
|
@ -5029,6 +5034,7 @@ interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransiti
|
||||||
- (void)contactsPicker:(ContactsPicker *)contactsPicker didSelectMultipleContacts:(NSArray<Contact *> *)contacts
|
- (void)contactsPicker:(ContactsPicker *)contactsPicker didSelectMultipleContacts:(NSArray<Contact *> *)contacts
|
||||||
{
|
{
|
||||||
OWSFail(@"%@ in %s with contacts: %@", self.logTag, __PRETTY_FUNCTION__, contacts);
|
OWSFail(@"%@ in %s with contacts: %@", self.logTag, __PRETTY_FUNCTION__, contacts);
|
||||||
|
[self dismissViewControllerAnimated:YES completion:nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL)contactsPicker:(ContactsPicker *)contactsPicker shouldSelectContact:(Contact *)contact
|
- (BOOL)contactsPicker:(ContactsPicker *)contactsPicker shouldSelectContact:(Contact *)contact
|
||||||
|
|
|
@ -88,6 +88,8 @@ class InviteFlow: NSObject, MFMessageComposeViewControllerDelegate, MFMailCompos
|
||||||
func contactsPicker(_: ContactsPicker, didSelectMultipleContacts contacts: [Contact]) {
|
func contactsPicker(_: ContactsPicker, didSelectMultipleContacts contacts: [Contact]) {
|
||||||
Logger.debug("\(TAG) didSelectContacts:\(contacts)")
|
Logger.debug("\(TAG) didSelectContacts:\(contacts)")
|
||||||
|
|
||||||
|
self.presentingViewController.dismiss(animated: true)
|
||||||
|
|
||||||
guard let inviteChannel = channel else {
|
guard let inviteChannel = channel else {
|
||||||
Logger.error("\(TAG) unexpected nil channel after returning from contact picker.")
|
Logger.error("\(TAG) unexpected nil channel after returning from contact picker.")
|
||||||
return
|
return
|
||||||
|
@ -124,14 +126,17 @@ class InviteFlow: NSObject, MFMessageComposeViewControllerDelegate, MFMailCompos
|
||||||
|
|
||||||
func contactsPicker(_: ContactsPicker, contactFetchDidFail error: NSError) {
|
func contactsPicker(_: ContactsPicker, contactFetchDidFail error: NSError) {
|
||||||
Logger.error("\(self.logTag) in \(#function) with error: \(error)")
|
Logger.error("\(self.logTag) in \(#function) with error: \(error)")
|
||||||
|
self.presentingViewController.dismiss(animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func contactsPickerDidCancel(_: ContactsPicker) {
|
func contactsPickerDidCancel(_: ContactsPicker) {
|
||||||
Logger.debug("\(self.logTag) in \(#function)")
|
Logger.debug("\(self.logTag) in \(#function)")
|
||||||
|
self.presentingViewController.dismiss(animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func contactsPicker(_: ContactsPicker, didSelectContact contact: Contact) {
|
func contactsPicker(_: ContactsPicker, didSelectContact contact: Contact) {
|
||||||
owsFail("\(logTag) in \(#function) InviteFlow only supports multi-select")
|
owsFail("\(logTag) in \(#function) InviteFlow only supports multi-select")
|
||||||
|
self.presentingViewController.dismiss(animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: SMS
|
// MARK: SMS
|
||||||
|
@ -146,7 +151,8 @@ class InviteFlow: NSObject, MFMessageComposeViewControllerDelegate, MFMailCompos
|
||||||
return UIAlertAction(title: messageTitle, style: .default) { _ in
|
return UIAlertAction(title: messageTitle, style: .default) { _ in
|
||||||
Logger.debug("\(self.TAG) Chose message.")
|
Logger.debug("\(self.TAG) Chose message.")
|
||||||
self.channel = .message
|
self.channel = .message
|
||||||
let picker = ContactsPicker(delegate: self, multiSelection: true, subtitleCellType: .phoneNumber)
|
let picker = ContactsPicker(allowsMultipleSelection: true, subtitleCellType: .phoneNumber)
|
||||||
|
picker.contactsPickerDelegate = self
|
||||||
picker.title = NSLocalizedString("INVITE_FRIENDS_PICKER_TITLE", comment: "Navbar title")
|
picker.title = NSLocalizedString("INVITE_FRIENDS_PICKER_TITLE", comment: "Navbar title")
|
||||||
let navigationController = UINavigationController(rootViewController: picker)
|
let navigationController = UINavigationController(rootViewController: picker)
|
||||||
self.presentingViewController.present(navigationController, animated: true)
|
self.presentingViewController.present(navigationController, animated: true)
|
||||||
|
@ -209,7 +215,8 @@ class InviteFlow: NSObject, MFMessageComposeViewControllerDelegate, MFMailCompos
|
||||||
Logger.debug("\(self.TAG) Chose mail.")
|
Logger.debug("\(self.TAG) Chose mail.")
|
||||||
self.channel = .mail
|
self.channel = .mail
|
||||||
|
|
||||||
let picker = ContactsPicker(delegate: self, multiSelection: true, subtitleCellType: .email)
|
let picker = ContactsPicker(allowsMultipleSelection: true, subtitleCellType: .email)
|
||||||
|
picker.contactsPickerDelegate = self
|
||||||
picker.title = NSLocalizedString("INVITE_FRIENDS_PICKER_TITLE", comment: "Navbar title")
|
picker.title = NSLocalizedString("INVITE_FRIENDS_PICKER_TITLE", comment: "Navbar title")
|
||||||
let navigationController = UINavigationController(rootViewController: picker)
|
let navigationController = UINavigationController(rootViewController: picker)
|
||||||
self.presentingViewController.present(navigationController, animated: true)
|
self.presentingViewController.present(navigationController, animated: true)
|
||||||
|
|
|
@ -19,6 +19,7 @@ class ContactCell: UITableViewCell {
|
||||||
var subtitleLabel: UILabel
|
var subtitleLabel: UILabel
|
||||||
|
|
||||||
var contact: Contact?
|
var contact: Contact?
|
||||||
|
var showsWhenSelected: Bool = false
|
||||||
|
|
||||||
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
|
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
|
||||||
self.contactImageView = AvatarImageView()
|
self.contactImageView = AvatarImageView()
|
||||||
|
@ -59,16 +60,19 @@ class ContactCell: UITableViewCell {
|
||||||
|
|
||||||
override func setSelected(_ selected: Bool, animated: Bool) {
|
override func setSelected(_ selected: Bool, animated: Bool) {
|
||||||
super.setSelected(selected, animated: animated)
|
super.setSelected(selected, animated: animated)
|
||||||
|
if showsWhenSelected {
|
||||||
accessoryType = selected ? .checkmark : .none
|
accessoryType = selected ? .checkmark : .none
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func didChangePreferredContentSize() {
|
func didChangePreferredContentSize() {
|
||||||
self.titleLabel.font = UIFont.ows_dynamicTypeBody
|
self.titleLabel.font = UIFont.ows_dynamicTypeBody
|
||||||
self.subtitleLabel.font = UIFont.ows_dynamicTypeSubheadline
|
self.subtitleLabel.font = UIFont.ows_dynamicTypeSubheadline
|
||||||
}
|
}
|
||||||
|
|
||||||
func configure(contact: Contact, subtitleType: SubtitleCellValue, contactsManager: OWSContactsManager) {
|
func configure(contact: Contact, subtitleType: SubtitleCellValue, showsWhenSelected: Bool, contactsManager: OWSContactsManager) {
|
||||||
self.contact = contact
|
self.contact = contact
|
||||||
|
self.showsWhenSelected = showsWhenSelected
|
||||||
|
|
||||||
titleLabel.attributedText = contact.cnContact?.formattedFullName(font: titleLabel.font)
|
titleLabel.attributedText = contact.cnContact?.formattedFullName(font: titleLabel.font)
|
||||||
updateSubtitle(subtitleType: subtitleType, contact: contact)
|
updateSubtitle(subtitleType: subtitleType, contact: contact)
|
||||||
|
|
|
@ -109,6 +109,65 @@ public class ContactShareViewModel: NSObject {
|
||||||
return dbRecord.isProfileAvatar
|
return dbRecord.isProfileAvatar
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func cnContact(mergedWithExistingContact existingContact: Contact) -> CNContact? {
|
||||||
|
|
||||||
|
guard let mergedCNContact: CNMutableContact = existingContact.cnContact?.mutableCopy() as? CNMutableContact else {
|
||||||
|
owsFail("\(logTag) in \(#function) mergedCNContact was unexpectedly nil")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let newCNContact = OWSContacts.systemContact(for: self.dbRecord) else {
|
||||||
|
owsFail("\(logTag) in \(#function) newCNContact was unexpectedly nil")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let normalize = { (input: String) -> String in
|
||||||
|
return (input as NSString).ows_stripped()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO display name - but only if existing is blank
|
||||||
|
|
||||||
|
var existingPhoneNumberSet: Set<String> = Set()
|
||||||
|
mergedCNContact.phoneNumbers.map { $0.value }.forEach { (existingPhoneNumber: CNPhoneNumber) in
|
||||||
|
// compare e164?
|
||||||
|
let normalizedValue = normalize(existingPhoneNumber.stringValue as String)
|
||||||
|
existingPhoneNumberSet.insert(normalizedValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
newCNContact.phoneNumbers.forEach { (newLabeledPhoneNumber: CNLabeledValue<CNPhoneNumber>) in
|
||||||
|
let normalizedValue = normalize(newLabeledPhoneNumber.value.stringValue as String)
|
||||||
|
guard !existingPhoneNumberSet.contains(normalizedValue) else {
|
||||||
|
Logger.debug("\(self.logTag) in \(#function) ignoring matching phone number: \(normalizedValue)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.debug("\(self.logTag) in \(#function) adding new phone number: \(normalizedValue)")
|
||||||
|
mergedCNContact.phoneNumbers.append(newLabeledPhoneNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
// // TODO emails
|
||||||
|
// var existingEmailSet: Set<String> = Set()
|
||||||
|
// mergedCNContact.emailAddresses.forEach { (existingEmail: CNLabeledValue<NSString>) in
|
||||||
|
// existingEmailSet.insert(normalize(existingEmail.value as String))
|
||||||
|
// }
|
||||||
|
|
||||||
|
// TODO address
|
||||||
|
|
||||||
|
// newCNContact.emailAddresses.forEach { newEmail in
|
||||||
|
// let match = mergedCNContact.emailAddresses.first { existingEmail in
|
||||||
|
// return normalize(existingEmail) == normalize(newEmail)
|
||||||
|
// }
|
||||||
|
// if match == nil {
|
||||||
|
// mergedCNContact.emailAddresses.add
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
return mergedCNContact.copy() as? CNContact
|
||||||
|
}
|
||||||
|
|
||||||
public func copy(withNamePrefix namePrefix: String?,
|
public func copy(withNamePrefix namePrefix: String?,
|
||||||
givenName: String?,
|
givenName: String?,
|
||||||
middleName: String?,
|
middleName: String?,
|
||||||
|
|
|
@ -308,7 +308,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
- (void)presentContactViewControllerForRecipientId:(NSString *)recipientId
|
- (void)presentContactViewControllerForRecipientId:(NSString *)recipientId
|
||||||
fromViewController:(UIViewController<ContactEditingDelegate> *)fromViewController
|
fromViewController:(UIViewController<ContactEditingDelegate> *)fromViewController
|
||||||
editImmediately:(BOOL)shouldEditImmediately
|
editImmediately:(BOOL)shouldEditImmediately
|
||||||
addToExistingCnContact:(CNContact *_Nullable)addToExistingCnContact
|
addToExistingCnContact:(CNContact *_Nullable)existingContact
|
||||||
{
|
{
|
||||||
SignalAccount *signalAccount = [self signalAccountForRecipientId:recipientId];
|
SignalAccount *signalAccount = [self signalAccountForRecipientId:recipientId];
|
||||||
|
|
||||||
|
@ -325,8 +325,8 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
CNContactViewController *_Nullable contactViewController;
|
CNContactViewController *_Nullable contactViewController;
|
||||||
CNContact *_Nullable cnContact = nil;
|
CNContact *_Nullable cnContact = nil;
|
||||||
if (addToExistingCnContact) {
|
if (existingContact) {
|
||||||
CNMutableContact *updatedContact = [addToExistingCnContact mutableCopy];
|
CNMutableContact *updatedContact = [existingContact mutableCopy];
|
||||||
NSMutableArray<CNLabeledValue *> *phoneNumbers
|
NSMutableArray<CNLabeledValue *> *phoneNumbers
|
||||||
= (updatedContact.phoneNumbers ? [updatedContact.phoneNumbers mutableCopy] : [NSMutableArray new]);
|
= (updatedContact.phoneNumbers ? [updatedContact.phoneNumbers mutableCopy] : [NSMutableArray new]);
|
||||||
// Only add recipientId as a phone number for the existing contact
|
// Only add recipientId as a phone number for the existing contact
|
||||||
|
|
|
@ -20,6 +20,7 @@ protocol ContactStoreAdaptee {
|
||||||
func startObservingChanges(changeHandler: @escaping () -> Void)
|
func startObservingChanges(changeHandler: @escaping () -> Void)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public
|
||||||
class ContactsFrameworkContactStoreAdaptee: ContactStoreAdaptee {
|
class ContactsFrameworkContactStoreAdaptee: ContactStoreAdaptee {
|
||||||
let TAG = "[ContactsFrameworkContactStoreAdaptee]"
|
let TAG = "[ContactsFrameworkContactStoreAdaptee]"
|
||||||
private let contactStore = CNContactStore()
|
private let contactStore = CNContactStore()
|
||||||
|
@ -29,7 +30,7 @@ class ContactsFrameworkContactStoreAdaptee: ContactStoreAdaptee {
|
||||||
|
|
||||||
let supportsContactEditing = true
|
let supportsContactEditing = true
|
||||||
|
|
||||||
private let allowedContactKeys: [CNKeyDescriptor] = [
|
public static let allowedContactKeys: [CNKeyDescriptor] = [
|
||||||
CNContactFormatter.descriptorForRequiredKeys(for: .fullName),
|
CNContactFormatter.descriptorForRequiredKeys(for: .fullName),
|
||||||
CNContactThumbnailImageDataKey as CNKeyDescriptor, // TODO full image instead of thumbnail?
|
CNContactThumbnailImageDataKey as CNKeyDescriptor, // TODO full image instead of thumbnail?
|
||||||
CNContactPhoneNumbersKey as CNKeyDescriptor,
|
CNContactPhoneNumbersKey as CNKeyDescriptor,
|
||||||
|
@ -92,7 +93,7 @@ class ContactsFrameworkContactStoreAdaptee: ContactStoreAdaptee {
|
||||||
func fetchContacts() -> Result<[Contact], Error> {
|
func fetchContacts() -> Result<[Contact], Error> {
|
||||||
var systemContacts = [CNContact]()
|
var systemContacts = [CNContact]()
|
||||||
do {
|
do {
|
||||||
let contactFetchRequest = CNContactFetchRequest(keysToFetch: self.allowedContactKeys)
|
let contactFetchRequest = CNContactFetchRequest(keysToFetch: ContactsFrameworkContactStoreAdaptee.allowedContactKeys)
|
||||||
contactFetchRequest.sortOrder = .userDefault
|
contactFetchRequest.sortOrder = .userDefault
|
||||||
try self.contactStore.enumerateContacts(with: contactFetchRequest) { (contact, _) -> Void in
|
try self.contactStore.enumerateContacts(with: contactFetchRequest) { (contact, _) -> Void in
|
||||||
systemContacts.append(contact)
|
systemContacts.append(contact)
|
||||||
|
|
Loading…
Reference in a new issue