diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index b7543a402..a8de933c9 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -81,6 +81,7 @@ 34DFCB851E8E04B500053165 /* AddToBlockListViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34DFCB841E8E04B500053165 /* AddToBlockListViewController.m */; }; 34E3E5681EC4B19400495BAC /* AudioProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34E3E5671EC4B19400495BAC /* AudioProgressView.swift */; }; 34E8BF381EE9E2FD00F5F4CA /* FingerprintViewScanController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34E8BF371EE9E2FD00F5F4CA /* FingerprintViewScanController.m */; }; + 34E8BF3B1EEB208E00F5F4CA /* DebugUIVerification.m in Sources */ = {isa = PBXBuildFile; fileRef = 34E8BF3A1EEB208E00F5F4CA /* DebugUIVerification.m */; }; 34F3089C1ECA4CDB00BB7697 /* TSUnreadIndicatorInteraction.m in Sources */ = {isa = PBXBuildFile; fileRef = 34F3089B1ECA4CDB00BB7697 /* TSUnreadIndicatorInteraction.m */; }; 34F3089F1ECA580B00BB7697 /* OWSUnreadIndicatorCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 34F3089E1ECA580B00BB7697 /* OWSUnreadIndicatorCell.m */; }; 34F308A21ECB469700BB7697 /* OWSBezierPathView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34F308A11ECB469700BB7697 /* OWSBezierPathView.m */; }; @@ -494,6 +495,8 @@ 34E3E5671EC4B19400495BAC /* AudioProgressView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioProgressView.swift; sourceTree = ""; }; 34E8BF361EE9E2FD00F5F4CA /* FingerprintViewScanController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FingerprintViewScanController.h; sourceTree = ""; }; 34E8BF371EE9E2FD00F5F4CA /* FingerprintViewScanController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FingerprintViewScanController.m; sourceTree = ""; }; + 34E8BF391EEB208E00F5F4CA /* DebugUIVerification.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUIVerification.h; sourceTree = ""; }; + 34E8BF3A1EEB208E00F5F4CA /* DebugUIVerification.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugUIVerification.m; sourceTree = ""; }; 34F3089A1ECA4CDB00BB7697 /* TSUnreadIndicatorInteraction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSUnreadIndicatorInteraction.h; sourceTree = ""; }; 34F3089B1ECA4CDB00BB7697 /* TSUnreadIndicatorInteraction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSUnreadIndicatorInteraction.m; sourceTree = ""; }; 34F3089D1ECA580B00BB7697 /* OWSUnreadIndicatorCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSUnreadIndicatorCell.h; sourceTree = ""; }; @@ -1030,10 +1033,12 @@ 34D8C02A1ED3685800188D7C /* DebugUIContacts.m */, 34D8C0231ED3673300188D7C /* DebugUIMessages.h */, 34D8C0241ED3673300188D7C /* DebugUIMessages.m */, - 34D8C0251ED3673300188D7C /* DebugUITableViewController.h */, - 34D8C0261ED3673300188D7C /* DebugUITableViewController.m */, 452037CF1EE84975004E4CDF /* DebugUISessionState.h */, 452037D01EE84975004E4CDF /* DebugUISessionState.m */, + 34D8C0251ED3673300188D7C /* DebugUITableViewController.h */, + 34D8C0261ED3673300188D7C /* DebugUITableViewController.m */, + 34E8BF391EEB208E00F5F4CA /* DebugUIVerification.h */, + 34E8BF3A1EEB208E00F5F4CA /* DebugUIVerification.m */, ); path = DebugUI; sourceTree = ""; @@ -2099,6 +2104,7 @@ 34B3F8931E8DF1710035BE1A /* SignalsNavigationController.m in Sources */, 76EB063A18170B33006006FC /* FunctionalUtil.m in Sources */, 34F308A21ECB469700BB7697 /* OWSBezierPathView.m in Sources */, + 34E8BF3B1EEB208E00F5F4CA /* DebugUIVerification.m in Sources */, 76EB058A18170B33006006FC /* Release.m in Sources */, 45D231771DC7E8F10034FA89 /* SessionResetJob.swift in Sources */, 450873C71D9D867B006B54F2 /* OWSIncomingMessageCollectionViewCell.m in Sources */, diff --git a/Signal/src/ViewControllers/ContactsViewHelper.m b/Signal/src/ViewControllers/ContactsViewHelper.m index 080e7468f..f99143fba 100644 --- a/Signal/src/ViewControllers/ContactsViewHelper.m +++ b/Signal/src/ViewControllers/ContactsViewHelper.m @@ -79,18 +79,18 @@ NS_ASSUME_NONNULL_BEGIN - (void)signalAccountsDidChange:(NSNotification *)notification { - dispatch_async(dispatch_get_main_queue(), ^{ - [self updateContacts]; - }); + OWSAssert([NSThread isMainThread]); + + [self updateContacts]; } - (void)blockedPhoneNumbersDidChange:(id)notification { - dispatch_async(dispatch_get_main_queue(), ^{ - self.blockedPhoneNumbers = [_blockingManager blockedPhoneNumbers]; + OWSAssert([NSThread isMainThread]); + + self.blockedPhoneNumbers = [_blockingManager blockedPhoneNumbers]; - [self updateContacts]; - }); + [self updateContacts]; } #pragma mark - Contacts diff --git a/Signal/src/ViewControllers/ConversationView/MessagesViewController.m b/Signal/src/ViewControllers/ConversationView/MessagesViewController.m index 15e017a25..a0f0470c0 100644 --- a/Signal/src/ViewControllers/ConversationView/MessagesViewController.m +++ b/Signal/src/ViewControllers/ConversationView/MessagesViewController.m @@ -182,7 +182,7 @@ typedef enum : NSUInteger { @property (nonatomic) UILabel *navigationBarTitleLabel; @property (nonatomic) UILabel *navigationBarSubtitleLabel; @property (nonatomic) UIButton *attachButton; -@property (nonatomic) UIView *blockStateIndicator; +@property (nonatomic) UIView *bannerView; // Back Button Unread Count @property (nonatomic, readonly) UIView *backButtonUnreadCountView; @@ -294,9 +294,9 @@ typedef enum : NSUInteger { - (void)blockedPhoneNumbersDidChange:(id)notification { - dispatch_async(dispatch_get_main_queue(), ^{ - [self ensureBlockStateIndicator]; - }); + OWSAssert([NSThread isMainThread]); + + [self ensureBannerState]; } - (void)identityStateDidChange:(NSNotification *)notification @@ -304,6 +304,7 @@ typedef enum : NSUInteger { OWSAssert([NSThread isMainThread]); [self updateNavigationBarSubtitleLabel]; + [self ensureBannerState]; } - (void)peekSetup @@ -543,7 +544,7 @@ typedef enum : NSUInteger { // Triggering modified notification renders "call notification" when leaving full screen call view [self.thread touch]; - [self ensureBlockStateIndicator]; + [self ensureBannerState]; [self resetContentAndLayout]; @@ -671,20 +672,52 @@ typedef enum : NSUInteger { { _userHasScrolled = userHasScrolled; - [self ensureBlockStateIndicator]; + [self ensureBannerState]; } -- (void)ensureBlockStateIndicator +- (void)ensureBannerState { // This method should be called rarely, so it's simplest to discard and // rebuild the indicator view every time. - [self.blockStateIndicator removeFromSuperview]; - self.blockStateIndicator = nil; + [self.bannerView removeFromSuperview]; + self.bannerView = nil; if (self.userHasScrolled) { return; } + // A collection of the group members who are "no longer verified". + NSMutableArray *noLongerVerifiedRecipientIds = [NSMutableArray new]; + for (NSString *recipientId in self.thread.recipientIdentifiers) { + if ([[OWSIdentityManager sharedManager] verificationStateForRecipientId:recipientId] + == OWSVerificationStateNoLongerVerified) { + [noLongerVerifiedRecipientIds addObject:recipientId]; + } + } + if (noLongerVerifiedRecipientIds.count > 0) { + NSString *message; + if (noLongerVerifiedRecipientIds.count > 1) { + message = NSLocalizedString(@"MESSAGES_VIEW_N_MEMBERS_NO_LONGER_VERIFIED", + @"Indicates that more than one member of this group conversation is no longer verified."); + } else { + NSString *recipientId = [noLongerVerifiedRecipientIds firstObject]; + NSString *displayName = [self.contactsManager displayNameForPhoneIdentifier:recipientId]; + NSString *format + = (self.isGroupConversation ? NSLocalizedString(@"MESSAGES_VIEW_1_MEMBER_NO_LONGER_VERIFIED_FORMAT", + @"Indicates that one member of this group conversation is no longer " + @"verified. Embeds {{user's name or phone number}}.") + : NSLocalizedString(@"MESSAGES_VIEW_CONTACT_NO_LONGER_VERIFIED_FORMAT", + @"Indicates that this 1:1 conversation is no longer verified. Embeds " + @"{{user's name or phone number}}.")); + message = [NSString stringWithFormat:format, displayName]; + } + + [self createBannerWithTitle:message + bannerColor:[UIColor ows_destructiveRedColor] + tapSelector:@selector(noLongerVerifiedBannerViewWasTapped:)]; + return; + } + NSString *blockStateMessage = nil; if ([self isBlockedContactConversation]) { blockStateMessage = NSLocalizedString( @@ -704,41 +737,61 @@ typedef enum : NSUInteger { } if (blockStateMessage) { - UILabel *label = [UILabel new]; - label.font = [UIFont ows_mediumFontWithSize:14.f]; - label.text = blockStateMessage; - label.textColor = [UIColor whiteColor]; + [self createBannerWithTitle:blockStateMessage + bannerColor:[UIColor ows_destructiveRedColor] + tapSelector:@selector(blockBannerViewWasTapped:)]; + } +} + +- (void)createBannerWithTitle:(NSString *)title bannerColor:(UIColor *)bannerColor tapSelector:(SEL)tapSelector +{ + OWSAssert(title.length > 0); + OWSAssert(bannerColor); + + UILabel *label = [UILabel new]; + label.font = [UIFont ows_mediumFontWithSize:14.f]; + label.text = title; + label.textColor = [UIColor whiteColor]; + label.numberOfLines = 0; + label.lineBreakMode = NSLineBreakByWordWrapping; + label.textAlignment = NSTextAlignmentCenter; - UIView *blockStateIndicator = [UIView new]; - blockStateIndicator.backgroundColor = [UIColor ows_redColor]; - blockStateIndicator.layer.cornerRadius = 2.5f; + UIView *bannerView = [UIView new]; + bannerView.backgroundColor = bannerColor; + bannerView.layer.cornerRadius = 2.5f; - // Use a shadow to "pop" the indicator above the other views. - blockStateIndicator.layer.shadowColor = [UIColor blackColor].CGColor; - blockStateIndicator.layer.shadowOffset = CGSizeMake(2, 3); - blockStateIndicator.layer.shadowRadius = 2.f; - blockStateIndicator.layer.shadowOpacity = 0.35f; + // Use a shadow to "pop" the indicator above the other views. + bannerView.layer.shadowColor = [UIColor blackColor].CGColor; + bannerView.layer.shadowOffset = CGSizeMake(2, 3); + bannerView.layer.shadowRadius = 2.f; + bannerView.layer.shadowOpacity = 0.35f; - [blockStateIndicator addSubview:label]; - [label autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:5]; - [label autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:5]; - [label autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:15]; - [label autoPinEdgeToSuperviewEdge:ALEdgeRight withInset:15]; + [bannerView addSubview:label]; + [label autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:5]; + [label autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:5]; + const CGFloat kBannerHPadding = 15.f; + [label autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:kBannerHPadding]; + [label autoPinEdgeToSuperviewEdge:ALEdgeRight withInset:kBannerHPadding]; - [blockStateIndicator addGestureRecognizer:[[UITapGestureRecognizer alloc] - initWithTarget:self - action:@selector(blockStateIndicatorWasTapped:)]]; + [bannerView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:tapSelector]]; - [self.view addSubview:blockStateIndicator]; - [blockStateIndicator autoHCenterInSuperview]; - [blockStateIndicator autoPinToTopLayoutGuideOfViewController:self withInset:10]; - [self.view layoutSubviews]; + [self.view addSubview:bannerView]; + [bannerView autoPinToTopLayoutGuideOfViewController:self withInset:10]; + [bannerView autoHCenterInSuperview]; - self.blockStateIndicator = blockStateIndicator; + CGFloat labelDesiredWidth = [label sizeThatFits:CGSizeZero].width; + CGFloat bannerDesiredWidth = labelDesiredWidth + kBannerHPadding * 2.f; + const CGFloat kMinBannerHMargin = 20.f; + if (bannerDesiredWidth + kMinBannerHMargin * 2.f >= self.view.width) { + [bannerView autoPinWidthToSuperviewWithMargin:kMinBannerHMargin]; } + + [self.view layoutSubviews]; + + self.bannerView = bannerView; } -- (void)blockStateIndicatorWasTapped:(UIGestureRecognizer *)sender +- (void)blockBannerViewWasTapped:(UIGestureRecognizer *)sender { if (sender.state != UIGestureRecognizerStateRecognized) { return; @@ -758,6 +811,13 @@ typedef enum : NSUInteger { } } +- (void)noLongerVerifiedBannerViewWasTapped:(UIGestureRecognizer *)sender +{ + if (sender.state == UIGestureRecognizerStateRecognized) { + [self showConversationSettingsAndShowVerification:YES]; + } +} + - (void)showUnblockContactUI:(BlockActionCompletionBlock)completionBlock { OWSAssert([self.thread isKindOfClass:[TSContactThread class]]); @@ -1213,11 +1273,7 @@ typedef enum : NSUInteger { // return from FingerprintViewController. [self dismissKeyBoard]; - FingerprintViewController *fingerprintViewController = [FingerprintViewController new]; - [fingerprintViewController configureWithRecipientId:recipientId]; - UINavigationController *navigationController = - [[UINavigationController alloc] initWithRootViewController:fingerprintViewController]; - [self presentViewController:navigationController animated:YES completion:nil]; + [FingerprintViewController showVerificationViewFromViewController:self recipientId:recipientId]; } #pragma mark - Calls @@ -1792,6 +1848,11 @@ typedef enum : NSUInteger { #pragma mark - Actions - (void)showConversationSettings +{ + [self showConversationSettingsAndShowVerification:NO]; +} + +- (void)showConversationSettingsAndShowVerification:(BOOL)showVerification { if (self.userLeftGroup) { DDLogDebug(@"%@ Ignoring request to show conversation settings, since user left group", self.tag); @@ -1801,6 +1862,7 @@ typedef enum : NSUInteger { OWSConversationSettingsTableViewController *settingsVC = [OWSConversationSettingsTableViewController new]; settingsVC.conversationSettingsViewDelegate = self; [settingsVC configureWithThread:self.thread]; + settingsVC.showVerificationOnAppear = showVerification; [self.navigationController pushViewController:settingsVC animated:YES]; } diff --git a/Signal/src/ViewControllers/DebugUI/DebugUITableViewController.m b/Signal/src/ViewControllers/DebugUI/DebugUITableViewController.m index 58b3ba264..9ed3c5e8c 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUITableViewController.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUITableViewController.m @@ -6,6 +6,7 @@ #import "DebugUIContacts.h" #import "DebugUIMessages.h" #import "DebugUISessionState.h" +#import "DebugUIVerification.h" #import "Signal-Swift.h" #import #import @@ -38,41 +39,43 @@ NS_ASSUME_NONNULL_BEGIN [self.navigationController pushViewController:viewController animated:YES]; } ++ (OWSTableItem *)itemForSubsection:(OWSTableSection *)section + viewController:(DebugUITableViewController *)viewController +{ + OWSAssert(section); + + __weak DebugUITableViewController *weakSelf = viewController; + return [OWSTableItem disclosureItemWithText:section.headerTitle + actionBlock:^{ + [weakSelf pushPageWithSection:section]; + }]; +} + + (void)presentDebugUIForThread:(TSThread *)thread fromViewController:(UIViewController *)fromViewController { OWSAssert(thread); OWSAssert(fromViewController); DebugUITableViewController *viewController = [DebugUITableViewController new]; - __weak DebugUITableViewController *weakSelf = viewController; OWSTableContents *contents = [OWSTableContents new]; contents.title = @"Debug: Conversation"; - OWSTableSection *messagesSection = [DebugUIMessages sectionForThread:thread]; - [contents addSection:[OWSTableSection - sectionWithTitle:messagesSection.headerTitle - items:@[ - [OWSTableItem disclosureItemWithText:messagesSection.headerTitle - actionBlock:^{ - [weakSelf pushPageWithSection:messagesSection]; - }], - ]]]; - + NSMutableArray *subsectionItems = [NSMutableArray new]; + [subsectionItems + addObject:[self itemForSubsection:[DebugUIMessages sectionForThread:thread] viewController:viewController]]; + [subsectionItems addObject:[self itemForSubsection:[DebugUIContacts section] viewController:viewController]]; if ([thread isKindOfClass:[TSContactThread class]]) { TSContactThread *contactThread = (TSContactThread *)thread; - OWSTableSection *sessionSection = [DebugUISessionState sectionForContactThread:contactThread]; - [contents addSection:[OWSTableSection - sectionWithTitle:sessionSection.headerTitle - items:@[ - [OWSTableItem disclosureItemWithText:sessionSection.headerTitle - actionBlock:^{ - [weakSelf - pushPageWithSection:sessionSection]; - }], - ]]]; + [subsectionItems addObject:[self itemForSubsection:[DebugUISessionState sectionForContactThread:contactThread] + viewController:viewController]]; + [subsectionItems addObject:[self itemForSubsection:[DebugUIVerification sectionForThread:contactThread] + viewController:viewController]]; + } + [contents addSection:[OWSTableSection sectionWithTitle:@"Sections" items:subsectionItems]]; + if ([thread isKindOfClass:[TSContactThread class]]) { // After enqueing the notification you may want to background the app or lock the screen before it triggers, so // we give a little delay. uint64_t notificationDelay = 5; @@ -138,8 +141,6 @@ NS_ASSUME_NONNULL_BEGIN ]]]; } // end contact thread section - [contents addSection:[DebugUIContacts section]]; - viewController.contents = contents; [viewController presentFromViewController:fromViewController]; } diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIVerification.h b/Signal/src/ViewControllers/DebugUI/DebugUIVerification.h new file mode 100644 index 000000000..609755b1f --- /dev/null +++ b/Signal/src/ViewControllers/DebugUI/DebugUIVerification.h @@ -0,0 +1,17 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import "OWSTableViewController.h" + +NS_ASSUME_NONNULL_BEGIN + +@class TSContactThread; + +@interface DebugUIVerification : NSObject + ++ (OWSTableSection *)sectionForThread:(TSContactThread *)thread; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIVerification.m b/Signal/src/ViewControllers/DebugUI/DebugUIVerification.m new file mode 100644 index 000000000..9f6890c95 --- /dev/null +++ b/Signal/src/ViewControllers/DebugUI/DebugUIVerification.m @@ -0,0 +1,77 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import "DebugUIVerification.h" +#import "DebugUIMessages.h" +#import "Signal-Swift.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation DebugUIVerification + +#pragma mark - Logging + ++ (NSString *)tag +{ + return [NSString stringWithFormat:@"[%@]", self.class]; +} + +- (NSString *)tag +{ + return self.class.tag; +} + +#pragma mark - Factory Methods + ++ (OWSTableSection *)sectionForThread:(TSContactThread *)thread +{ + OWSAssert(thread); + + NSString *recipientId = thread.contactIdentifier; + OWSAssert(recipientId.length > 0); + + return [OWSTableSection + sectionWithTitle:@"Verification" + items:@[ + [OWSTableItem itemWithTitle:@"Default" + actionBlock:^{ + [DebugUIVerification setVerificationState:OWSVerificationStateDefault + recipientId:recipientId]; + }], + [OWSTableItem itemWithTitle:@"Verified" + actionBlock:^{ + [DebugUIVerification setVerificationState:OWSVerificationStateVerified + recipientId:recipientId]; + }], + [OWSTableItem itemWithTitle:@"No Longer Verified" + actionBlock:^{ + [DebugUIVerification + setVerificationState:OWSVerificationStateNoLongerVerified + recipientId:recipientId]; + }], + ]]; +} + ++ (void)setVerificationState:(OWSVerificationState)verificationState recipientId:(NSString *)recipientId +{ + OWSAssert(recipientId.length > 0); + + OWSRecipientIdentity *_Nullable recipientIdentity = + [[OWSIdentityManager sharedManager] recipientIdentityForRecipientId:recipientId]; + OWSAssert(recipientIdentity); + // By capturing the identity key when we enter these views, we prevent the edge case + // where the user verifies a key that we learned about while this view was open. + NSData *identityKey = recipientIdentity.identityKey; + OWSAssert(identityKey.length > 0); + + [OWSIdentityManager.sharedManager setVerificationState:verificationState + identityKey:identityKey + recipientId:recipientId + sendSyncMessage:verificationState != OWSVerificationStateNoLongerVerified]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/FingerprintViewController.h b/Signal/src/ViewControllers/FingerprintViewController.h index c9c2a2374..e612cd3ae 100644 --- a/Signal/src/ViewControllers/FingerprintViewController.h +++ b/Signal/src/ViewControllers/FingerprintViewController.h @@ -6,7 +6,7 @@ NS_ASSUME_NONNULL_BEGIN @interface FingerprintViewController : UIViewController -- (void)configureWithRecipientId:(NSString *)recipientId NS_SWIFT_NAME(configure(recipientId:)); ++ (void)showVerificationViewFromViewController:(UIViewController *)viewController recipientId:(NSString *)recipientId; @end diff --git a/Signal/src/ViewControllers/FingerprintViewController.m b/Signal/src/ViewControllers/FingerprintViewController.m index a0a1ea0cd..e7a193d90 100644 --- a/Signal/src/ViewControllers/FingerprintViewController.m +++ b/Signal/src/ViewControllers/FingerprintViewController.m @@ -89,6 +89,27 @@ typedef void (^CustomLayoutBlock)(); @implementation FingerprintViewController ++ (void)showVerificationViewFromViewController:(UIViewController *)viewController recipientId:(NSString *)recipientId +{ + OWSAssert(recipientId.length > 0); + + OWSRecipientIdentity *_Nullable recipientIdentity = + [[OWSIdentityManager sharedManager] recipientIdentityForRecipientId:recipientId]; + if (!recipientIdentity) { + [OWSAlerts showAlertWithTitle:NSLocalizedString(@"CANT_VERIFY_IDENTITY_ALERT_TITLE", + @"Title for alert explaining that a user cannot be verified.") + message:NSLocalizedString(@"CANT_VERIFY_IDENTITY_ALERT_MESSAGE", + @"Message for alert explaining that a user cannot be verified.")]; + return; + } + + FingerprintViewController *fingerprintViewController = [FingerprintViewController new]; + [fingerprintViewController configureWithRecipientId:recipientId]; + UINavigationController *navigationController = + [[UINavigationController alloc] initWithRootViewController:fingerprintViewController]; + [viewController presentViewController:navigationController animated:YES completion:nil]; +} + - (instancetype)init { self = [super init]; @@ -493,6 +514,8 @@ typedef void (^CustomLayoutBlock)(); :self.identityKey recipientId:self.recipientId sendSyncMessage:YES]; + + [self dismissViewControllerAnimated:YES completion:nil]; } } diff --git a/Signal/src/ViewControllers/OWSConversationSettingsTableViewController.h b/Signal/src/ViewControllers/OWSConversationSettingsTableViewController.h index e94c19c99..6ab122765 100644 --- a/Signal/src/ViewControllers/OWSConversationSettingsTableViewController.h +++ b/Signal/src/ViewControllers/OWSConversationSettingsTableViewController.h @@ -14,6 +14,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, weak) id conversationSettingsViewDelegate; +@property (nonatomic) BOOL showVerificationOnAppear; + - (void)configureWithThread:(TSThread *)thread; @end diff --git a/Signal/src/ViewControllers/OWSConversationSettingsTableViewController.m b/Signal/src/ViewControllers/OWSConversationSettingsTableViewController.m index 67c961fce..9f351e6bf 100644 --- a/Signal/src/ViewControllers/OWSConversationSettingsTableViewController.m +++ b/Signal/src/ViewControllers/OWSConversationSettingsTableViewController.m @@ -95,6 +95,21 @@ NS_ASSUME_NONNULL_BEGIN _messageSender = [Environment getCurrent].messageSender; _blockingManager = [OWSBlockingManager sharedManager]; _contactsViewHelper = [[ContactsViewHelper alloc] initWithDelegate:self]; + + [self observeNotifications]; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)observeNotifications +{ + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(identityStateDidChange:) + name:kNSNotificationName_IdentityStateDidChange + object:nil]; } - (NSString *)threadName @@ -196,6 +211,20 @@ NS_ASSUME_NONNULL_BEGIN [self updateTableContents]; } +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + + if (self.showVerificationOnAppear) { + self.showVerificationOnAppear = NO; + if (self.isGroupThread) { + [self showGroupMembersView]; + } else { + [self showVerificationView]; + } + } +} + - (void)updateTableContents { OWSTableContents *contents = [OWSTableContents new]; @@ -211,23 +240,16 @@ NS_ASSUME_NONNULL_BEGIN firstSection.customHeaderHeight = @(100.f); if (!self.isGroupThread && self.thread.hasSafetyNumbers) { - [firstSection - addItem:[OWSTableItem itemWithCustomCellBlock:^{ - return [weakSelf disclosureCellWithName:NSLocalizedString(@"VERIFY_PRIVACY", - @"table cell label in conversation settings") - iconName:@"table_ic_verify"]; - } - actionBlock:^{ - OWSConversationSettingsTableViewController *strongSelf = weakSelf; - if (!strongSelf) { - return; - } - FingerprintViewController *fingerprintViewController = [FingerprintViewController new]; - [fingerprintViewController configureWithRecipientId:strongSelf.thread.contactIdentifier]; - UINavigationController *navigationController = - [[UINavigationController alloc] initWithRootViewController:fingerprintViewController]; - [strongSelf presentViewController:navigationController animated:YES completion:nil]; - }]]; + [firstSection addItem:[OWSTableItem itemWithCustomCellBlock:^{ + return [weakSelf + disclosureCellWithName: + NSLocalizedString(@"VERIFY_PRIVACY", + @"Label for button or row which allows users to verify the safety number of another user.") + iconName:@"table_ic_verify"]; + } + actionBlock:^{ + [weakSelf showVerificationView]; + }]]; } [firstSection @@ -349,14 +371,7 @@ NS_ASSUME_NONNULL_BEGIN iconName:@"table_ic_group_members"]; } actionBlock:^{ - OWSConversationSettingsTableViewController *strongSelf = weakSelf; - if (!strongSelf) { - return; - } - ShowGroupMembersViewController *showGroupMembersViewController = - [ShowGroupMembersViewController new]; - [showGroupMembersViewController configWithThread:(TSGroupThread *)strongSelf.thread]; - [strongSelf.navigationController pushViewController:showGroupMembersViewController animated:YES]; + [weakSelf showGroupMembersView]; }], [OWSTableItem itemWithCustomCellBlock:^{ return [weakSelf disclosureCellWithName:NSLocalizedString(@"LEAVE_GROUP_ACTION", @@ -535,24 +550,52 @@ NS_ASSUME_NONNULL_BEGIN [threadTitleLabel autoPinEdgeToSuperviewEdge:ALEdgeLeft]; [threadTitleLabel autoPinEdgeToSuperviewEdge:ALEdgeRight]; - if (![self isGroupThread] && ![self.thread.name isEqualToString:self.thread.contactIdentifier]) { - NSString *subtitle = - [PhoneNumber bestEffortFormatPartialUserSpecifiedTextToLookLikeAPhoneNumber:self.thread.contactIdentifier]; + __block UIView *lastTitleView = threadTitleLabel; - UILabel *threadSubtitleLabel = [UILabel new]; - threadSubtitleLabel.text = subtitle; - threadSubtitleLabel.textColor = [UIColor blackColor]; - // TODO: - threadSubtitleLabel.font = [UIFont ows_regularFontWithSize:12.f]; - threadSubtitleLabel.lineBreakMode = NSLineBreakByTruncatingTail; - [threadNameView addSubview:threadSubtitleLabel]; - [threadSubtitleLabel autoPinEdgeToSuperviewEdge:ALEdgeBottom]; - [threadSubtitleLabel autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:threadTitleLabel]; - [threadSubtitleLabel autoPinEdgeToSuperviewEdge:ALEdgeLeft]; - } else { - [threadTitleLabel autoPinEdgeToSuperviewEdge:ALEdgeBottom]; + if (![self isGroupThread]) { + const CGFloat kSubtitlePointSize = 12.f; + void (^addSubtitle)(NSAttributedString *) = ^(NSAttributedString *subtitle) { + UILabel *subtitleLabel = [UILabel new]; + subtitleLabel.textColor = [UIColor ows_darkGrayColor]; + subtitleLabel.font = [UIFont ows_regularFontWithSize:kSubtitlePointSize]; + subtitleLabel.attributedText = subtitle; + subtitleLabel.lineBreakMode = NSLineBreakByTruncatingTail; + [threadNameView addSubview:subtitleLabel]; + [subtitleLabel autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:lastTitleView]; + [subtitleLabel autoPinEdgeToSuperviewEdge:ALEdgeLeft]; + lastTitleView = subtitleLabel; + }; + + NSString *recipientId = self.thread.contactIdentifier; + + BOOL hasName = ![self.thread.name isEqualToString:recipientId]; + if (hasName) { + NSAttributedString *subtitle = [[NSAttributedString alloc] + initWithString:[PhoneNumber + bestEffortFormatPartialUserSpecifiedTextToLookLikeAPhoneNumber:recipientId]]; + addSubtitle(subtitle); + } + + BOOL isVerified = [[OWSIdentityManager sharedManager] verificationStateForRecipientId:recipientId] + == OWSVerificationStateVerified; + if (isVerified) { + NSMutableAttributedString *subtitle = [NSMutableAttributedString new]; + // "checkmark" + [subtitle appendAttributedString:[[NSAttributedString alloc] + initWithString:@"\uf00c " + attributes:@{ + NSFontAttributeName : + [UIFont ows_fontAwesomeFont:kSubtitlePointSize], + }]]; + [subtitle appendAttributedString:[[NSAttributedString alloc] + initWithString:NSLocalizedString(@"PRIVACY_IDENTITY_IS_VERIFIED_BADGE", + @"Badge indicating that the user is verified.")]]; + addSubtitle(subtitle); + } } + [lastTitleView autoPinEdgeToSuperviewEdge:ALEdgeBottom]; + [firstSectionHeader addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(conversationNameTouched:)]]; @@ -631,6 +674,21 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - Actions +- (void)showVerificationView +{ + NSString *recipientId = self.thread.contactIdentifier; + OWSAssert(recipientId.length > 0); + + [FingerprintViewController showVerificationViewFromViewController:self recipientId:recipientId]; +} + +- (void)showGroupMembersView +{ + ShowGroupMembersViewController *showGroupMembersViewController = [ShowGroupMembersViewController new]; + [showGroupMembersViewController configWithThread:(TSGroupThread *)self.thread]; + [self.navigationController pushViewController:showGroupMembersViewController animated:YES]; +} + - (void)showUpdateGroupView:(UpdateGroupMode)mode { OWSAssert(self.conversationSettingsViewDelegate); @@ -927,6 +985,15 @@ NS_ASSUME_NONNULL_BEGIN [self.tableView reloadData]; } +#pragma mark - Notifications + +- (void)identityStateDidChange:(NSNotification *)notification +{ + OWSAssert([NSThread isMainThread]); + + [self updateTableContents]; +} + #pragma mark - Logging + (NSString *)tag diff --git a/Signal/src/ViewControllers/SafetyNumberConfirmationAlert.swift b/Signal/src/ViewControllers/SafetyNumberConfirmationAlert.swift index 73f19184a..8e47fc2c9 100644 --- a/Signal/src/ViewControllers/SafetyNumberConfirmationAlert.swift +++ b/Signal/src/ViewControllers/SafetyNumberConfirmationAlert.swift @@ -63,7 +63,7 @@ class SafetyNumberConfirmationAlert: NSObject { } actionSheetController.addAction(confirmAction) - let showSafetyNumberAction = UIAlertAction(title: NSLocalizedString("VERIFY_PRIVACY", comment: "Action sheet item"), style: .default) { _ in + let showSafetyNumberAction = UIAlertAction(title: NSLocalizedString("VERIFY_PRIVACY", comment: "Label for button or row which allows users to verify the safety number of another user."), style: .default) { _ in Logger.info("\(self.TAG) Opted to show Safety Number for identity: \(untrustedIdentity)") self.presentSafetyNumberViewController(theirIdentityKey: untrustedIdentity.identityKey, @@ -82,10 +82,11 @@ class SafetyNumberConfirmationAlert: NSObject { } public func presentSafetyNumberViewController(theirIdentityKey: Data, theirRecipientId: String, theirDisplayName: String, completion: (() -> Void)? = nil) { - let fingerprintViewController = FingerprintViewController() - fingerprintViewController.configure(recipientId: theirRecipientId) - let navigationController = UINavigationController(rootViewController:fingerprintViewController) - UIApplication.shared.frontmostViewController?.present(navigationController, animated: true, completion: completion) + guard let fromViewController = UIApplication.shared.frontmostViewController else { + Logger.info("\(self.TAG) Missing frontmostViewController") + return + } + FingerprintViewController.showVerificationView(from:fromViewController, recipientId:theirRecipientId) } private func untrustedIdentityForSending(recipientIds: [String]) -> OWSRecipientIdentity? { diff --git a/Signal/src/ViewControllers/ShowGroupMembersViewController.m b/Signal/src/ViewControllers/ShowGroupMembersViewController.m index bf1f89134..54751c95c 100644 --- a/Signal/src/ViewControllers/ShowGroupMembersViewController.m +++ b/Signal/src/ViewControllers/ShowGroupMembersViewController.m @@ -63,6 +63,21 @@ NS_ASSUME_NONNULL_BEGIN - (void)commonInit { _contactsViewHelper = [[ContactsViewHelper alloc] initWithDelegate:self]; + + [self observeNotifications]; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)observeNotifications +{ + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(identityStateDidChange:) + name:kNSNotificationName_IdentityStateDidChange + object:nil]; } - (void)configWithThread:(TSGroupThread *)thread @@ -134,6 +149,12 @@ NS_ASSUME_NONNULL_BEGIN [cell configureWithRecipientId:recipientId contactsManager:helper.contactsManager]; } + BOOL isVerified = [[OWSIdentityManager sharedManager] verificationStateForRecipientId:recipientId] + == OWSVerificationStateVerified; + if (isVerified) { + [cell addVerifiedSubtitle]; + } + return cell; } customRowHeight:[ContactTableViewCell rowHeight] @@ -253,6 +274,14 @@ NS_ASSUME_NONNULL_BEGIN handler:^(UIAlertAction *_Nonnull action) { [self callMember:recipientId]; }]]; + [actionSheetController + addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"VERIFY_PRIVACY", + @"Label for button or row which allows users to verify the " + @"safety number of another user.") + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *_Nonnull action) { + [self verifySafetyNumber:recipientId]; + }]]; } UIAlertAction *dismissAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"TXT_CANCEL_TITLE", @"") @@ -284,6 +313,13 @@ NS_ASSUME_NONNULL_BEGIN [Environment callUserWithIdentifier:recipientId]; } +- (void)verifySafetyNumber:(NSString *)recipientId +{ + OWSAssert(recipientId.length > 0); + + [FingerprintViewController showVerificationViewFromViewController:self recipientId:recipientId]; +} + #pragma mark - ContactsViewHelperDelegate - (void)contactsViewHelperDidUpdateContacts @@ -313,6 +349,15 @@ NS_ASSUME_NONNULL_BEGIN [self dismissViewControllerAnimated:YES completion:nil]; } +#pragma mark - Notifications + +- (void)identityStateDidChange:(NSNotification *)notification +{ + OWSAssert([NSThread isMainThread]); + + [self updateTableContents]; +} + #pragma mark - Logging + (NSString *)tag diff --git a/Signal/src/ViewControllers/SignalsViewController.m b/Signal/src/ViewControllers/SignalsViewController.m index ac4a23fc6..b2a085c2d 100644 --- a/Signal/src/ViewControllers/SignalsViewController.m +++ b/Signal/src/ViewControllers/SignalsViewController.m @@ -132,18 +132,18 @@ NSString *const SignalsViewControllerSegueShowIncomingCall = @"ShowIncomingCallS - (void)blockedPhoneNumbersDidChange:(id)notification { - dispatch_async(dispatch_get_main_queue(), ^{ - _blockedPhoneNumberSet = [NSSet setWithArray:[_blockingManager blockedPhoneNumbers]]; - - [self.tableView reloadData]; - }); + OWSAssert([NSThread isMainThread]); + + _blockedPhoneNumberSet = [NSSet setWithArray:[_blockingManager blockedPhoneNumbers]]; + + [self.tableView reloadData]; } - (void)signalAccountsDidChange:(id)notification { - dispatch_async(dispatch_get_main_queue(), ^{ - [self.tableView reloadData]; - }); + OWSAssert([NSThread isMainThread]); + + [self.tableView reloadData]; } #pragma mark - View Life Cycle diff --git a/Signal/src/util/OWSContactsSyncing.m b/Signal/src/util/OWSContactsSyncing.m index 175bdf9e2..62a9bc373 100644 --- a/Signal/src/util/OWSContactsSyncing.m +++ b/Signal/src/util/OWSContactsSyncing.m @@ -61,9 +61,9 @@ NSString *const kTSStorageManagerOWSContactsSyncingLastMessageKey = - (void)signalAccountsDidChange:(id)notification { - dispatch_async(dispatch_get_main_queue(), ^{ - [self sendSyncContactsMessageIfPossible]; - }); + OWSAssert([NSThread isMainThread]); + + [self sendSyncContactsMessageIfPossible]; } #pragma mark - Methods diff --git a/Signal/src/views/ContactTableViewCell.h b/Signal/src/views/ContactTableViewCell.h index 8cf323220..10e273b5d 100644 --- a/Signal/src/views/ContactTableViewCell.h +++ b/Signal/src/views/ContactTableViewCell.h @@ -33,6 +33,8 @@ extern NSString *const kContactsTable_CellReuseIdentifier; - (void)configureWithThread:(TSThread *)thread contactsManager:(OWSContactsManager *)contactsManager; +- (void)addVerifiedSubtitle; + @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/views/ContactTableViewCell.m b/Signal/src/views/ContactTableViewCell.m index 8fba252d7..8dcb421bc 100644 --- a/Signal/src/views/ContactTableViewCell.m +++ b/Signal/src/views/ContactTableViewCell.m @@ -23,6 +23,7 @@ const NSUInteger kContactTableViewCellAvatarSize = 40; @property (nonatomic) IBOutlet UILabel *nameLabel; @property (nonatomic) IBOutlet UIImageView *avatarView; +@property (nonatomic, nullable) UILabel *subtitle; @end @@ -144,11 +145,67 @@ const NSUInteger kContactTableViewCellAvatarSize = 40; [self layoutSubviews]; } +- (void)addVerifiedSubtitle +{ + [self.subtitle removeFromSuperview]; + + const CGFloat kSubtitlePointSize = 10.f; + NSMutableAttributedString *text = [NSMutableAttributedString new]; + // "checkmark" + [text appendAttributedString:[[NSAttributedString alloc] + initWithString:@"\uf00c " + attributes:@{ + NSFontAttributeName : [UIFont ows_fontAwesomeFont:kSubtitlePointSize], + }]]; + [text appendAttributedString:[[NSAttributedString alloc] + initWithString:NSLocalizedString(@"PRIVACY_IDENTITY_IS_VERIFIED_BADGE", + @"Badge indicating that the user is verified.")]]; + self.subtitle = [UILabel new]; + self.subtitle.font = [UIFont ows_regularFontWithSize:kSubtitlePointSize]; + self.subtitle.textColor = [UIColor ows_darkGrayColor]; + self.subtitle.attributedText = text; + [self.subtitle sizeToFit]; + [self.contentView addSubview:self.subtitle]; + + [self setNeedsLayout]; +} + +- (void)setFrame:(CGRect)frame +{ + [super setFrame:frame]; + + [self layoutSubviews]; +} + +- (void)setBounds:(CGRect)bounds +{ + [super setBounds:bounds]; + + [self layoutSubviews]; +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + + if (self.subtitle) { + OWSAssert(self.nameLabel.superview == self.contentView); + const CGFloat kSubtitleVMargin + = ((self.contentView.height - self.nameLabel.font.lineHeight) * 0.5f - self.subtitle.height) * 0.5f; + self.subtitle.frame = CGRectMake(self.nameLabel.left, + round((self.contentView.height - self.subtitle.height) - kSubtitleVMargin), + self.subtitle.width, + self.subtitle.height); + } +} + - (void)prepareForReuse { self.accessoryMessage = nil; self.accessoryView = nil; self.accessoryType = UITableViewCellAccessoryNone; + [self.subtitle removeFromSuperview]; + self.subtitle = nil; } @end diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 438e5a6f4..1261d3cb0 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -226,6 +226,12 @@ /* The generic name used for calls if CallKit privacy is enabled */ "CALLKIT_ANONYMOUS_CONTACT_NAME" = "Signal User"; +/* Message for alert explaining that a user cannot be verified. */ +"CANT_VERIFY_IDENTITY_ALERT_MESSAGE" = "This user can't be verified until you've exchanged messages with them."; + +/* Title for alert explaining that a user cannot be verified. */ +"CANT_VERIFY_IDENTITY_ALERT_TITLE" = "Error"; + /* Title for the 'censorship circumvention country' view. */ "CENSORSHIP_CIRCUMVENTION_COUNTRY_VIEW_TITLE" = "Select Country"; @@ -715,9 +721,15 @@ /* message footer while attachment is uploading */ "MESSAGE_STATUS_UPLOADING" = "Uploading…"; +/* Indicates that one member of this group conversation is no longer verified. Embeds {{user's name or phone number}}. */ +"MESSAGES_VIEW_1_MEMBER_NO_LONGER_VERIFIED_FORMAT" = "%@ is no longer verified."; + /* Indicates that this 1:1 conversation has been blocked. */ "MESSAGES_VIEW_CONTACT_BLOCKED" = "You Blocked this User"; +/* Indicates that this 1:1 conversation is no longer verified. Embeds {{user's name or phone number}}. */ +"MESSAGES_VIEW_CONTACT_NO_LONGER_VERIFIED_FORMAT" = "%@ is no longer verified."; + /* Action sheet title after tapping on failed download. */ "MESSAGES_VIEW_FAILED_DOWNLOAD_ACTIONSHEET_TITLE" = "Download Failed."; @@ -730,6 +742,9 @@ /* Indicates that some members of this group has been blocked. Embeds {{the number of blocked users in this group}}. */ "MESSAGES_VIEW_GROUP_N_MEMBERS_BLOCKED_FORMAT" = "You Blocked %d Members of this Group"; +/* Indicates that more than one member of this group conversation is no longer verified. */ +"MESSAGES_VIEW_N_MEMBERS_NO_LONGER_VERIFIED" = "More than one member of this group is no longer verified."; + /* The subtitle for the messages view title indicates that the title can be tapped to access settings for this conversation. */ "MESSAGES_VIEW_TITLE_SUBTITLE" = "Tap here for settings"; @@ -943,6 +958,9 @@ /* Label indicating that the user is not verified. Embeds {{the user's name or phone number}}. */ "PRIVACY_IDENTITY_IS_NOT_VERIFIED_FORMAT" = "%@ is not verified."; +/* Badge indicating that the user is verified. */ +"PRIVACY_IDENTITY_IS_VERIFIED_BADGE" = "Verified"; + /* Label indicating that the user is verified. Embeds {{the user's name or phone number}}. */ "PRIVACY_IDENTITY_IS_VERIFIED_FORMAT" = "%@ is verified."; @@ -1426,8 +1444,7 @@ /* Generic message indicating that verification state changed for a given user. */ "VERIFICATION_STATE_CHANGE_GENERIC" = "Verification state changed."; -/* Action sheet item - table cell label in conversation settings */ +/* Label for button or row which allows users to verify the safety number of another user. */ "VERIFY_PRIVACY" = "Show Safety Number"; /* Indicates how to cancel a voice message. */