Merge branch 'charlesmchen/contactView'

pull/1/head
Matthew Chen 7 years ago
commit ed7fe90e15

@ -230,6 +230,7 @@
34E3E5681EC4B19400495BAC /* AudioProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34E3E5671EC4B19400495BAC /* AudioProgressView.swift */; };
34E3EF0D1EFC235B007F6822 /* DebugUIDiskUsage.m in Sources */ = {isa = PBXBuildFile; fileRef = 34E3EF0C1EFC235B007F6822 /* DebugUIDiskUsage.m */; };
34E3EF101EFC2684007F6822 /* DebugUIPage.m in Sources */ = {isa = PBXBuildFile; fileRef = 34E3EF0F1EFC2684007F6822 /* DebugUIPage.m */; };
34E88D262098C5AE00A608F4 /* ContactViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34E88D252098C5AE00A608F4 /* ContactViewController.swift */; };
34E8A8D12085238A00B272B1 /* ProtoParsingTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 34E8A8D02085238900B272B1 /* ProtoParsingTest.m */; };
34F308A21ECB469700BB7697 /* OWSBezierPathView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34F308A11ECB469700BB7697 /* OWSBezierPathView.m */; };
34FD93701E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 34FD936F1E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.m */; };
@ -881,6 +882,7 @@
34E3EF0C1EFC235B007F6822 /* DebugUIDiskUsage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugUIDiskUsage.m; sourceTree = "<group>"; };
34E3EF0E1EFC2684007F6822 /* DebugUIPage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUIPage.h; sourceTree = "<group>"; };
34E3EF0F1EFC2684007F6822 /* DebugUIPage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugUIPage.m; sourceTree = "<group>"; };
34E88D252098C5AE00A608F4 /* ContactViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactViewController.swift; sourceTree = "<group>"; };
34E8A8D02085238900B272B1 /* ProtoParsingTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ProtoParsingTest.m; sourceTree = "<group>"; };
34F308A01ECB469700BB7697 /* OWSBezierPathView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBezierPathView.h; sourceTree = "<group>"; };
34F308A11ECB469700BB7697 /* OWSBezierPathView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBezierPathView.m; sourceTree = "<group>"; };
@ -1624,6 +1626,7 @@
34B3F83B1E8DF1700035BE1A /* CallViewController.swift */,
34B3F83E1E8DF1700035BE1A /* ContactsPicker.swift */,
34B3F83F1E8DF1700035BE1A /* ContactsPicker.xib */,
34E88D252098C5AE00A608F4 /* ContactViewController.swift */,
3448BFC01EDF0EA7005B2D69 /* ConversationView */,
346B66301F4E29B200E5122F /* CropScaleImageViewController.swift */,
34D8C0221ED3673300188D7C /* DebugUI */,
@ -1631,6 +1634,7 @@
34BECE2C1F7ABCE000D7438D /* GifPicker */,
34386A4C207D0C01009F5D9C /* HomeView */,
34B3F84C1E8DF1700035BE1A /* InviteFlow.swift */,
3496744E2076ACCE00080B5F /* LongTextViewController.swift */,
45B9EE9A200E91FB005D2F2D /* MediaDetailViewController.h */,
45B9EE9B200E91FB005D2F2D /* MediaDetailViewController.m */,
452EC6DE205E9E30000E787C /* MediaGalleryViewController.swift */,
@ -1641,7 +1645,6 @@
34B3F8501E8DF1700035BE1A /* NewContactThreadViewController.m */,
34B3F8541E8DF1700035BE1A /* NewGroupViewController.h */,
34B3F8551E8DF1700035BE1A /* NewGroupViewController.m */,
3496744E2076ACCE00080B5F /* LongTextViewController.swift */,
34A55F3620485464002CC6DE /* OWS2FARegistrationViewController.h */,
34A55F3520485464002CC6DE /* OWS2FARegistrationViewController.m */,
45D2AC01204885170033C692 /* OWS2FAReminderViewController.swift */,
@ -3255,6 +3258,7 @@
34D1F0841F8678AA0066283D /* ConversationInputToolbar.m in Sources */,
457F671B20746193000EABCD /* QuotedReplyPreview.swift in Sources */,
34DBF004206BD5A500025978 /* OWSBubbleView.m in Sources */,
34E88D262098C5AE00A608F4 /* ContactViewController.swift in Sources */,
FCC81A981A44558300DFEC7D /* UIDevice+TSHardwareVersion.m in Sources */,
76EB054018170B33006006FC /* AppDelegate.m in Sources */,
34D1F0831F8678AA0066283D /* ConversationInputTextView.m in Sources */,

@ -16,6 +16,7 @@
#import "MediaDetailViewController.h"
#import "NotificationSettingsViewController.h"
#import "NotificationsManager.h"
#import "OWSAddToContactViewController.h"
#import "OWSAnyTouchGestureRecognizer.h"
#import "OWSAudioPlayer.h"
#import "OWSBackup.h"

