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/ViewControllers/ContactViewController.swift b/Signal/src/ViewControllers/ContactViewController.swift new file mode 100644 index 000000000..605220570 --- /dev/null +++ b/Signal/src/ViewControllers/ContactViewController.swift @@ -0,0 +1,303 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation +import SignalServiceKit +import SignalMessaging +import Reachability + +class ContactViewController: OWSViewController { + + let TAG = "[ContactView]" + + enum ContactViewMode { + case systemContactWithSignal, + systemContactWithoutSignal, + nonSystemContactWithSignal, + nonSystemContactWithoutSignal, + noPhoneNumber, + unknown + } + + enum ContactLookupMode { + case notLookingUp, + lookingUp, + lookedUpNoAccount, + lookedUpHasAccount + } + + private var hasLoadedView = false + + private var viewMode = ContactViewMode.unknown { + didSet { + SwiftAssertIsOnMainThread(#function) + + if oldValue != viewMode && hasLoadedView { + updateContent() + } + } + } + + private var lookupMode = ContactLookupMode.notLookingUp { + didSet { + SwiftAssertIsOnMainThread(#function) + + if oldValue != lookupMode && hasLoadedView { + updateContent() + } + } + } + + let contactsManager: OWSContactsManager + + var reachability: Reachability? + + override public var canBecomeFirstResponder: Bool { + return true + } + + 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 + + super.init(nibName: nil, bundle: nil) + + tryToDetermineMode() + + NotificationCenter.default.addObserver(forName: .OWSContactsManagerSignalAccountsDidChange, object: nil, queue: nil) { [weak self] _ in + guard let strongSelf = self else { return } + strongSelf.tryToDetermineMode() + } + + reachability = Reachability.forInternetConnection() + + NotificationCenter.default.addObserver(forName: .reachabilityChanged, object: nil, queue: nil) { [weak self] _ in + guard let strongSelf = self else { return } + strongSelf.tryToDetermineMode() + } + } + + // MARK: - View Lifecycle + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + self.becomeFirstResponder() + + contactsManager.requestSystemContactsOnce(completion: { [weak self] _ in + guard let strongSelf = self else { return } + strongSelf.tryToDetermineMode() + }) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.becomeFirstResponder() + } + + override func loadView() { + super.loadView() + self.view.backgroundColor = UIColor.white + + updateContent() + + hasLoadedView = true + } + + private func tryToDetermineMode() { + SwiftAssertIsOnMainThread(#function) + + guard let firstPhoneNumber = contact.phoneNumbers?.first else { + viewMode = .noPhoneNumber + return + } + if contactsManager.hasSignalAccount(forRecipientId: firstPhoneNumber.phoneNumber) { + viewMode = .systemContactWithSignal + return + } + if contactsManager.allContactsMap[firstPhoneNumber.phoneNumber] != nil { + // We can infer that this is _not_ a signal user because + // all contacts in contactsManager.allContactsMap have + // already been looked up. + viewMode = .systemContactWithoutSignal + return + } + + switch lookupMode { + case .notLookingUp: + lookupMode = .lookingUp + viewMode = .unknown + ContactsUpdater.shared().lookupIdentifiers([firstPhoneNumber.phoneNumber], success: { [weak self] (signalRecipients) in + guard let strongSelf = self else { return } + + let hasSignalAccount = signalRecipients.filter({ (signalRecipient) -> Bool in + return signalRecipient.recipientId() == firstPhoneNumber.phoneNumber + }).count > 0 + + if hasSignalAccount { + strongSelf.lookupMode = .lookedUpHasAccount + strongSelf.tryToDetermineMode() + } else { + strongSelf.lookupMode = .lookedUpNoAccount + strongSelf.tryToDetermineMode() + } + }) { [weak self] (error) in + guard let strongSelf = self else { return } + Logger.error("\(strongSelf.logTag) error looking up contact: \(error)") + strongSelf.lookupMode = .notLookingUp + strongSelf.tryToDetermineModeRetry() + } + return + case .lookingUp: + viewMode = .unknown + return + case .lookedUpNoAccount: + viewMode = .nonSystemContactWithoutSignal + return + case .lookedUpHasAccount: + viewMode = .nonSystemContactWithSignal + return + } + } + + private func tryToDetermineModeRetry() { + // Try again after a minute. + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 60.0) { [weak self] in + guard let strongSelf = self else { return } + strongSelf.tryToDetermineMode() + } + } + + private func updateContent() { + SwiftAssertIsOnMainThread(#function) + + for subview in self.view.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 + self.view.addSubview(topView) + topView.autoPinEdge(toSuperviewEdge: .top) + topView.autoPinWidthToSuperview() + + // 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: 0) + avatarView.autoHCenterInSuperview() + avatarView.autoSetDimension(.width, toSize: avatarSize) + avatarView.autoSetDimension(.height, toSize: avatarSize) + + let nameLabel = UILabel() + nameLabel.text = contact.displayName + nameLabel.font = UIFont.ows_dynamicTypeTitle3 + nameLabel.textColor = UIColor.black + nameLabel.lineBreakMode = .byTruncatingTail + nameLabel.textAlignment = .center + topView.addSubview(nameLabel) + nameLabel.autoPinEdge(.top, to: .bottom, of: avatarView, withOffset: 10) + nameLabel.autoPinLeadingToSuperviewMargin() + nameLabel.autoPinTrailingToSuperviewMargin() + + var lastView: UIView = nameLabel + + if let firstPhoneNumber = contact.phoneNumbers?.first { + let phoneNumberLabel = UILabel() + phoneNumberLabel.text = firstPhoneNumber.phoneNumber + phoneNumberLabel.font = UIFont.ows_dynamicTypeCaption1 + phoneNumberLabel.textColor = UIColor.black + phoneNumberLabel.lineBreakMode = .byTruncatingTail + phoneNumberLabel.textAlignment = .center + topView.addSubview(phoneNumberLabel) + phoneNumberLabel.autoPinEdge(.top, to: .bottom, of: lastView, withOffset: 10) + phoneNumberLabel.autoPinLeadingToSuperviewMargin() + phoneNumberLabel.autoPinTrailingToSuperviewMargin() + lastView = phoneNumberLabel + } + + switch viewMode { + case .systemContactWithSignal: + break + case .systemContactWithoutSignal: + break + case .nonSystemContactWithSignal: + break + case .nonSystemContactWithoutSignal: + break + case .noPhoneNumber: + 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: 10) + } + +// acceptIncomingButton = createButton(image: #imageLiteral(resourceName: "call-active-wide"), +// action: #selector(didPressAnswerCall)) +// acceptIncomingButton.accessibilityLabel = NSLocalizedString("CALL_VIEW_ACCEPT_INCOMING_CALL_LABEL", +// comment: "Accessibility label for accepting incoming calls") + +// func createButton(image: UIImage, action: Selector) -> UIButton { +// let button = UIButton() +// button.setImage(image, for: .normal) +// button.imageEdgeInsets = UIEdgeInsets(top: buttonInset(), +// left: buttonInset(), +// bottom: buttonInset(), +// right: buttonInset()) +// button.addTarget(self, action: action, for: .touchUpInside) +// button.autoSetDimension(.width, toSize: buttonSize()) +// button.autoSetDimension(.height, toSize: buttonSize()) +// return button +// } +// +// // MARK: - Layout +// + +// +// func didPressFlipCamera(sender: UIButton) { +// // toggle value +// sender.isSelected = !sender.isSelected +// +// let useBackCamera = sender.isSelected +// Logger.info("\(TAG) in \(#function) with useBackCamera: \(useBackCamera)") +// +// callUIAdapter.setCameraSource(call: call, useBackCamera: useBackCamera) +// } +// +// internal func dismissImmediately(completion: (() -> Void)?) { +// if ContactView.kShowCallViewOnSeparateWindow { +// OWSWindowManager.shared().endCall(self) +// completion?() +// } else { +// self.dismiss(animated: true, completion: completion) +// } +// } +// +} 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.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 0d1c0801a..828c00de2 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -2073,6 +2073,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/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) {