From e7126f8c60cd5d2cee77a8ec93db13f81fe09384 Mon Sep 17 00:00:00 2001 From: Russ Shanahan Date: Thu, 1 Dec 2016 16:17:57 -0500 Subject: [PATCH] Less confusing "#" avatar for unknown Contact instead of "+" For consistency with the Android and Desktop client behavior. * Show a placeholder avatar when no image, initials (#1512) If all we know about the user is their phone number, their avatar image is rendered as a placeholder. Previously, it would render the first few characters of their phone number as if they were initials (eg. "+") * Rename, extend OWSContactsManager methods (#1512) Rename from: nameStringForPhoneIdentifier to: displayNameForPhoneIdentifier Also, add: - (BOOL)nameExistsForPhoneIdentifier:(NSString *)identifier; Which reports whether there's any "name" for a contact. * Remove unused typedefs These aren't used in the project anymore, and they were causing compiling warnings due to a lack of nullability indication. * Resolve some OWSContactsManager nullability warnings Did a pass through all of the existing nullability warnings in OWSContactsManager. Tried to pick descriptors that best reflected the behavior of the methods. // FREEBIE --- Podfile.lock | 2 +- Signal/src/Models/OWSContactAvatarBuilder.m | 18 ++++--- .../TSMessageAdapaters/TSMessageAdapter.m | 4 +- Signal/src/contact/OWSContactsManager.h | 22 ++++---- Signal/src/contact/OWSContactsManager.m | 51 +++++++++++++------ Signal/src/environment/NotificationsManager.m | 2 +- Signal/src/network/PushManager.m | 2 +- .../view controllers/MessagesViewController.m | 6 +-- 8 files changed, 64 insertions(+), 43 deletions(-) diff --git a/Podfile.lock b/Podfile.lock index ba7c3c4c6..4dd345054 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -132,7 +132,7 @@ EXTERNAL SOURCES: CHECKOUT OPTIONS: SignalServiceKit: - :commit: 34ffce89f59356ab23f290866b1c3437f03312ce + :commit: 71250281596cdd6a03072d1b4a23aea8ce490eeb :git: https://github.com/WhisperSystems/SignalServiceKit.git SocketRocket: :commit: 41b57bb2fc292a814f758441a05243eb38457027 diff --git a/Signal/src/Models/OWSContactAvatarBuilder.m b/Signal/src/Models/OWSContactAvatarBuilder.m index 1579cfa51..bc9d75ac1 100644 --- a/Signal/src/Models/OWSContactAvatarBuilder.m +++ b/Signal/src/Models/OWSContactAvatarBuilder.m @@ -57,8 +57,9 @@ NS_ASSUME_NONNULL_BEGIN } NSMutableString *initials = [NSMutableString string]; - - if (self.contactName.length > 0) { + BOOL contactHasName = [self.contactsManager nameExistsForPhoneIdentifier:self.signalId]; + if (contactHasName) { + // Make an image from the contact's initials NSArray *words = [self.contactName componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; for (NSString *word in words) { @@ -67,19 +68,20 @@ NS_ASSUME_NONNULL_BEGIN [initials appendString:[firstLetter uppercaseString]]; } } + + NSRange stringRange = { 0, MIN([initials length], (NSUInteger)3) }; // Rendering max 3 letters. + initials = [[initials substringWithRange:stringRange] mutableCopy]; + } else { + // We don't have a name for this contact, so we can't make an "initials" image + [initials appendString:@"#"]; } - - NSRange stringRange = { 0, MIN([initials length], (NSUInteger)3) }; // Rendering max 3 letters. - initials = [[initials substringWithRange:stringRange] mutableCopy]; - + UIColor *backgroundColor = [UIColor backgroundColorForContact:self.signalId]; - UIImage *image = [[JSQMessagesAvatarImageFactory avatarImageWithUserInitials:initials backgroundColor:backgroundColor textColor:[UIColor whiteColor] font:[UIFont ows_boldFontWithSize:36.0] diameter:100] avatarImage]; - [self.contactsManager.avatarCache setObject:image forKey:self.signalId]; return image; } diff --git a/Signal/src/Models/TSMessageAdapaters/TSMessageAdapter.m b/Signal/src/Models/TSMessageAdapaters/TSMessageAdapter.m index 0f1af5a14..5f299244f 100644 --- a/Signal/src/Models/TSMessageAdapaters/TSMessageAdapter.m +++ b/Signal/src/Models/TSMessageAdapaters/TSMessageAdapter.m @@ -100,7 +100,7 @@ if ([interaction isKindOfClass:[TSIncomingMessage class]]) { NSString *contactId = ((TSContactThread *)thread).contactIdentifier; adapter.senderId = contactId; - adapter.senderDisplayName = [contactsManager nameStringForPhoneIdentifier:contactId]; + adapter.senderDisplayName = [contactsManager displayNameForPhoneIdentifier:contactId]; adapter.messageType = TSIncomingMessageAdapter; } else { adapter.senderId = ME_MESSAGE_IDENTIFIER; @@ -111,7 +111,7 @@ if ([interaction isKindOfClass:[TSIncomingMessage class]]) { TSIncomingMessage *message = (TSIncomingMessage *)interaction; adapter.senderId = message.authorId; - adapter.senderDisplayName = [contactsManager nameStringForPhoneIdentifier:message.authorId]; + adapter.senderDisplayName = [contactsManager displayNameForPhoneIdentifier:message.authorId]; adapter.messageType = TSIncomingMessageAdapter; } else { adapter.senderId = ME_MESSAGE_IDENTIFIER; diff --git a/Signal/src/contact/OWSContactsManager.h b/Signal/src/contact/OWSContactsManager.h index c3c076535..0d5f8d2dc 100644 --- a/Signal/src/contact/OWSContactsManager.h +++ b/Signal/src/contact/OWSContactsManager.h @@ -12,28 +12,26 @@ #define SIGNAL_LIST_UPDATED @"Signal_AB_UPDATED" -typedef void (^ABAccessRequestCompletionBlock)(BOOL hasAccess); -typedef void (^ABReloadRequestCompletionBlock)(NSArray *contacts); - @interface OWSContactsManager : NSObject -@property CNContactStore *contactStore; -@property NSCache *avatarCache; +@property CNContactStore * _Nullable contactStore; +@property NSCache * _Nonnull avatarCache; -- (ObservableValue *)getObservableContacts; +- (ObservableValue * _Nonnull)getObservableContacts; -- (NSArray *)getContactsFromAddressBook:(ABAddressBookRef)addressBook; -- (Contact *)latestContactForPhoneNumber:(PhoneNumber *)phoneNumber; +- (NSArray * _Nonnull)getContactsFromAddressBook:(ABAddressBookRef _Nonnull)addressBook; +- (Contact * _Nullable)latestContactForPhoneNumber:(PhoneNumber * _Nullable)phoneNumber; - (void)verifyABPermission; -- (NSArray *)allContacts; -- (NSArray *)signalContacts; +- (NSArray * _Nonnull)allContacts; +- (NSArray * _Nonnull)signalContacts; - (void)doAfterEnvironmentInitSetup; -- (NSString *)nameStringForPhoneIdentifier:(NSString *)identifier; -- (UIImage *)imageForPhoneIdentifier:(NSString *)identifier; +- (NSString * _Nonnull)displayNameForPhoneIdentifier:(NSString * _Nullable)identifier; +- (BOOL)nameExistsForPhoneIdentifier:(NSString * _Nullable)identifier; +- (UIImage * _Nullable)imageForPhoneIdentifier:(NSString * _Nullable)identifier; + (NSComparator)contactComparator; diff --git a/Signal/src/contact/OWSContactsManager.m b/Signal/src/contact/OWSContactsManager.m index 37ce0ade7..9e73971c4 100644 --- a/Signal/src/contact/OWSContactsManager.m +++ b/Signal/src/contact/OWSContactsManager.m @@ -224,7 +224,7 @@ void onAddressBookChanged(ABAddressBookRef notifyAddressBook, CFDictionaryRef in return futureAddressBookSource.future; } -- (NSArray *)getContactsFromAddressBook:(ABAddressBookRef)addressBook { +- (NSArray *)getContactsFromAddressBook:(ABAddressBookRef _Nonnull)addressBook { CFArrayRef allPeople = ABAddressBookCopyArrayOfAllPeople(addressBook); CFMutableArrayRef allPeopleMutable = CFArrayCreateMutableCopy(kCFAllocatorDefault, CFArrayGetCount(allPeople), allPeople); @@ -290,7 +290,7 @@ void onAddressBookChanged(ABAddressBookRef notifyAddressBook, CFDictionaryRef in andContactID:recordID]; } -- (Contact *)latestContactForPhoneNumber:(PhoneNumber *)phoneNumber { +- (Contact * _Nullable)latestContactForPhoneNumber:(PhoneNumber *)phoneNumber { NSArray *allContacts = [self allContacts]; ContactSearchBlock searchBlock = ^BOOL(Contact *contact, NSUInteger idx, BOOL *stop) { @@ -361,7 +361,7 @@ void onAddressBookChanged(ABAddressBookRef notifyAddressBook, CFDictionaryRef in } -+ (BOOL)name:(NSString *)nameString matchesQuery:(NSString *)queryString { ++ (BOOL)name:(NSString * _Nonnull)nameString matchesQuery:(NSString * _Nonnull)queryString { NSCharacterSet *whitespaceSet = NSCharacterSet.whitespaceCharacterSet; NSArray *queryStrings = [queryString componentsSeparatedByCharactersInSet:whitespaceSet]; NSArray *nameStrings = [nameString componentsSeparatedByCharactersInSet:whitespaceSet]; @@ -395,36 +395,57 @@ void onAddressBookChanged(ABAddressBookRef notifyAddressBook, CFDictionaryRef in return [Contact comparatorSortingNamesByFirstThenLast:firstNameOrdering]; } -- (NSArray *)signalContacts { +- (NSArray * _Nonnull)signalContacts { return [self getSignalUsersFromContactsArray:[self allContacts]]; } -- (NSString *)nameStringForPhoneIdentifier:(NSString *)identifier { +- (NSString * _Nonnull)displayNameForPhoneIdentifier:(NSString * _Nullable)identifier { if (!identifier) { return NSLocalizedString(@"UNKNOWN_CONTACT_NAME", @"Displayed if for some reason we can't determine a contacts phone number *or* name"); } - for (Contact *contact in self.allContacts) { - for (PhoneNumber *phoneNumber in contact.parsedPhoneNumbers) { - if ([phoneNumber.toE164 isEqualToString:identifier]) { - return contact.fullName; - } - } - } - return identifier; + Contact *contact = [self contactForPhoneIdentifier:identifier]; + + NSString *displayName = (contact.fullName.length > 0) ? contact.fullName : identifier; + + return displayName; } -- (UIImage *)imageForPhoneIdentifier:(NSString *)identifier { +- (BOOL)nameExistsForPhoneIdentifier:(NSString * _Nullable)identifier { + Contact *contact = [self contactForPhoneIdentifier:identifier]; + NSString *name = contact.fullName; + + if (name.length <= 0) return NO; + + // OWSContactsManager::contactForRecord will use the first phone number as a name + // in absense of a name or business name during import. Make sure that's not happening here. + if ((contact.userTextPhoneNumbers.count > 0) && ([contact.userTextPhoneNumbers[0] isEqualToString:name])) { + return NO; + } + + return YES; +} + +- (Contact * _Nullable)contactForPhoneIdentifier:(NSString * _Nullable)identifier { + if (!identifier) { + return nil; + } for (Contact *contact in self.allContacts) { for (PhoneNumber *phoneNumber in contact.parsedPhoneNumbers) { if ([phoneNumber.toE164 isEqualToString:identifier]) { - return contact.image; + return contact; } } } return nil; } +- (UIImage * _Nullable)imageForPhoneIdentifier:(NSString * _Nullable)identifier { + Contact *contact = [self contactForPhoneIdentifier:identifier]; + + return contact.image; +} + #pragma mark - Logging + (NSString *)tag diff --git a/Signal/src/environment/NotificationsManager.m b/Signal/src/environment/NotificationsManager.m index 2052c4164..de01888bd 100644 --- a/Signal/src/environment/NotificationsManager.m +++ b/Signal/src/environment/NotificationsManager.m @@ -113,7 +113,7 @@ @{Signal_Thread_UserInfo_Key : thread.uniqueId, Signal_Message_UserInfo_Key : message.uniqueId}; if ([thread isGroupThread]) { - NSString *sender = [self.contactsManager nameStringForPhoneIdentifier:message.authorId]; + NSString *sender = [self.contactsManager displayNameForPhoneIdentifier:message.authorId]; NSString *threadName = [NSString stringWithFormat:@"\"%@\"", name]; notification.alertBody = [NSString stringWithFormat:NSLocalizedString(@"APN_MESSAGE_IN_GROUP_DETAILED", nil), diff --git a/Signal/src/network/PushManager.m b/Signal/src/network/PushManager.m index ce7a3dc75..f21b1d589 100644 --- a/Signal/src/network/PushManager.m +++ b/Signal/src/network/PushManager.m @@ -111,7 +111,7 @@ UILocalNotification *notification = [[UILocalNotification alloc] init]; NSString *callerId = call.initiatorNumber.toE164; - NSString *displayName = [self.contactsManager nameStringForPhoneIdentifier:callerId]; + NSString *displayName = [self.contactsManager displayNameForPhoneIdentifier:callerId]; PropertyListPreferences *prefs = [Environment preferences]; notification.alertBody = @"☎️ "; diff --git a/Signal/src/view controllers/MessagesViewController.m b/Signal/src/view controllers/MessagesViewController.m index 78f48415b..5c22f424b 100644 --- a/Signal/src/view controllers/MessagesViewController.m +++ b/Signal/src/view controllers/MessagesViewController.m @@ -1079,7 +1079,7 @@ typedef enum : NSUInteger { } } else if (message.messageType == TSIncomingMessageAdapter && [self.thread isKindOfClass:[TSGroupThread class]]) { TSIncomingMessage *incomingMessage = (TSIncomingMessage *)message.interaction; - NSString *_Nonnull name = [self.contactsManager nameStringForPhoneIdentifier:incomingMessage.authorId]; + NSString *_Nonnull name = [self.contactsManager displayNameForPhoneIdentifier:incomingMessage.authorId]; NSAttributedString *senderNameString = [[NSAttributedString alloc] initWithString:name]; return senderNameString; @@ -1539,7 +1539,7 @@ typedef enum : NSUInteger { - (void)tappedInvalidIdentityKeyErrorMessage:(TSInvalidIdentityKeyErrorMessage *)errorMessage { - NSString *keyOwner = [self.contactsManager nameStringForPhoneIdentifier:errorMessage.theirSignalId]; + NSString *keyOwner = [self.contactsManager displayNameForPhoneIdentifier:errorMessage.theirSignalId]; NSString *titleFormat = NSLocalizedString(@"SAFETY_NUMBERS_ACTIONSHEET_TITLE", @"Action sheet heading"); NSString *titleText = [NSString stringWithFormat:titleFormat, keyOwner]; @@ -1601,7 +1601,7 @@ typedef enum : NSUInteger { } OWSFingerprint *fingerprint = (OWSFingerprint *)sender; - NSString *contactName = [self.contactsManager nameStringForPhoneIdentifier:fingerprint.theirStableId]; + NSString *contactName = [self.contactsManager displayNameForPhoneIdentifier:fingerprint.theirStableId]; [vc configureWithThread:self.thread fingerprint:fingerprint contactName:contactName]; } else if ([segue.destinationViewController isKindOfClass:[OWSConversationSettingsTableViewController class]]) { OWSConversationSettingsTableViewController *controller