diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index c466642a5..34a9dfbc4 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -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 = ""; }; 34E3EF0E1EFC2684007F6822 /* DebugUIPage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUIPage.h; sourceTree = ""; }; 34E3EF0F1EFC2684007F6822 /* DebugUIPage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugUIPage.m; sourceTree = ""; }; + 34E88D252098C5AE00A608F4 /* ContactViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactViewController.swift; sourceTree = ""; }; 34E8A8D02085238900B272B1 /* ProtoParsingTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ProtoParsingTest.m; sourceTree = ""; }; 34F308A01ECB469700BB7697 /* OWSBezierPathView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBezierPathView.h; sourceTree = ""; }; 34F308A11ECB469700BB7697 /* OWSBezierPathView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBezierPathView.m; sourceTree = ""; }; @@ -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 */, diff --git a/Signal/src/Signal-Bridging-Header.h b/Signal/src/Signal-Bridging-Header.h index b1cc323eb..e8a428a91 100644 --- a/Signal/src/Signal-Bridging-Header.h +++ b/Signal/src/Signal-Bridging-Header.h @@ -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" diff --git a/Signal/src/ViewControllers/ContactViewController.swift b/Signal/src/ViewControllers/ContactViewController.swift new file mode 100644 index 000000000..0465fa42a --- /dev/null +++ b/Signal/src/ViewControllers/ContactViewController.swift @@ -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() + } +} diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.h index 19323d81f..fedb9a51e 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.h +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.h @@ -40,6 +40,8 @@ typedef NS_ENUM(NSUInteger, OWSMessageGestureLocation) { quotedReply:(OWSQuotedReplyModel *)quotedReply failedThumbnailDownloadAttachmentPointer:(TSAttachmentPointer *)attachmentPointer; +- (void)didTapContactShareViewItem:(ConversationViewItem *)viewItem; + @end @interface OWSMessageBubbleView : UIView diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m index 50cc326ce..413133b60 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m @@ -1203,7 +1203,7 @@ NS_ASSUME_NONNULL_BEGIN break; } case OWSMessageCellType_ContactShare: - // TODO: + [self.delegate didTapContactShareViewItem:self.viewItem]; break; } } diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.h b/Signal/src/ViewControllers/ConversationView/ConversationViewController.h index ec7610a4a..002a95218 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.h +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.h @@ -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; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 0d1c0801a..0bc937072 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -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 { diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m index 181c97cca..c4b13a2e0 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m @@ -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; } diff --git a/Signal/src/ViewControllers/HomeView/HomeViewController.h b/Signal/src/ViewControllers/HomeView/HomeViewController.h index 632419ab0..a19c94488 100644 --- a/Signal/src/ViewControllers/HomeView/HomeViewController.h +++ b/Signal/src/ViewControllers/HomeView/HomeViewController.h @@ -2,6 +2,7 @@ // Copyright (c) 2018 Open Whisper Systems. All rights reserved. // +#import "ConversationViewController.h" #import #import @@ -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; diff --git a/Signal/src/ViewControllers/HomeView/HomeViewController.m b/Signal/src/ViewControllers/HomeView/HomeViewController.m index 08d1adffa..8974bd59a 100644 --- a/Signal/src/ViewControllers/HomeView/HomeViewController.m +++ b/Signal/src/ViewControllers/HomeView/HomeViewController.m @@ -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]; diff --git a/Signal/src/ViewControllers/MessageDetailViewController.swift b/Signal/src/ViewControllers/MessageDetailViewController.swift index 3c77d3433..81de50e24 100644 --- a/Signal/src/ViewControllers/MessageDetailViewController.swift +++ b/Signal/src/ViewControllers/MessageDetailViewController.swift @@ -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) { diff --git a/Signal/src/ViewControllers/NewContactThreadViewController.m b/Signal/src/ViewControllers/NewContactThreadViewController.m index 8ff55a48b..bf1c145c7 100644 --- a/Signal/src/ViewControllers/NewContactThreadViewController.m +++ b/Signal/src/ViewControllers/NewContactThreadViewController.m @@ -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]; }]; } diff --git a/Signal/src/ViewControllers/ThreadSettings/ShowGroupMembersViewController.m b/Signal/src/ViewControllers/ThreadSettings/ShowGroupMembersViewController.m index b5fa90582..bb3b8bbdb 100644 --- a/Signal/src/ViewControllers/ThreadSettings/ShowGroupMembersViewController.m +++ b/Signal/src/ViewControllers/ThreadSettings/ShowGroupMembersViewController.m @@ -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 diff --git a/Signal/src/call/OutboundCallInitiator.swift b/Signal/src/call/OutboundCallInitiator.swift index 1d411ff45..c081b4e0f 100644 --- a/Signal/src/call/OutboundCallInitiator.swift +++ b/Signal/src/call/OutboundCallInitiator.swift @@ -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 diff --git a/Signal/src/call/UserInterface/CallUIAdapter.swift b/Signal/src/call/UserInterface/CallUIAdapter.swift index d280ec797..67865be3b 100644 --- a/Signal/src/call/UserInterface/CallUIAdapter.swift +++ b/Signal/src/call/UserInterface/CallUIAdapter.swift @@ -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) { diff --git a/Signal/src/environment/SignalApp.h b/Signal/src/environment/SignalApp.h index f5480119f..e3116c9e4 100644 --- a/Signal/src/environment/SignalApp.h +++ b/Signal/src/environment/SignalApp.h @@ -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 diff --git a/Signal/src/environment/SignalApp.m b/Signal/src/environment/SignalApp.m index 9fed45388..49198f316 100644 --- a/Signal/src/environment/SignalApp.m +++ b/Signal/src/environment/SignalApp.m @@ -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]; }); } diff --git a/Signal/src/network/PushManager.m b/Signal/src/network/PushManager.m index 42abb5c7f..37859c532 100644 --- a/Signal/src/network/PushManager.m +++ b/Signal/src/network/PushManager.m @@ -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]; diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 4dc1145ad..131814aa5 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -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"; diff --git a/SignalMessaging/Views/ContactsViewHelper.h b/SignalMessaging/Views/ContactsViewHelper.h index e963ba687..e48b1675a 100644 --- a/SignalMessaging/Views/ContactsViewHelper.h +++ b/SignalMessaging/Views/ContactsViewHelper.h @@ -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 @@ -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 diff --git a/SignalMessaging/Views/ContactsViewHelper.m b/SignalMessaging/Views/ContactsViewHelper.m index af2ae1829..17c38ef4d 100644 --- a/SignalMessaging/Views/ContactsViewHelper.m +++ b/SignalMessaging/Views/ContactsViewHelper.m @@ -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 diff --git a/SignalServiceKit/src/Messages/Interactions/OWSContact+Private.h b/SignalServiceKit/src/Messages/Interactions/OWSContact+Private.h index 9c2a4e7c0..5a037eb84 100644 --- a/SignalServiceKit/src/Messages/Interactions/OWSContact+Private.h +++ b/SignalServiceKit/src/Messages/Interactions/OWSContact+Private.h @@ -57,9 +57,9 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, nullable) NSString *organizationName; @property (nonatomic, nullable) NSString *displayName; -@property (nonatomic, nullable) NSArray *phoneNumbers; -@property (nonatomic, nullable) NSArray *emails; -@property (nonatomic, nullable) NSArray *addresses; +@property (nonatomic) NSArray *phoneNumbers; +@property (nonatomic) NSArray *emails; +@property (nonatomic) NSArray *addresses; @property (nonatomic, nullable) TSAttachment *avatar; @property (nonatomic) BOOL isProfileAvatar; diff --git a/SignalServiceKit/src/Messages/Interactions/OWSContact.h b/SignalServiceKit/src/Messages/Interactions/OWSContact.h index 8f3f5c07f..46e796931 100644 --- a/SignalServiceKit/src/Messages/Interactions/OWSContact.h +++ b/SignalServiceKit/src/Messages/Interactions/OWSContact.h @@ -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 *phoneNumbers; -@property (nonatomic, readonly, nullable) NSArray *emails; -@property (nonatomic, readonly, nullable) NSArray *addresses; +@property (nonatomic, readonly) NSArray *phoneNumbers; +@property (nonatomic, readonly) NSArray *emails; +@property (nonatomic, readonly) NSArray *addresses; // TODO: This is provisional. @property (nonatomic, readonly, nullable) TSAttachment *avatar; diff --git a/SignalServiceKit/src/Messages/Interactions/OWSContact.m b/SignalServiceKit/src/Messages/Interactions/OWSContact.m index aa15b985e..0c4800b01 100644 --- a/SignalServiceKit/src/Messages/Interactions/OWSContact.m +++ b/SignalServiceKit/src/Messages/Interactions/OWSContact.m @@ -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 *phoneNumbers; -@property (nonatomic, nullable) NSArray *emails; -@property (nonatomic, nullable) NSArray *addresses; +@property (nonatomic) NSArray *phoneNumbers; +@property (nonatomic) NSArray *emails; +@property (nonatomic) NSArray *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