@ -0,0 +1,661 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
import Foundation
import SignalServiceKit
import SignalMessaging
import Reachability
import ContactsUI
import MessageUI
class TappableView: UIView {
let actionBlock : (() -> Void)
// MARK: - Initializers
@available(*, unavailable, message: "use init(call:) constructor instead.")
required init?(coder aDecoder: NSCoder) {
fatalError("Unimplemented")
}
required init(actionBlock : @escaping () -> Void) {
self.actionBlock = actionBlock
super.init(frame: CGRect.zero)
self.isUserInteractionEnabled = true
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(wasTapped)))
}
func wasTapped(sender: UIGestureRecognizer) {
Logger.info("\(logTag) \(#function)")
guard sender.state == .recognized else {
return
}
actionBlock()
}
}
// MARK: -
class ContactViewController: OWSViewController, CNContactViewControllerDelegate {
enum ContactViewMode {
case systemContactWithSignal,
systemContactWithoutSignal,
nonSystemContact,
noPhoneNumber,
unknown
}
private var hasLoadedView = false
private var viewMode = ContactViewMode.unknown {
didSet {
SwiftAssertIsOnMainThread(#function)
if oldValue != viewMode && hasLoadedView {
updateContent()
}
}
}
let contactsManager: OWSContactsManager
var reachability: Reachability?
private let contact: OWSContact
// MARK: - Initializers
@available(*, unavailable, message: "use init(call:) constructor instead.")
required init?(coder aDecoder: NSCoder) {
fatalError("Unimplemented")
}
required init(contact: OWSContact) {
contactsManager = Environment.current().contactsManager
self.contact = contact
self.scrollView = UIScrollView()
super.init(nibName: nil, bundle: nil)
updateMode()
NotificationCenter.default.addObserver(forName: .OWSContactsManagerSignalAccountsDidChange, object: nil, queue: nil) { [weak self] _ in
guard let strongSelf = self else { return }
strongSelf.updateMode()
}
reachability = Reachability.forInternetConnection()
NotificationCenter.default.addObserver(forName: .reachabilityChanged, object: nil, queue: nil) { [weak self] _ in
guard let strongSelf = self else { return }
strongSelf.updateMode()
}
}
// MARK: - View Lifecycle
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
UIUtil.applySignalAppearence()
contactsManager.requestSystemContactsOnce(completion: { [weak self] _ in
guard let strongSelf = self else { return }
strongSelf.updateMode()
})
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIUtil.applySignalAppearence()
}
private var scrollView: UIScrollView
override func loadView() {
super.loadView()
self.view.addSubview(scrollView)
scrollView.layoutMargins = .zero
scrollView.autoPinWidthToSuperview()
scrollView.autoPin(toTopLayoutGuideOf: self, withInset: 0)
scrollView.autoPin(toBottomLayoutGuideOf: self, withInset: 0)
self.view.backgroundColor = UIColor.white
updateContent()
hasLoadedView = true
}
private func updateMode() {
SwiftAssertIsOnMainThread(#function)
guard phoneNumbersForContact().count > 0 else {
viewMode = .noPhoneNumber
return
}
if systemContactsWithSignalAccountsForContact().count > 0 {
viewMode = .systemContactWithSignal
return
}
if systemContactsForContact().count > 0 {
viewMode = .systemContactWithoutSignal
return
}
viewMode = .nonSystemContact
}
private func systemContactsWithSignalAccountsForContact() -> [String] {
SwiftAssertIsOnMainThread(#function)
return phoneNumbersForContact().filter({ (phoneNumber) -> Bool in
return contactsManager.hasSignalAccount(forRecipientId: phoneNumber)
})
}
private func systemContactsForContact() -> [String] {
SwiftAssertIsOnMainThread(#function)
return phoneNumbersForContact().filter({ (phoneNumber) -> Bool in
return contactsManager.allContactsMap[phoneNumber] != nil
})
}
private func phoneNumbersForContact() -> [String] {
SwiftAssertIsOnMainThread(#function)
var result = [String]()
for phoneNumber in contact.phoneNumbers {
result.append(phoneNumber.phoneNumber)
}
return result
}
private func updateContent() {
SwiftAssertIsOnMainThread(#function)
let rootView = self.scrollView
for subview in rootView.subviews {
subview.removeFromSuperview()
}
// TODO: The design calls for no navigation bar, just a back button.
let topView = UIView.container()
topView.backgroundColor = UIColor(rgbHex: 0xefeff4)
topView.preservesSuperviewLayoutMargins = true
rootView.addSubview(topView)
topView.autoPinEdge(toSuperviewEdge: .top)
topView.autoPinEdge(.left, to: .left, of: self.view)
topView.autoPinEdge(.right, to: .right, of: self.view)
// TODO: Use actual avatar.
let avatarSize = CGFloat(100)
let avatarView = UIView.container()
avatarView.backgroundColor = UIColor.ows_materialBlue
avatarView.layer.cornerRadius = avatarSize * 0.5
topView.addSubview(avatarView)
avatarView.autoPin(toTopLayoutGuideOf: self, withInset: 20)
avatarView.autoHCenterInSuperview()
avatarView.autoSetDimension(.width, toSize: avatarSize)
avatarView.autoSetDimension(.height, toSize: avatarSize)
let nameLabel = UILabel()
nameLabel.text = contact.displayName
nameLabel.font = UIFont.ows_dynamicTypeTitle2.ows_bold()
nameLabel.textColor = UIColor.black
nameLabel.lineBreakMode = .byTruncatingTail
nameLabel.textAlignment = .center
topView.addSubview(nameLabel)
nameLabel.autoPinEdge(.top, to: .bottom, of: avatarView, withOffset: 10)
nameLabel.autoPinLeadingToSuperviewMargin(withInset: hMargin)
nameLabel.autoPinTrailingToSuperviewMargin(withInset: hMargin)
var lastView: UIView = nameLabel
if let firstPhoneNumber = contact.phoneNumbers.first {
let phoneNumberLabel = UILabel()
phoneNumberLabel.text = firstPhoneNumber.phoneNumber
phoneNumberLabel.font = UIFont.ows_dynamicTypeCaption2
phoneNumberLabel.textColor = UIColor.black
phoneNumberLabel.lineBreakMode = .byTruncatingTail
phoneNumberLabel.textAlignment = .center
topView.addSubview(phoneNumberLabel)
phoneNumberLabel.autoPinEdge(.top, to: .bottom, of: lastView, withOffset: 5)
phoneNumberLabel.autoPinLeadingToSuperviewMargin(withInset: hMargin)
phoneNumberLabel.autoPinTrailingToSuperviewMargin(withInset: hMargin)
lastView = phoneNumberLabel
}
switch viewMode {
case .systemContactWithSignal:
// Show actions buttons for system contacts with a Signal account.
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.distribution = .fillEqually
stackView.addArrangedSubview(createCircleActionButton(text: NSLocalizedString("ACTION_SEND_MESSAGE",
comment: "Label for 'sent message' button in contact view."),
actionBlock: { [weak self] _ in
guard let strongSelf = self else { return }
strongSelf.didPressSendMessage()
}))
stackView.addArrangedSubview(createCircleActionButton(text: NSLocalizedString("ACTION_AUDIO_CALL",
comment: "Label for 'audio call' button in contact view."),
actionBlock: { [weak self] _ in
guard let strongSelf = self else { return }
strongSelf.didPressAudioCall()
}))
stackView.addArrangedSubview(createCircleActionButton(text: NSLocalizedString("ACTION_VIDEO_CALL",
comment: "Label for 'video call' button in contact view."),
actionBlock: { [weak self] _ in
guard let strongSelf = self else { return }
strongSelf.didPressVideoCall()
}))
topView.addSubview(stackView)
stackView.autoPinEdge(.top, to: .bottom, of: lastView, withOffset: 20)
stackView.autoPinLeadingToSuperviewMargin(withInset: hMargin)
stackView.autoPinTrailingToSuperviewMargin(withInset: hMargin)
lastView = stackView
case .systemContactWithoutSignal:
// Show invite button for system contacts without a Signal account.
let inviteButton = createLargePillButton(text: NSLocalizedString("ACTION_INVITE",
comment: "Label for 'invite' button in contact view."),
actionBlock: { [weak self] _ in
guard let strongSelf = self else { return }
strongSelf.didPressInvite()
})
topView.addSubview(inviteButton)
inviteButton.autoPinEdge(.top, to: .bottom, of: lastView, withOffset: 20)
inviteButton.autoPinLeadingToSuperviewMargin(withInset: 55)
inviteButton.autoPinTrailingToSuperviewMargin(withInset: 55)
lastView = inviteButton
case .nonSystemContact:
// Show no action buttons for contacts not in user's device contacts.
break
case .noPhoneNumber:
// Show no action buttons for contacts without a phone number.
break
case .unknown:
let activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .whiteLarge)
topView.addSubview(activityIndicator)
activityIndicator.autoPinEdge(.top, to: .bottom, of: lastView, withOffset: 10)
activityIndicator.autoHCenterInSuperview()
lastView = activityIndicator
break
}
lastView.autoPinEdge(toSuperviewEdge: .bottom, withInset: 15)
let bottomView = UIView.container()
bottomView.backgroundColor = UIColor.white
bottomView.layoutMargins = .zero
bottomView.preservesSuperviewLayoutMargins = false
rootView.addSubview(bottomView)
bottomView.autoPinEdge(.top, to: .bottom, of: topView)
bottomView.autoPinEdge(toSuperviewEdge: .bottom)
bottomView.autoPinEdge(.left, to: .left, of: self.view)
bottomView.autoPinEdge(.right, to: .right, of: self.view)
bottomView.setContentHuggingVerticalLow()
var lastRow: UIView?
let addSpacerRow = {
guard let prevRow = lastRow else {
owsFail("\(self.logTag) missing last row")
return
}
let row = UIView()
row.backgroundColor = UIColor(rgbHex: 0xdedee1)
bottomView.addSubview(row)
row.autoSetDimension(.height, toSize: 1)
row.autoPinLeadingToSuperviewMargin(withInset: self.hMargin)
row.autoPinTrailingToSuperviewMargin()
row.autoPinEdge(.top, to: .bottom, of: prevRow)
lastRow = row
}
let addRow: ((UIView) -> Void) = { (row) in
if lastRow != nil {
addSpacerRow()
}
bottomView.addSubview(row)
row.autoPinLeadingToSuperviewMargin()
row.autoPinTrailingToSuperviewMargin()
if let lastRow = lastRow {
row.autoPinEdge(.top, to: .bottom, of: lastRow)
} else {
row.autoPinEdge(toSuperviewEdge: .top)
}
lastRow = row
}
if viewMode == .nonSystemContact {
addRow(createActionRow(labelText: NSLocalizedString("CONVERSATION_SETTINGS_NEW_CONTACT",
comment: "Label for 'new contact' button in conversation settings view."),
action: #selector(didPressCreateNewContact)))
addRow(createActionRow(labelText: NSLocalizedString("CONVERSATION_SETTINGS_ADD_TO_EXISTING_CONTACT",
comment: "Label for 'new contact' button in conversation settings view."),
action: #selector(didPressAddToExistingContact)))
}
// TODO: Not designed yet.
// if viewMode == .systemContactWithSignal ||
// viewMode == .systemContactWithoutSignal {
// addRow(createActionRow(labelText:NSLocalizedString("ACTION_SHARE_CONTACT",
// comment:"Label for 'share contact' button."),
// action:#selector(didPressShareContact)))
// }
for phoneNumber in contact.phoneNumbers {
// TODO: Try to format the phone number nicely.
addRow(createNameValueRow(name: phoneNumber.labelString(),
value: phoneNumber.phoneNumber,
actionBlock: {
guard let url = NSURL(string: "tel:\(phoneNumber.phoneNumber)") else {
owsFail("\(ContactViewController.logTag) could not open phone number.")
return
}
UIApplication.shared.openURL(url as URL)
}))
}
for email in contact.emails {
addRow(createNameValueRow(name: email.labelString(),
value: email.email,
actionBlock: {
guard let url = NSURL(string: "mailto:\(email.email)") else {
owsFail("\(ContactViewController.logTag) could not open email.")
return
}
UIApplication.shared.openURL(url as URL)
}))
}
// TODO: Should we present addresses here too? How?
lastRow?.autoPinEdge(toSuperviewEdge: .bottom)
}
private let hMargin = CGFloat(16)
private func createActionRow(labelText: String, action: Selector) -> UIView {
let row = UIView()
row.layoutMargins.left = 0
row.layoutMargins.right = 0
row.isUserInteractionEnabled = true
row.addGestureRecognizer(UITapGestureRecognizer(target: self, action: action))
let label = UILabel()
label.text = labelText
label.font = UIFont.ows_dynamicTypeBody
label.textColor = UIColor.ows_materialBlue
label.lineBreakMode = .byTruncatingTail
row.addSubview(label)
label.autoPinTopToSuperviewMargin()
label.autoPinBottomToSuperviewMargin()
label.autoPinLeadingToSuperviewMargin(withInset: hMargin)
label.autoPinTrailingToSuperviewMargin(withInset: hMargin)
return row
}
private func createNameValueRow(name: String, value: String, actionBlock : @escaping () -> Void) -> UIView {
let row = TappableView(actionBlock: actionBlock)
row.layoutMargins.left = 0
row.layoutMargins.right = 0
let nameLabel = UILabel()
nameLabel.text = name
nameLabel.font = UIFont.ows_dynamicTypeCaption1
nameLabel.textColor = UIColor.black
nameLabel.lineBreakMode = .byTruncatingTail
row.addSubview(nameLabel)
nameLabel.autoPinTopToSuperviewMargin()
nameLabel.autoPinLeadingToSuperviewMargin(withInset: hMargin)
nameLabel.autoPinTrailingToSuperviewMargin(withInset: hMargin)
let valueLabel = UILabel()
valueLabel.text = value
valueLabel.font = UIFont.ows_dynamicTypeCaption1
valueLabel.textColor = UIColor.ows_materialBlue
valueLabel.lineBreakMode = .byTruncatingTail
row.addSubview(valueLabel)
valueLabel.autoPinEdge(.top, to: .bottom, of: nameLabel, withOffset: 3)
valueLabel.autoPinBottomToSuperviewMargin()
valueLabel.autoPinLeadingToSuperviewMargin(withInset: hMargin)
valueLabel.autoPinTrailingToSuperviewMargin(withInset: hMargin)
// TODO: Should there be a disclosure icon here?
return row
}
// TODO: Use real assets.
private func createCircleActionButton(text: String, actionBlock : @escaping () -> Void) -> UIView {
let buttonSize = CGFloat(50)
let button = TappableView(actionBlock: actionBlock)
button.layoutMargins = .zero
button.autoSetDimension(.width, toSize: buttonSize, relation: .greaterThanOrEqual)
let circleView = UIView()
circleView.backgroundColor = UIColor.white
circleView.autoSetDimension(.width, toSize: buttonSize)
circleView.autoSetDimension(.height, toSize: buttonSize)
circleView.layer.cornerRadius = buttonSize * 0.5
button.addSubview(circleView)
circleView.autoPinEdge(toSuperviewEdge: .top)
circleView.autoHCenterInSuperview()
let label = UILabel()
label.text = text
label.font = UIFont.ows_dynamicTypeCaption2
label.textColor = UIColor.black
label.lineBreakMode = .byTruncatingTail
label.textAlignment = .center
button.addSubview(label)
label.autoPinEdge(.top, to: .bottom, of: circleView, withOffset: 3)
label.autoPinEdge(toSuperviewEdge: .bottom)
label.autoPinLeadingToSuperviewMargin()
label.autoPinTrailingToSuperviewMargin()
return button
}
private func createLargePillButton(text: String, actionBlock : @escaping () -> Void) -> UIView {
let button = TappableView(actionBlock: actionBlock)
button.backgroundColor = UIColor.white
button.layoutMargins = .zero
button.autoSetDimension(.height, toSize: 45)
button.layer.cornerRadius = 5
let label = UILabel()
label.text = text
label.font = UIFont.ows_dynamicTypeCaption1
label.textColor = UIColor.ows_materialBlue
label.lineBreakMode = .byTruncatingTail
label.textAlignment = .center
button.addSubview(label)
label.autoPinLeadingToSuperviewMargin(withInset: 20)
label.autoPinTrailingToSuperviewMargin(withInset: 20)
label.autoVCenterInSuperview()
label.autoPinEdge(toSuperviewEdge: .top, withInset: 0, relation: .greaterThanOrEqual)
label.autoPinEdge(toSuperviewEdge: .bottom, withInset: 0, relation: .greaterThanOrEqual)
return button
}
func didPressCreateNewContact(sender: UIGestureRecognizer) {
Logger.info("\(logTag) \(#function)")
guard sender.state == .recognized else {
return
}
presentNewContactView()
}
func didPressAddToExistingContact(sender: UIGestureRecognizer) {
Logger.info("\(logTag) \(#function)")
guard sender.state == .recognized else {
return
}
presentSelectAddToExistingContactView()
}
func didPressShareContact(sender: UIGestureRecognizer) {
Logger.info("\(logTag) \(#function)")
guard sender.state == .recognized else {
return
}
// TODO:
}
func didPressSendMessage() {
Logger.info("\(logTag) \(#function)")
// TODO: We're taking the first Signal account id. We might
// want to let the user select if there's more than one.
guard let recipientId = systemContactsWithSignalAccountsForContact().first else {
owsFail("\(logTag) missing Signal recipient id.")
return
}
SignalApp.shared().presentConversation(forRecipientId: recipientId, action: .compose)
}
func didPressAudioCall() {
Logger.info("\(logTag) \(#function)")
// TODO: We're taking the first Signal account id. We might
// want to let the user select if there's more than one.
guard let recipientId = systemContactsWithSignalAccountsForContact().first else {
owsFail("\(logTag) missing Signal recipient id.")
return
}
SignalApp.shared().presentConversation(forRecipientId: recipientId, action: .audioCall)
}
func didPressVideoCall() {
Logger.info("\(logTag) \(#function)")
// TODO: We're taking the first Signal account id. We might
// want to let the user select if there's more than one.
guard let recipientId = systemContactsWithSignalAccountsForContact().first else {
owsFail("\(logTag) missing Signal recipient id.")
return
}
SignalApp.shared().presentConversation(forRecipientId: recipientId, action: .videoCall)
}
func didPressInvite() {
Logger.info("\(logTag) \(#function)")
guard MFMessageComposeViewController.canSendText() else {
Logger.info("\(logTag) Device cannot send text")
OWSAlerts.showErrorAlert(message: NSLocalizedString("UNSUPPORTED_FEATURE_ERROR", comment: ""))
return
}
let phoneNumbers = phoneNumbersForContact()
guard phoneNumbers.count > 0 else {
owsFail("\(logTag) no phone numbers.")
return
}
let inviteFlow =
InviteFlow(presentingViewController: self, contactsManager: contactsManager)
inviteFlow.sendSMSTo(phoneNumbers: phoneNumbers)
}
// MARK: -
private func presentNewContactView() {
guard contactsManager.supportsContactEditing else {
owsFail("\(logTag) Contact editing not supported")
return
}
guard let systemContact = OWSContacts.systemContact(for: contact) else {
owsFail("\(logTag) Could not derive system contact.")
return
}
guard contactsManager.isSystemContactsAuthorized else {
ContactsViewHelper.presentMissingContactAccessAlertController(from: self)
return
}
let contactViewController = CNContactViewController(forNewContact: systemContact)
contactViewController.delegate = self
contactViewController.allowsActions = false
contactViewController.allowsEditing = true
contactViewController.navigationItem.leftBarButtonItem = UIBarButtonItem(title: CommonStrings.cancelButton, style: .plain, target: self, action: #selector(didFinishEditingContact))
contactViewController.navigationItem.leftBarButtonItem = UIBarButtonItem(title: CommonStrings.cancelButton,
style: .plain,
target: self,
action: #selector(didFinishEditingContact))
self.navigationController?.pushViewController(contactViewController, animated: true)
// HACK otherwise CNContactViewController Navbar is shown as black.
// RADAR rdar://28433898 http://www.openradar.me/28433898
// CNContactViewController incompatible with opaque navigation bar
UIUtil.applyDefaultSystemAppearence()
}
private func presentSelectAddToExistingContactView() {
guard contactsManager.supportsContactEditing else {
owsFail("\(logTag) Contact editing not supported")
return
}
guard contactsManager.isSystemContactsAuthorized else {
ContactsViewHelper.presentMissingContactAccessAlertController(from: self)
return
}
guard let firstPhoneNumber = contact.phoneNumbers.first else {
owsFail("\(logTag) Missing phone number.")
return
}
// TODO: We need to modify OWSAddToContactViewController to take a OWSContact
// and merge it with an existing CNContact.
let viewController = OWSAddToContactViewController()
viewController.configure(withRecipientId: firstPhoneNumber.phoneNumber)
self.navigationController?.pushViewController(viewController, animated: true)
}
// MARK: - CNContactViewControllerDelegate
@objc public func contactViewController(_ viewController: CNContactViewController, didCompleteWith contact: CNContact?) {
Logger.info("\(logTag) \(#function)")
self.navigationController?.popToViewController(self, animated: true)
updateContent()
}
@objc public func didFinishEditingContact() {
Logger.info("\(logTag) \(#function)")
self.navigationController?.popToViewController(self, animated: true)
updateContent()
}
}

@ -40,6 +40,8 @@ typedef NS_ENUM(NSUInteger, OWSMessageGestureLocation) {
quotedReply:(OWSQuotedReplyModel *)quotedReply
failedThumbnailDownloadAttachmentPointer:(TSAttachmentPointer *)attachmentPointer;
- (void)didTapContactShareViewItem:(ConversationViewItem *)viewItem;
@end
@interface OWSMessageBubbleView : UIView

@ -1203,7 +1203,7 @@ NS_ASSUME_NONNULL_BEGIN
break;
}
case OWSMessageCellType_ContactShare:
// TODO:
[self.delegate didTapContactShareViewItem:self.viewItem];
break;
}
}

@ -6,15 +6,20 @@
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSUInteger, ConversationViewAction) {
ConversationViewActionNone,
ConversationViewActionCompose,
ConversationViewActionAudioCall,
ConversationViewActionVideoCall,
};
@class TSThread;
@interface ConversationViewController : OWSViewController
@property (nonatomic, readonly) TSThread *thread;
- (void)configureForThread:(TSThread *)thread
keyboardOnViewAppearing:(BOOL)keyboardAppearing
callOnViewAppearing:(BOOL)callOnViewAppearing;
- (void)configureForThread:(TSThread *)thread action:(ConversationViewAction)action;
- (void)popKeyBoard;

@ -189,8 +189,7 @@ typedef enum : NSUInteger {
@property (nonatomic, readonly) NSUInteger backButtonUnreadCount;
@property (nonatomic) NSUInteger lastRangeLength;
@property (nonatomic) BOOL composeOnOpen;
@property (nonatomic) BOOL callOnOpen;
@property (nonatomic) ConversationViewAction actionOnOpen;
@property (nonatomic) BOOL peek;
@property (nonatomic, readonly) OWSContactsManager *contactsManager;
@ -396,7 +395,7 @@ typedef enum : NSUInteger {
- (void)peekSetup
{
_peek = YES;
[self setComposeOnOpen:NO];
self.actionOnOpen = ConversationViewActionNone;
}
- (void)popped
@ -405,21 +404,11 @@ typedef enum : NSUInteger {
[self hideInputIfNeeded];
}
- (void)configureForThread:(TSThread *)thread
keyboardOnViewAppearing:(BOOL)keyboardOnViewAppearing
callOnViewAppearing:(BOOL)callOnViewAppearing
- (void)configureForThread:(TSThread *)thread action:(ConversationViewAction)action
{
// At most one.
OWSAssert(!keyboardOnViewAppearing || !callOnViewAppearing);
if (callOnViewAppearing) {
keyboardOnViewAppearing = NO;
}
_thread = thread;
_isGroupConversation = [self.thread isKindOfClass:[TSGroupThread class]];
_composeOnOpen = keyboardOnViewAppearing;
_callOnOpen = callOnViewAppearing;
self.actionOnOpen = action;
_cellMediaCache = [NSCache new];
// Cache the cell media for ~24 cells.
self.cellMediaCache.countLimit = 24;
@ -1032,15 +1021,22 @@ typedef enum : NSUInteger {
[self updateNavigationBarSubtitleLabel];
[self updateBackButtonUnreadCount];
if (_composeOnOpen && !self.inputToolbar.hidden) {
[self popKeyBoard];
_composeOnOpen = NO;
}
if (_callOnOpen) {
[self callAction];
_callOnOpen = NO;
switch (self.actionOnOpen) {
case ConversationViewActionNone:
break;
case ConversationViewActionCompose:
[self popKeyBoard];
break;
case ConversationViewActionAudioCall:
[self startAudioCall];
break;
case ConversationViewActionVideoCall:
[self startVideoCall];
break;
}
self.actionOnOpen = ConversationViewActionNone;
self.isViewCompletelyAppeared = YES;
self.viewHasEverAppeared = YES;
@ -1227,7 +1223,7 @@ typedef enum : NSUInteger {
imageEdgeInsets.bottom = round(kBarButtonSize - (image.size.height + imageEdgeInsets.top));
callButton.imageEdgeInsets = imageEdgeInsets;
callButton.accessibilityLabel = NSLocalizedString(@"CALL_LABEL", "Accessibility label for placing call button");
[callButton addTarget:self action:@selector(callAction) forControlEvents:UIControlEventTouchUpInside];
[callButton addTarget:self action:@selector(startAudioCall) forControlEvents:UIControlEventTouchUpInside];
callButton.frame = CGRectMake(0,
0,
round(image.size.width + imageEdgeInsets.left + imageEdgeInsets.right),
@ -1362,7 +1358,17 @@ typedef enum : NSUInteger {
#pragma mark - Calls
- (void)callAction
- (void)startAudioCall
{
[self callWithVideo:NO];
}
- (void)startVideoCall
{
[self callWithVideo:YES];
}
- (void)callWithVideo:(BOOL)isVideo
{
OWSAssert([self.thread isKindOfClass:[TSContactThread class]]);
@ -1375,7 +1381,7 @@ typedef enum : NSUInteger {
if ([self isBlockedContactConversation]) {
[self showUnblockContactUI:^(BOOL isBlocked) {
if (!isBlocked) {
[weakSelf callAction];
[weakSelf callWithVideo:isVideo];
}
}];
return;
@ -1385,14 +1391,14 @@ typedef enum : NSUInteger {
[self showSafetyNumberConfirmationIfNecessaryWithConfirmationText:[CallStrings confirmAndCallButtonTitle]
completion:^(BOOL didConfirmIdentity) {
if (didConfirmIdentity) {
[weakSelf callAction];
[weakSelf callWithVideo:isVideo];
}
}];
if (didShowSNAlert) {
return;
}
[self.outboundCallInitiator initiateCallWithRecipientId:self.thread.contactIdentifier];
[self.outboundCallInitiator initiateCallWithRecipientId:self.thread.contactIdentifier isVideo:isVideo];
}
- (BOOL)canCall
@ -1875,7 +1881,7 @@ typedef enum : NSUInteger {
UIAlertAction *callAction = [UIAlertAction actionWithTitle:[CallStrings callBackAlertCallButton]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *_Nonnull action) {
[weakSelf callAction];
[weakSelf startAudioCall];
}];
[alertController addAction:callAction];
[alertController addAction:[OWSAlerts cancelAction]];
@ -2073,6 +2079,17 @@ typedef enum : NSUInteger {
[self.navigationController pushViewController:view animated:YES];
}
- (void)didTapContactShareViewItem:(ConversationViewItem *)conversationItem
{
OWSAssertIsOnMainThread();
OWSAssert(conversationItem);
OWSAssert(conversationItem.contactShare);
OWSAssert([conversationItem.interaction isKindOfClass:[TSMessage class]]);
ContactViewController *view = [[ContactViewController alloc] initWithContact:conversationItem.contactShare];
[self.navigationController pushViewController:view animated:YES];
}
- (void)didTapFailedIncomingAttachment:(ConversationViewItem *)viewItem
attachmentPointer:(TSAttachmentPointer *)attachmentPointer
{

@ -2976,11 +2976,11 @@ typedef OWSContact * (^OWSContactBlock)(void);
OWSContactPhoneNumber *phoneNumber1 = [OWSContactPhoneNumber new];
phoneNumber1.phoneType = OWSContactPhoneType_Home;
phoneNumber1.phoneNumber = @"+13213214321";
phoneNumber1.phoneNumber = @"+13213215555";
OWSContactPhoneNumber *phoneNumber2 = [OWSContactPhoneNumber new];
phoneNumber2.phoneType = OWSContactPhoneType_Custom;
phoneNumber2.label = @"Carphone";
phoneNumber2.phoneNumber = @"+13332221111";
phoneNumber2.phoneNumber = @"+13332226666";
contact.phoneNumbers = @[
phoneNumber1,
phoneNumber2,
@ -2988,11 +2988,11 @@ typedef OWSContact * (^OWSContactBlock)(void);
OWSContactEmail *email1 = [OWSContactEmail new];
email1.emailType = OWSContactEmailType_Home;
email1.email = @"a@b.com";
email1.email = @"a1@b.com";
OWSContactEmail *email2 = [OWSContactEmail new];
email2.emailType = OWSContactEmailType_Custom;
email2.label = @"customer support";
email2.email = @"a@b.com";
email2.email = @"a2@b.com";
contact.emails = @[
email1,
email2,
@ -3023,6 +3023,46 @@ typedef OWSContact * (^OWSContactBlock)(void);
// TODO: Avatar
return contact;
}]];
[actions addObject:[self fakeContactShareMessageAction:thread
label:@"Long values"
contactBlock:^{
OWSContact *contact = [OWSContact new];
contact.givenName = @"Bobasdjasdlkjasldkjas";
contact.familyName = @"Bobasdjasdlkjasldkjas";
OWSContactEmail *email = [OWSContactEmail new];
email.emailType = OWSContactEmailType_Mobile;
email.email = @"asdlakjsaldkjasldkjasdlkjasdlkjasdlkajsa@b.com";
contact.emails = @[
email,
];
return contact;
}]];
[actions addObject:[self fakeContactShareMessageAction:thread
label:@"System Contact w/o Signal"
contactBlock:^{
OWSContact *contact = [OWSContact new];
contact.givenName = @"Add Me To Your Contacts";
OWSContactPhoneNumber *phoneNumber = [OWSContactPhoneNumber new];
phoneNumber.phoneType = OWSContactPhoneType_Work;
phoneNumber.phoneNumber = @"+324602053911";
contact.phoneNumbers = @[
phoneNumber,
];
return contact;
}]];
[actions addObject:[self fakeContactShareMessageAction:thread
label:@"System Contact w. Signal"
contactBlock:^{
OWSContact *contact = [OWSContact new];
contact.givenName = @"Add Me To Your Contacts";
OWSContactPhoneNumber *phoneNumber = [OWSContactPhoneNumber new];
phoneNumber.phoneType = OWSContactPhoneType_Work;
phoneNumber.phoneNumber = @"+32460205392";
contact.phoneNumbers = @[
phoneNumber,
];
return contact;
}]];
return actions;
}

@ -2,6 +2,7 @@
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "ConversationViewController.h"
#import <SignalMessaging/OWSViewController.h>
#import <UIKit/UIKit.h>
@ -9,9 +10,7 @@
@interface HomeViewController : OWSViewController
- (void)presentThread:(TSThread *)thread
keyboardOnViewAppearing:(BOOL)keyboardOnViewAppearing
callOnViewAppearing:(BOOL)callOnViewAppearing;
- (void)presentThread:(TSThread *)thread action:(ConversationViewAction)action;
- (void)showNewConversationView;

@ -5,7 +5,6 @@
#import "HomeViewController.h"
#import "AppDelegate.h"
#import "AppSettingsViewController.h"
#import "ConversationViewController.h"
#import "HomeViewCell.h"
#import "NewContactThreadViewController.h"
#import "OWSNavigationController.h"
@ -383,7 +382,7 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations
ConversationViewController *vc = [ConversationViewController new];
TSThread *thread = [self threadForIndexPath:indexPath];
self.lastThread = thread;
[vc configureForThread:thread keyboardOnViewAppearing:NO callOnViewAppearing:NO];
[vc configureForThread:thread action:ConversationViewActionNone];
[vc peekSetup];
return vc;
@ -912,17 +911,12 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations
}
TSThread *thread = [self threadForIndexPath:indexPath];
[self presentThread:thread keyboardOnViewAppearing:NO callOnViewAppearing:NO];
[self presentThread:thread action:ConversationViewActionNone];
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
- (void)presentThread:(TSThread *)thread
keyboardOnViewAppearing:(BOOL)keyboardOnViewAppearing
callOnViewAppearing:(BOOL)callOnViewAppearing
- (void)presentThread:(TSThread *)thread action:(ConversationViewAction)action
{
// At most one.
OWSAssert(!keyboardOnViewAppearing || !callOnViewAppearing);
if (thread == nil) {
OWSFail(@"Thread unexpectedly nil");
return;
@ -931,9 +925,7 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations
// We do this synchronously if we're already on the main thread.
DispatchMainThreadSafe(^{
ConversationViewController *mvc = [ConversationViewController new];
[mvc configureForThread:thread
keyboardOnViewAppearing:keyboardOnViewAppearing
callOnViewAppearing:callOnViewAppearing];
[mvc configureForThread:thread action:action];
self.lastThread = thread;
[self pushTopLevelViewController:mvc animateDismissal:YES animatePresentation:YES];

@ -615,6 +615,15 @@ class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDele
mediaGalleryViewController.presentDetailView(fromViewController: self, mediaMessage: self.message, replacingView: imageView)
}
func didTapContactShare(_ viewItem: ConversationViewItem) {
guard let contact = viewItem.contactShare() else {
owsFail("\(logTag) missing contact.")
return
}
let contactViewController = ContactViewController(contact: contact)
self.navigationController?.pushViewController(contactViewController, animated: true)
}
var audioAttachmentPlayer: OWSAudioPlayer?
func didTapAudioViewItem(_ viewItem: ConversationViewItem, attachmentStream: TSAttachmentStream) {

@ -809,7 +809,8 @@ NS_ASSUME_NONNULL_BEGIN
OWSAssert(thread != nil);
[self dismissViewControllerAnimated:YES
completion:^() {
[SignalApp.sharedApp presentConversationForThread:thread withCompose:YES];
[SignalApp.sharedApp presentConversationForThread:thread
action:ConversationViewActionCompose];
}];
}

@ -407,12 +407,12 @@ NS_ASSUME_NONNULL_BEGIN
{
OWSAssert(recipientId.length > 0);
[SignalApp.sharedApp presentConversationForRecipientId:recipientId withCompose:YES];
[SignalApp.sharedApp presentConversationForRecipientId:recipientId action:ConversationViewActionCompose];
}
- (void)callMember:(NSString *)recipientId
{
[SignalApp.sharedApp callRecipientId:recipientId];
[SignalApp.sharedApp presentConversationForRecipientId:recipientId action:ConversationViewActionAudioCall];
}
- (void)showSafetyNumberView:(NSString *)recipientId

@ -35,13 +35,14 @@ import SignalMessaging
return false
}
return initiateCall(recipientId: recipientId)
return initiateCall(recipientId: recipientId, isVideo: false)
}
/**
* |recipientId| is a e164 formatted phone number.
*/
public func initiateCall(recipientId: String) -> Bool {
public func initiateCall(recipientId: String,
isVideo: Bool) -> Bool {
// Rather than an init-assigned dependency property, we access `callUIAdapter` via Environment
// because it can change after app launch due to user settings
guard let callUIAdapter = SignalApp.shared().callUIAdapter else {
@ -58,7 +59,7 @@ import SignalMessaging
contactsManager: self.contactsManager,
completion: { didConfirmIdentity in
if didConfirmIdentity {
_ = self.initiateCall(recipientId: recipientId)
_ = self.initiateCall(recipientId: recipientId, isVideo: isVideo)
}
})
guard !showedAlert else {
@ -81,7 +82,7 @@ import SignalMessaging
OWSAlerts.showNoMicrophonePermissionAlert()
return
}
callUIAdapter.startAndShowOutgoingCall(recipientId: recipientId)
callUIAdapter.startAndShowOutgoingCall(recipientId: recipientId, hasLocalVideo: isVideo)
})
return true

@ -28,7 +28,7 @@ protocol CallUIAdaptee {
func failCall(_ call: SignalCall, error: CallError)
func setIsMuted(call: SignalCall, isMuted: Bool)
func setHasLocalVideo(call: SignalCall, hasLocalVideo: Bool)
func startAndShowOutgoingCall(recipientId: String)
func startAndShowOutgoingCall(recipientId: String, hasLocalVideo: Bool)
}
// Shared default implementations
@ -63,7 +63,7 @@ extension CallUIAdaptee {
notificationsAdapter.presentMissedCall(call, callerName: callerName)
}
internal func startAndShowOutgoingCall(recipientId: String) {
internal func startAndShowOutgoingCall(recipientId: String, hasLocalVideo: Bool) {
SwiftAssertIsOnMainThread(#function)
guard self.callService.call == nil else {
@ -73,6 +73,7 @@ extension CallUIAdaptee {
}
let call = self.startOutgoingCall(handle: recipientId)
call.hasLocalVideo = hasLocalVideo
self.showCall(call)
}
}
@ -186,10 +187,10 @@ extension CallUIAdaptee {
}
}
internal func startAndShowOutgoingCall(recipientId: String) {
internal func startAndShowOutgoingCall(recipientId: String, hasLocalVideo: Bool) {
SwiftAssertIsOnMainThread(#function)
adaptee.startAndShowOutgoingCall(recipientId: recipientId)
adaptee.startAndShowOutgoingCall(recipientId: recipientId, hasLocalVideo: hasLocalVideo)
}
internal func recipientAcceptedCall(_ call: SignalCall) {

@ -1,7 +1,9 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "ConversationViewController.h"
@class AccountManager;
@class CallService;
@class CallUIAdapter;
@ -33,11 +35,10 @@
#pragma mark - View Convenience Methods
- (void)presentConversationForRecipientId:(NSString *)recipientId;
- (void)presentConversationForRecipientId:(NSString *)recipientId withCompose:(BOOL)compose;
- (void)callRecipientId:(NSString *)recipientId;
- (void)presentConversationForRecipientId:(NSString *)recipientId action:(ConversationViewAction)action;
- (void)presentConversationForThreadId:(NSString *)threadId;
- (void)presentConversationForThread:(TSThread *)thread;
- (void)presentConversationForThread:(TSThread *)thread withCompose:(BOOL)compose;
- (void)presentConversationForThread:(TSThread *)thread action:(ConversationViewAction)action;
#pragma mark - Methods

@ -152,35 +152,18 @@
- (void)presentConversationForRecipientId:(NSString *)recipientId
{
[self presentConversationForRecipientId:recipientId keyboardOnViewAppearing:YES callOnViewAppearing:NO];
[self presentConversationForRecipientId:recipientId action:ConversationViewActionNone];
}
- (void)presentConversationForRecipientId:(NSString *)recipientId withCompose:(BOOL)compose
- (void)presentConversationForRecipientId:(NSString *)recipientId action:(ConversationViewAction)action
{
[self presentConversationForRecipientId:recipientId keyboardOnViewAppearing:compose callOnViewAppearing:NO];
}
- (void)callRecipientId:(NSString *)recipientId
{
[self presentConversationForRecipientId:recipientId keyboardOnViewAppearing:NO callOnViewAppearing:YES];
}
- (void)presentConversationForRecipientId:(NSString *)recipientId
keyboardOnViewAppearing:(BOOL)keyboardOnViewAppearing
callOnViewAppearing:(BOOL)callOnViewAppearing
{
// At most one.
OWSAssert(!keyboardOnViewAppearing || !callOnViewAppearing);
DispatchMainThreadSafe(^{
__block TSThread *thread = nil;
[OWSPrimaryStorage.dbReadWriteConnection
readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
thread = [TSContactThread getOrCreateThreadWithContactId:recipientId transaction:transaction];
}];
[self presentConversationForThread:thread
keyboardOnViewAppearing:keyboardOnViewAppearing
callOnViewAppearing:callOnViewAppearing];
[self presentConversationForThread:thread action:action];
});
}
@ -199,21 +182,12 @@
- (void)presentConversationForThread:(TSThread *)thread
{
[self presentConversationForThread:thread withCompose:YES];
}
- (void)presentConversationForThread:(TSThread *)thread withCompose:(BOOL)compose
{
[self presentConversationForThread:thread keyboardOnViewAppearing:compose callOnViewAppearing:NO];
[self presentConversationForThread:thread action:ConversationViewActionNone];
}
- (void)presentConversationForThread:(TSThread *)thread
keyboardOnViewAppearing:(BOOL)keyboardOnViewAppearing
callOnViewAppearing:(BOOL)callOnViewAppearing
- (void)presentConversationForThread:(TSThread *)thread action:(ConversationViewAction)action
{
OWSAssertIsOnMainThread();
// At most one.
OWSAssert(!keyboardOnViewAppearing || !callOnViewAppearing);
DDLogInfo(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
@ -233,9 +207,7 @@
}
}
[self.homeViewController presentThread:thread
keyboardOnViewAppearing:keyboardOnViewAppearing
callOnViewAppearing:callOnViewAppearing];
[self.homeViewController presentThread:thread action:action];
});
}

@ -253,7 +253,7 @@ NSString *const Signal_Message_MarkAsRead_Identifier = @"Signal_Message_MarkAsRe
return;
}
[self.callUIAdapter startAndShowOutgoingCallWithRecipientId:recipientId];
[self.callUIAdapter startAndShowOutgoingCallWithRecipientId:recipientId hasLocalVideo:NO];
completionHandler();
} else if ([identifier isEqualToString:PushManagerActionsShowThread]) {
NSString *threadId = notification.userInfo[Signal_Thread_UserInfo_Key];

@ -4,6 +4,21 @@
/* Action sheet item */
"ACCEPT_NEW_IDENTITY_ACTION" = "Accept New Safety Number";
/* Label for 'audio call' button in contact view. */
"ACTION_AUDIO_CALL" = "Signal Call";
/* Label for 'invite' button in contact view. */
"ACTION_INVITE" = "Invite to Signal";
/* Label for 'sent message' button in contact view. */
"ACTION_SEND_MESSAGE" = "Send Message";
/* Label for 'share contact' button. */
"ACTION_SHARE_CONTACT" = "Share Contact";
/* Label for 'video call' button in contact view. */
"ACTION_VIDEO_CALL" = "Video Call";
/* A label for the 'add by phone number' button in the 'add group member' view */
"ADD_GROUP_MEMBER_VIEW_BUTTON" = "Add";

@ -1,12 +1,13 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
@class ContactsViewHelper;
@class Contact;
@class ContactsViewHelper;
@class SignalAccount;
@protocol CNContactViewControllerDelegate;
@protocol ContactsViewHelperDelegate <NSObject>
@ -27,9 +28,9 @@ NS_ASSUME_NONNULL_BEGIN
#pragma mark -
@class OWSContactsManager;
@class OWSBlockingManager;
@class CNContact;
@class OWSBlockingManager;
@class OWSContactsManager;
@interface ContactsViewHelper : NSObject
@ -81,6 +82,8 @@ NS_ASSUME_NONNULL_BEGIN
editImmediately:(BOOL)shouldEditImmediately
addToExistingCnContact:(CNContact *_Nullable)cnContact;
+ (void)presentMissingContactAccessAlertControllerFromViewController:(UIViewController *)viewController;
@end
NS_ASSUME_NONNULL_END

@ -264,6 +264,11 @@ NS_ASSUME_NONNULL_BEGIN
#pragma mark - Editing
- (void)presentMissingContactAccessAlertControllerFromViewController:(UIViewController *)viewController
{
[ContactsViewHelper presentMissingContactAccessAlertControllerFromViewController:viewController];
}
+ (void)presentMissingContactAccessAlertControllerFromViewController:(UIViewController *)viewController
{
UIAlertController *alertController = [UIAlertController
alertControllerWithTitle:NSLocalizedString(@"EDIT_CONTACT_WITHOUT_CONTACTS_PERMISSION_ALERT_TITLE", comment

@ -57,9 +57,9 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, nullable) NSString *organizationName;
@property (nonatomic, nullable) NSString *displayName;
@property (nonatomic, nullable) NSArray<OWSContactPhoneNumber *> *phoneNumbers;
@property (nonatomic, nullable) NSArray<OWSContactEmail *> *emails;
@property (nonatomic, nullable) NSArray<OWSContactAddress *> *addresses;
@property (nonatomic) NSArray<OWSContactPhoneNumber *> *phoneNumbers;
@property (nonatomic) NSArray<OWSContactEmail *> *emails;
@property (nonatomic) NSArray<OWSContactAddress *> *addresses;
@property (nonatomic, nullable) TSAttachment *avatar;
@property (nonatomic) BOOL isProfileAvatar;

@ -30,6 +30,8 @@ typedef NS_ENUM(NSUInteger, OWSContactPhoneType) {
- (BOOL)ows_isValid;
- (NSString *)labelString;
@end
#pragma mark -
@ -51,6 +53,8 @@ typedef NS_ENUM(NSUInteger, OWSContactEmailType) {
- (BOOL)ows_isValid;
- (NSString *)labelString;
@end
#pragma mark -
@ -77,6 +81,8 @@ typedef NS_ENUM(NSUInteger, OWSContactAddressType) {
- (BOOL)ows_isValid;
- (NSString *)labelString;
@end
#pragma mark -
@ -91,9 +97,9 @@ typedef NS_ENUM(NSUInteger, OWSContactAddressType) {
@property (nonatomic, readonly, nullable) NSString *organizationName;
@property (nonatomic, readonly, nullable) NSString *displayName;
@property (nonatomic, readonly, nullable) NSArray<OWSContactPhoneNumber *> *phoneNumbers;
@property (nonatomic, readonly, nullable) NSArray<OWSContactEmail *> *emails;
@property (nonatomic, readonly, nullable) NSArray<OWSContactAddress *> *addresses;
@property (nonatomic, readonly) NSArray<OWSContactPhoneNumber *> *phoneNumbers;
@property (nonatomic, readonly) NSArray<OWSContactEmail *> *emails;
@property (nonatomic, readonly) NSArray<OWSContactAddress *> *addresses;
// TODO: This is provisional.
@property (nonatomic, readonly, nullable) TSAttachment *avatar;

@ -42,6 +42,20 @@ NS_ASSUME_NONNULL_BEGIN
}
}
- (NSString *)labelString
{
switch (self.phoneType) {
case OWSContactPhoneType_Home:
return [CNLabeledValue localizedStringForLabel:CNLabelHome];
case OWSContactPhoneType_Mobile:
return [CNLabeledValue localizedStringForLabel:CNLabelPhoneNumberMobile];
case OWSContactPhoneType_Work:
return [CNLabeledValue localizedStringForLabel:CNLabelWork];
default:
return self.label;
}
}
@end
#pragma mark -
@ -74,6 +88,20 @@ NS_ASSUME_NONNULL_BEGIN
}
}
- (NSString *)labelString
{
switch (self.emailType) {
case OWSContactEmailType_Home:
return [CNLabeledValue localizedStringForLabel:CNLabelHome];
case OWSContactEmailType_Mobile:
return [CNLabeledValue localizedStringForLabel:CNLabelPhoneNumberMobile];
case OWSContactEmailType_Work:
return [CNLabeledValue localizedStringForLabel:CNLabelWork];
default:
return self.label;
}
}
@end
#pragma mark -
@ -114,6 +142,18 @@ NS_ASSUME_NONNULL_BEGIN
}
}
- (NSString *)labelString
{
switch (self.addressType) {
case OWSContactAddressType_Home:
return [CNLabeledValue localizedStringForLabel:CNLabelHome];
case OWSContactAddressType_Work:
return [CNLabeledValue localizedStringForLabel:CNLabelWork];
default:
return self.label;
}
}
@end
#pragma mark -
@ -128,9 +168,9 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, nullable) NSString *organizationName;
@property (nonatomic, nullable) NSString *displayName;
@property (nonatomic, nullable) NSArray<OWSContactPhoneNumber *> *phoneNumbers;
@property (nonatomic, nullable) NSArray<OWSContactEmail *> *emails;
@property (nonatomic, nullable) NSArray<OWSContactAddress *> *addresses;
@property (nonatomic) NSArray<OWSContactPhoneNumber *> *phoneNumbers;
@property (nonatomic) NSArray<OWSContactEmail *> *emails;
@property (nonatomic) NSArray<OWSContactAddress *> *addresses;
@property (nonatomic, nullable) TSAttachment *avatar;
@property (nonatomic) BOOL isProfileAvatar;
@ -141,6 +181,17 @@ NS_ASSUME_NONNULL_BEGIN
@implementation OWSContact
- (instancetype)init
{
if (self = [super init]) {
_phoneNumbers = @[];
_emails = @[];
_addresses = @[];
}
return self;
}
- (void)normalize
{
self.phoneNumbers = [self.phoneNumbers

Loading…
Cancel
Save