From 5ccecae3f6005d13e90185d5df32f030f768fdba Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 14 Jun 2021 11:11:52 -0300 Subject: [PATCH 1/2] Elaborate the available actions when long-pressing link-ified items in cvc body text. --- .../Individual/IndividualCallService.swift | 4 +- ...ersationViewController+BodyTextItems.swift | 154 ++++++++++++++++-- .../ConversationViewController.m | 2 +- ...ationSettingsViewController+Contents.swift | 6 +- .../translations/en.lproj/Localizable.strings | 18 ++ SignalMessaging/Views/CommonStrings.swift | 3 + SignalMessaging/contacts/OWSContactsManager.h | 6 +- SignalMessaging/contacts/OWSContactsManager.m | 29 +++- .../src/Messages/Interactions/OWSContact.m | 10 +- .../src/Protocols/ContactsManagerProtocol.h | 7 +- .../src/TestUtils/FakeContactsManager.swift | 9 + 11 files changed, 216 insertions(+), 32 deletions(-) diff --git a/Signal/src/Calls/Individual/IndividualCallService.swift b/Signal/src/Calls/Individual/IndividualCallService.swift index 8706903f2a..6df00a211d 100644 --- a/Signal/src/Calls/Individual/IndividualCallService.swift +++ b/Signal/src/Calls/Individual/IndividualCallService.swift @@ -502,7 +502,7 @@ import SignalMessaging var isUnknownCaller = false if call.individualCall.direction == .incoming { - isUnknownCaller = !self.contactsManagerImpl.hasSignalAccount(for: call.individualCall.thread.contactAddress) + isUnknownCaller = !self.contactsManagerImpl.isSystemContactWithSignalAccount(call.individualCall.thread.contactAddress) if isUnknownCaller { Logger.warn("Using relay server because remote user is an unknown caller") } @@ -763,7 +763,7 @@ import SignalMessaging return } call.individualCall.isRemoteSharingScreen = false - + case .reconnecting: self.handleReconnecting(call: call) diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController+BodyTextItems.swift b/Signal/src/ViewControllers/ConversationView/ConversationViewController+BodyTextItems.swift index 3b60af2455..006b373432 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController+BodyTextItems.swift +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController+BodyTextItems.swift @@ -68,8 +68,7 @@ extension ConversationViewController { case .dataItem(let dataItem): switch dataItem.dataType { case .link: - // TODO: Show action sheet with options for links. - didTapLink(dataItem: dataItem) + didLongPressLink(dataItem: dataItem) case .address: // Open in iOS Maps app using URL. // @@ -78,14 +77,7 @@ extension ConversationViewController { // TODO: Show action sheet with options for addresses. UIApplication.shared.open(dataItem.url, options: [:], completionHandler: nil) case .phoneNumber: - // Initiate PSTN call using URL. - // - // https://developer.apple.com/library/archive/featuredarticles/iPhoneURLScheme_Reference/PhoneLinks/PhoneLinks.html - // https://developer.apple.com/library/archive/featuredarticles/iPhoneURLScheme_Reference/SMSLinks/SMSLinks.html - // https://developer.apple.com/library/archive/featuredarticles/iPhoneURLScheme_Reference/FacetimeLinks/FacetimeLinks.html - // - // TODO: Show action sheet with options for phone numbers. - UIApplication.shared.open(dataItem.url, options: [:], completionHandler: nil) + didLongPressPhoneNumber(dataItem: dataItem) case .date: // Open in iOS Calendar app using default URL. // @@ -106,6 +98,141 @@ extension ConversationViewController { } } + // * URL + // * tap - open URL in safari + // * long press - preview + open link in safari / add to reading list / copy link / share + private func didLongPressLink(dataItem: CVBodyTextLabel.DataItem) { + let actionSheet = ActionSheetController(title: dataItem.snippet.strippedOrNil) + + actionSheet.addAction(ActionSheetAction(title: NSLocalizedString("MESSAGE_ACTION_LINK_OPEN_LINK", + comment: "Label for button to open a link."), + accessibilityIdentifier: "link_open_link", + style: .default) { [weak self] _ in + self?.openLink(dataItem: dataItem) + }) + actionSheet.addAction(ActionSheetAction(title: CommonStrings.copyButton, + accessibilityIdentifier: "link_copy", + style: .default) { _ in + UIPasteboard.general.string = dataItem.snippet + // TODO: Show toast? + }) + actionSheet.addAction(ActionSheetAction(title: CommonStrings.shareButton, + accessibilityIdentifier: "link_share", + style: .default) { _ in + AttachmentSharing.showShareUI(for: dataItem.url, sender: self) + }) + actionSheet.addAction(OWSActionSheets.cancelAction) + + presentActionSheet(actionSheet) + } + + // * phone number + // * tap - action sheet with call. + // * long press - show phone number + call PSTN / facetime audio / facetime video / send messages / add to contacts / copy + private func didLongPressPhoneNumber(dataItem: CVBodyTextLabel.DataItem) { + guard let snippet = dataItem.snippet.strippedOrNil, + let phoneNumber = PhoneNumber.tryParsePhoneNumber(fromUserSpecifiedText: snippet), + let e164 = phoneNumber.toE164().strippedOrNil else { + owsFailDebug("Invalid phone number.") + UIApplication.shared.open(dataItem.url, options: [:], completionHandler: nil) + return + } + let address = SignalServiceAddress(phoneNumber: e164) + + if !address.isLocalAddress, + Self.contactsManagerImpl.isKnownRegisteredUserWithSneakyTransaction(address: address) { + let groupViewHelper: GroupViewHelper? = { + guard threadViewModel.isGroupThread else { + return nil + } + let groupViewHelper = GroupViewHelper(threadViewModel: threadViewModel) + groupViewHelper.delegate = self + return groupViewHelper + }() + let actionSheet = MemberActionSheet(address: address, groupViewHelper: groupViewHelper) + actionSheet.present(from: self) + return + } + + let actionSheet = ActionSheetController(title: e164) + + if address.isLocalAddress { + // Show no options. + } else if blockingManager.isAddressBlocked(address) { + actionSheet.addAction(ActionSheetAction(title: NSLocalizedString( + "BLOCK_LIST_UNBLOCK_BUTTON", + comment: "Button label for the 'unblock' button" + ), + accessibilityIdentifier: "phone_number_unblock", + style: .default) { [weak self] _ in + guard let self = self else { return } + BlockListUIUtils.showUnblockAddressActionSheet( + address, + from: self, + completionBlock: nil + ) + }) + } else { + // https://developer.apple.com/library/archive/featuredarticles/iPhoneURLScheme_Reference/PhoneLinks/PhoneLinks.html + actionSheet.addAction(ActionSheetAction(title: NSLocalizedString("MESSAGE_ACTION_PHONE_NUMBER_CALL", + comment: "Label for button to call a phone number."), + accessibilityIdentifier: "phone_number_call", + style: .default) { _ in + guard let url = URL(string: "tel:" + e164) else { + owsFailDebug("Invalid phone number.") + return + } + UIApplication.shared.open(url, options: [:], completionHandler: nil) + }) + // https://developer.apple.com/library/archive/featuredarticles/iPhoneURLScheme_Reference/SMSLinks/SMSLinks.html + actionSheet.addAction(ActionSheetAction(title: NSLocalizedString("MESSAGE_ACTION_PHONE_NUMBER_SMS", + comment: "Label for button to send a text message a phone number."), + accessibilityIdentifier: "phone_number_text_message", + style: .default) { _ in + guard let url = URL(string: "sms:" + e164) else { + owsFailDebug("Invalid phone number.") + return + } + UIApplication.shared.open(url, options: [:], completionHandler: nil) + }) + // https://developer.apple.com/library/archive/featuredarticles/iPhoneURLScheme_Reference/FacetimeLinks/FacetimeLinks.html + actionSheet.addAction(ActionSheetAction(title: NSLocalizedString("MESSAGE_ACTION_PHONE_NUMBER_FACETIME_VIDEO", + comment: "Label for button to make a Facetime video call to a phone number."), + accessibilityIdentifier: "phone_number_facetime_video", + style: .default) { _ in + guard let url = URL(string: "facetime:" + e164) else { + owsFailDebug("Invalid phone number.") + return + } + UIApplication.shared.open(url, options: [:], completionHandler: nil) + }) + actionSheet.addAction(ActionSheetAction(title: NSLocalizedString("MESSAGE_ACTION_PHONE_NUMBER_FACETIME_AUDIO", + comment: "Label for button to make a Facetime audio call to a phone number."), + accessibilityIdentifier: "phone_number_facetime_audio", + style: .default) { _ in + guard let url = URL(string: "facetime-audio:" + e164) else { + owsFailDebug("Invalid phone number.") + return + } + UIApplication.shared.open(url, options: [:], completionHandler: nil) + }) + // TODO: We could show an "add to contact" action for this phone number. + // Ideally we could detect whether this phone number is already in a system contact. + // TODO: We could show an "share" action for this phone number. + } + + actionSheet.addAction(ActionSheetAction(title: CommonStrings.copyButton, + accessibilityIdentifier: "phone_number_copy", + style: .default) { _ in + UIPasteboard.general.string = dataItem.snippet + // TODO: Show toast? + }) + + actionSheet.addAction(OWSActionSheets.cancelAction) + + presentActionSheet(actionSheet) + } + private func didLongPressEmail(dataItem: CVBodyTextLabel.DataItem) { let actionSheet = ActionSheetController(title: dataItem.snippet.strippedOrNil) @@ -129,6 +256,7 @@ extension ConversationViewController { // TODO: We could show (Send Signal Message/Signal call) actions for this email address. // Ideally we could detect whether this email address corresponds to a system contact // which is a registered Signal user. + // TODO: We could show an "share" action for this email address. actionSheet.addAction(OWSActionSheets.cancelAction) @@ -138,6 +266,12 @@ extension ConversationViewController { private func didTapLink(dataItem: CVBodyTextLabel.DataItem) { AssertIsOnMainThread() + openLink(dataItem: dataItem) + } + + private func openLink(dataItem: CVBodyTextLabel.DataItem) { + AssertIsOnMainThread() + if isMailtoUrl(dataItem.url) { didTapEmail(dataItem: dataItem) } else { diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 6bdd675167..0f69673423 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -1116,7 +1116,7 @@ typedef enum : NSUInteger { } // If the user is in the system contacts, show a badge - if ([self.contactsManagerImpl hasSignalAccountForAddress:thread.contactAddress]) { + if ([self.contactsManagerImpl isSystemContactWithAddress:thread.contactAddress]) { icon = [[UIImage imageNamed:@"contact-outline-16"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; } diff --git a/Signal/src/ViewControllers/ThreadSettings/ConversationSettingsViewController+Contents.swift b/Signal/src/ViewControllers/ThreadSettings/ConversationSettingsViewController+Contents.swift index 40b28ff079..acc3e209e0 100644 --- a/Signal/src/ViewControllers/ThreadSettings/ConversationSettingsViewController+Contents.swift +++ b/Signal/src/ViewControllers/ThreadSettings/ConversationSettingsViewController+Contents.swift @@ -21,12 +21,12 @@ extension ConversationSettingsViewController { return !thread.isGroupThread } - private var hasExistingContact: Bool { + private var hasExistingSystemContact: Bool { guard let contactThread = thread as? TSContactThread else { owsFailDebug("Invalid thread.") return false } - return contactsManagerImpl.hasSignalAccount(for: contactThread.contactAddress) + return contactsManagerImpl.isSystemContact(address: contactThread.contactAddress) } // MARK: - Table @@ -175,7 +175,7 @@ extension ConversationSettingsViewController { let contactThread = thread as? TSContactThread, contactsManagerImpl.supportsContactEditing else { return } - if hasExistingContact { + if hasExistingSystemContact { section.add(OWSTableItem(customCellBlock: { [weak self] in guard let self = self else { owsFailDebug("Missing self") diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 7e0fe9dac5..5af5b92799 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -439,6 +439,9 @@ /* Label for the 'set' button. */ "BUTTON_SET" = "Set"; +/* Label for the 'share' button. */ +"BUTTON_SHARE" = "Share"; + /* Label for the 'start' button. */ "BUTTON_START" = "Start"; @@ -2974,6 +2977,21 @@ /* Action sheet button title */ "MESSAGE_ACTION_FORWARD_MESSAGE" = "Forward This Message"; +/* Label for button to open a link. */ +"MESSAGE_ACTION_LINK_OPEN_LINK" = "Open Link"; + +/* Label for button to call a phone number. */ +"MESSAGE_ACTION_PHONE_NUMBER_CALL" = "Call"; + +/* Label for button to make a Facetime audio call to a phone number. */ +"MESSAGE_ACTION_PHONE_NUMBER_FACETIME_AUDIO" = "Facetime Audio"; + +/* Label for button to make a Facetime video call to a phone number. */ +"MESSAGE_ACTION_PHONE_NUMBER_FACETIME_VIDEO" = "Facetime Video"; + +/* Label for button to send a text message a phone number. */ +"MESSAGE_ACTION_PHONE_NUMBER_SMS" = "Send Text Message"; + /* Action sheet button title */ "MESSAGE_ACTION_REPLY" = "Reply to This Message"; diff --git a/SignalMessaging/Views/CommonStrings.swift b/SignalMessaging/Views/CommonStrings.swift index 8db40fee90..9b03cdaa17 100644 --- a/SignalMessaging/Views/CommonStrings.swift +++ b/SignalMessaging/Views/CommonStrings.swift @@ -68,6 +68,9 @@ import Foundation static public let saveButton = NSLocalizedString("ALERT_SAVE", comment: "The label for the 'save' button in action sheets.") + @objc + static public let shareButton = NSLocalizedString("BUTTON_SHARE", comment: "Label for the 'share' button.") + @objc static public let help = NSLocalizedString("SETTINGS_HELP", comment: "Title for help button and help pages in app settings.") diff --git a/SignalMessaging/contacts/OWSContactsManager.h b/SignalMessaging/contacts/OWSContactsManager.h index 5956931eae..885f6c8285 100644 --- a/SignalMessaging/contacts/OWSContactsManager.h +++ b/SignalMessaging/contacts/OWSContactsManager.h @@ -48,8 +48,6 @@ extern NSString *const OWSContactsManagerSignalAccountsDidChangeNotification; // This will always return an instance of SignalAccount. - (SignalAccount *)fetchOrBuildSignalAccountForAddress:(SignalServiceAddress *)address; -- (BOOL)hasSignalAccountForAddress:(SignalServiceAddress *)address; -- (BOOL)hasSignalAccountForAddress:(SignalServiceAddress *)address transaction:(SDSAnyReadTransaction *)transaction; #pragma mark - System Contact Fetching @@ -100,6 +98,10 @@ extern NSString *const OWSContactsManagerSignalAccountsDidChangeNotification; transaction:(SDSAnyReadTransaction *)transaction; - (nullable UIImage *)imageForAddressWithSneakyTransaction:(nullable SignalServiceAddress *)address; +- (BOOL)isKnownRegisteredUserWithSneakyTransaction:(SignalServiceAddress *)address + NS_SWIFT_NAME(isKnownRegisteredUserWithSneakyTransaction(address:)); +- (BOOL)isKnownRegisteredUser:(SignalServiceAddress *)address transaction:(SDSAnyReadTransaction *)transaction; + @end NS_ASSUME_NONNULL_END diff --git a/SignalMessaging/contacts/OWSContactsManager.m b/SignalMessaging/contacts/OWSContactsManager.m index beba10fc79..9680220fea 100644 --- a/SignalMessaging/contacts/OWSContactsManager.m +++ b/SignalMessaging/contacts/OWSContactsManager.m @@ -922,19 +922,19 @@ NSString *const OWSContactsManagerKeyNextFullIntersectionDate = @"OWSContactsMan return [self isSystemContactWithPhoneNumber:phoneNumber]; } -- (BOOL)isSystemContactWithSignalAccount:(NSString *)phoneNumber +- (BOOL)isSystemContactWithSignalAccount:(SignalServiceAddress *)address { - OWSAssertDebug(phoneNumber.length > 0); + OWSAssertDebug(address.isValid); - return [self hasSignalAccountForAddress:[[SignalServiceAddress alloc] initWithPhoneNumber:phoneNumber]]; + return [self hasSignalAccountForAddress:address]; } -- (BOOL)isSystemContactWithSignalAccount:(NSString *)phoneNumber transaction:(SDSAnyReadTransaction *)transaction +- (BOOL)isSystemContactWithSignalAccount:(SignalServiceAddress *)address + transaction:(SDSAnyReadTransaction *)transaction { - OWSAssertDebug(phoneNumber.length > 0); + OWSAssertDebug(address.isValid); - return [self hasSignalAccountForAddress:[[SignalServiceAddress alloc] initWithPhoneNumber:phoneNumber] - transaction:transaction]; + return [self hasSignalAccountForAddress:address transaction:transaction]; } - (BOOL)hasNameInSystemContactsForAddress:(SignalServiceAddress *)address @@ -1358,6 +1358,19 @@ NSString *const OWSContactsManagerKeyNextFullIntersectionDate = @"OWSContactsMan return [self displayNameForAddress:signalAccount.recipientAddress transaction:transaction]; } -NS_ASSUME_NONNULL_END +- (BOOL)isKnownRegisteredUserWithSneakyTransaction:(SignalServiceAddress *)address +{ + __block BOOL result; + [self.databaseStorage readWithBlock:^( + SDSAnyReadTransaction *transaction) { result = [self isKnownRegisteredUser:address transaction:transaction]; }]; + return result; +} + +- (BOOL)isKnownRegisteredUser:(SignalServiceAddress *)address transaction:(SDSAnyReadTransaction *)transaction +{ + return [SignalRecipient isRegisteredRecipient:address transaction:transaction]; +} @end + +NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Messages/Interactions/OWSContact.m b/SignalServiceKit/src/Messages/Interactions/OWSContact.m index eac1ea0c55..4912aea8d5 100644 --- a/SignalServiceKit/src/Messages/Interactions/OWSContact.m +++ b/SignalServiceKit/src/Messages/Interactions/OWSContact.m @@ -544,18 +544,20 @@ NSString *NSStringForContactAddressType(OWSContactAddressType value) - (NSArray *)systemContactsWithSignalAccountPhoneNumbers { return [self.e164PhoneNumbers - filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSString *_Nullable recipientId, + filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSString *_Nullable phoneNumber, NSDictionary *_Nullable bindings) { - return [OWSContact.contactsManager isSystemContactWithSignalAccount:recipientId]; + SignalServiceAddress *address = [[SignalServiceAddress alloc] initWithPhoneNumber:phoneNumber]; + return [OWSContact.contactsManager isSystemContactWithSignalAccount:address]; }]]; } - (NSArray *)systemContactsWithSignalAccountPhoneNumbersWithTransaction:(SDSAnyReadTransaction *)transaction { return [self.e164PhoneNumbers - filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSString *_Nullable recipientId, + filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSString *_Nullable phoneNumber, NSDictionary *_Nullable bindings) { - return [OWSContact.contactsManager isSystemContactWithSignalAccount:recipientId transaction:transaction]; + SignalServiceAddress *address = [[SignalServiceAddress alloc] initWithPhoneNumber:phoneNumber]; + return [OWSContact.contactsManager isSystemContactWithSignalAccount:address transaction:transaction]; }]]; } diff --git a/SignalServiceKit/src/Protocols/ContactsManagerProtocol.h b/SignalServiceKit/src/Protocols/ContactsManagerProtocol.h index 20c71e1c9e..71088eecd0 100644 --- a/SignalServiceKit/src/Protocols/ContactsManagerProtocol.h +++ b/SignalServiceKit/src/Protocols/ContactsManagerProtocol.h @@ -53,8 +53,11 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)isSystemContactWithPhoneNumber:(NSString *)phoneNumber NS_SWIFT_NAME(isSystemContact(phoneNumber:)); - (BOOL)isSystemContactWithAddress:(SignalServiceAddress *)address NS_SWIFT_NAME(isSystemContact(address:)); -- (BOOL)isSystemContactWithSignalAccount:(NSString *)phoneNumber; -- (BOOL)isSystemContactWithSignalAccount:(NSString *)phoneNumber transaction:(SDSAnyReadTransaction *)transaction; +- (BOOL)isSystemContactWithSignalAccount:(SignalServiceAddress *)address + NS_SWIFT_NAME(isSystemContactWithSignalAccount(_:)); +- (BOOL)isSystemContactWithSignalAccount:(SignalServiceAddress *)address + transaction:(SDSAnyReadTransaction *)transaction + NS_SWIFT_NAME(isSystemContactWithSignalAccount(_:transaction:)); - (BOOL)hasNameInSystemContactsForAddress:(SignalServiceAddress *)address; - (BOOL)hasNameInSystemContactsForAddress:(SignalServiceAddress *)address transaction:(SDSAnyReadTransaction *)transaction; diff --git a/SignalServiceKit/src/TestUtils/FakeContactsManager.swift b/SignalServiceKit/src/TestUtils/FakeContactsManager.swift index a1c7a48ab6..6bf79774ea 100644 --- a/SignalServiceKit/src/TestUtils/FakeContactsManager.swift +++ b/SignalServiceKit/src/TestUtils/FakeContactsManager.swift @@ -66,6 +66,15 @@ public class FakeContactsManager: NSObject, ContactsManagerProtocol { return true } + public func isSystemContactWithSignalAccount(_ address: SignalServiceAddress) -> Bool { + return true + } + + public func isSystemContactWithSignalAccount(_ address: SignalServiceAddress, + transaction: SDSAnyReadTransaction) -> Bool { + return true + } + public func sortSignalServiceAddresses(_ addresses: [SignalServiceAddress], transaction: SDSAnyReadTransaction) -> [SignalServiceAddress] { return addresses From ed22200444b9888a59ed6d4f413ba9c39cb77165 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 17 Jun 2021 10:04:03 -0300 Subject: [PATCH 2/2] Respond to CR. --- .../ConversationViewController+BodyTextItems.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController+BodyTextItems.swift b/Signal/src/ViewControllers/ConversationView/ConversationViewController+BodyTextItems.swift index 006b373432..80c725716d 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController+BodyTextItems.swift +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController+BodyTextItems.swift @@ -139,7 +139,7 @@ extension ConversationViewController { } let address = SignalServiceAddress(phoneNumber: e164) - if !address.isLocalAddress, + if address.isLocalAddress || Self.contactsManagerImpl.isKnownRegisteredUserWithSneakyTransaction(address: address) { let groupViewHelper: GroupViewHelper? = { guard threadViewModel.isGroupThread else {