Merge branch 'mkirk/profile-name-for-avatar'

This commit is contained in:
Michael Kirk 2017-08-25 12:42:12 -04:00
commit 3cccf92757
12 changed files with 234 additions and 18 deletions

View File

@ -148,6 +148,9 @@
454EBAB41F2BE14C00ACE0BB /* OWSAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D99C911F2937CC00D284D6 /* OWSAnalytics.swift */; };
455A16DD1F1FEA0000F86704 /* Metal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 455A16DB1F1FEA0000F86704 /* Metal.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
455A16DE1F1FEA0000F86704 /* MetalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 455A16DC1F1FEA0000F86704 /* MetalKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
455AC69B1F4F79E500134004 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 455AC69A1F4F79E500134004 /* ImageCache.swift */; };
455AC69C1F4F79E500134004 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 455AC69A1F4F79E500134004 /* ImageCache.swift */; };
455AC69E1F4F8B0300134004 /* ImageCacheTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 455AC69D1F4F8B0300134004 /* ImageCacheTest.swift */; };
45638BDC1F3DD0D400128435 /* DebugUICalling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45638BDB1F3DD0D400128435 /* DebugUICalling.swift */; };
45638BDF1F3DDB2200128435 /* MessageSender+Promise.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45638BDE1F3DDB2200128435 /* MessageSender+Promise.swift */; };
4563ADF11F22BD7100DEB8C7 /* OWS106EnsureProfileComplete.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4563ADF01F22BD7100DEB8C7 /* OWS106EnsureProfileComplete.swift */; };
@ -602,6 +605,8 @@
454B35071D08EED80026D658 /* mk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = mk; path = translations/mk.lproj/Localizable.strings; sourceTree = "<group>"; };
455A16DB1F1FEA0000F86704 /* Metal.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Metal.framework; path = System/Library/Frameworks/Metal.framework; sourceTree = SDKROOT; };
455A16DC1F1FEA0000F86704 /* MetalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MetalKit.framework; path = System/Library/Frameworks/MetalKit.framework; sourceTree = SDKROOT; };
455AC69A1F4F79E500134004 /* ImageCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = "<group>"; };
455AC69D1F4F8B0300134004 /* ImageCacheTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageCacheTest.swift; sourceTree = "<group>"; };
45638BDB1F3DD0D400128435 /* DebugUICalling.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugUICalling.swift; sourceTree = "<group>"; };
45638BDE1F3DDB2200128435 /* MessageSender+Promise.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MessageSender+Promise.swift"; sourceTree = "<group>"; };
4563ADF01F22BD7100DEB8C7 /* OWS106EnsureProfileComplete.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWS106EnsureProfileComplete.swift; sourceTree = "<group>"; };
@ -1439,6 +1444,7 @@
76EB04FB18170B33006006FC /* Util.h */,
45F170D51E315310003FC1F2 /* Weak.swift */,
45F170CB1E310E22003FC1F2 /* WeakTimer.swift */,
455AC69A1F4F79E500134004 /* ImageCache.swift */,
);
path = util;
sourceTree = "<group>";
@ -1639,6 +1645,7 @@
B660F6B31C29868000687D6E /* UtilTest.h */,
B660F6B41C29868000687D6E /* UtilTest.m */,
45666F571D9B2880008FE134 /* OWSScrubbingLogFormatterTest.m */,
455AC69D1F4F8B0300134004 /* ImageCacheTest.swift */,
);
path = util;
sourceTree = "<group>";
@ -2194,6 +2201,7 @@
34E3EF0D1EFC235B007F6822 /* DebugUIDiskUsage.m in Sources */,
344F2F671E57A932000D9322 /* UIViewController+OWS.m in Sources */,
B6DA6B071B8A2F9A00CA6F98 /* AppStoreRating.m in Sources */,
455AC69B1F4F79E500134004 /* ImageCache.swift in Sources */,
451A13B11E13DED2000A50FD /* CallNotificationsAdapter.swift in Sources */,
3456710A1E8A9F5D006EE662 /* TSGenericAttachmentAdapter.m in Sources */,
450DF2091E0DD2C6003D14BE /* UserNotificationsAdaptee.swift in Sources */,
@ -2434,6 +2442,7 @@
B660F7771C29988E00687D6E /* UIImage+normalizeImage.m in Sources */,
954AEE6A1DF33E01002E5410 /* ContactsPickerTest.swift in Sources */,
B660F77B1C29988E00687D6E /* Queue.m in Sources */,
455AC69C1F4F79E500134004 /* ImageCache.swift in Sources */,
45666F581D9B2880008FE134 /* OWSScrubbingLogFormatterTest.m in Sources */,
B660F77F1C29988E00687D6E /* DateUtil.m in Sources */,
B660F7811C29988E00687D6E /* FunctionalUtil.m in Sources */,
@ -2462,6 +2471,7 @@
B660F6D21C29868000687D6E /* PushManagerTest.m in Sources */,
45C0DC1F1E69011F00E04C47 /* UIStoryboard+OWS.swift in Sources */,
4505C2C01E648EA300CEBF41 /* ExperienceUpgrade.swift in Sources */,
455AC69E1F4F8B0300134004 /* ImageCacheTest.swift in Sources */,
450873C81D9D867B006B54F2 /* OWSIncomingMessageCollectionViewCell.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;

View File

@ -243,6 +243,7 @@ static NSString *const kURLHostVerifyPrefix = @"verify";
[[TSStorageManager sharedManager] setupDatabaseWithSafeBlockingMigrations:^{
[VersionMigrations runSafeBlockingMigrations];
}];
[[Environment getCurrent].contactsManager startObserving];
self.incomingMessageReadObserver =
[[OWSIncomingMessageReadObserver alloc] initWithMessageSender:[Environment getCurrent].messageSender];

View File

@ -4,6 +4,7 @@
#import "OWSContactAvatarBuilder.h"
#import "OWSContactsManager.h"
#import "Signal-Swift.h"
#import "TSContactThread.h"
#import "TSGroupThread.h"
#import "TSThread.h"
@ -48,7 +49,14 @@ NS_ASSUME_NONNULL_BEGIN
diameter:(NSUInteger)diameter
contactsManager:(OWSContactsManager *)contactsManager
{
NSString *name = [contactsManager displayNameForPhoneIdentifier:signalId];
// Name for avatar initials.
NSString *_Nullable name = [contactsManager nameFromSystemContactsForRecipientId:signalId];
if (name.length == 0) {
name = [contactsManager profileNameForRecipientId:signalId];
}
if (name.length == 0) {
name = signalId;
}
return [self initWithContactId:signalId name:name diameter:diameter contactsManager:contactsManager];
}
@ -69,8 +77,8 @@ NS_ASSUME_NONNULL_BEGIN
- (UIImage *)buildDefaultImage
{
NSString *cacheKey = [NSString stringWithFormat:@"signalId:%@:diamater:%lu", self.signalId, (unsigned long)self.diameter];
UIImage *cachedAvatar = [self.contactsManager.avatarCache objectForKey:cacheKey];
UIImage *cachedAvatar =
[self.contactsManager.avatarCache imageForKey:self.signalId diameter:(CGFloat)self.diameter];
if (cachedAvatar) {
return cachedAvatar;
}
@ -108,7 +116,8 @@ NS_ASSUME_NONNULL_BEGIN
textColor:[UIColor whiteColor]
font:[UIFont ows_boldFontWithSize:fontSize]
diameter:self.diameter] avatarImage];
[self.contactsManager.avatarCache setObject:image forKey:cacheKey];
[self.contactsManager.avatarCache setImage:image forKey:self.signalId diameter:self.diameter];
return image;
}

View File

@ -7,6 +7,7 @@
NS_ASSUME_NONNULL_BEGIN
extern NSString *const kNSNotificationName_LocalProfileDidChange;
extern NSString *const kNSNotificationName_OtherUsersProfileWillChange;
extern NSString *const kNSNotificationName_OtherUsersProfileDidChange;
extern NSString *const kNSNotificationName_ProfileWhitelistDidChange;
extern NSString *const kNSNotificationKey_ProfileRecipientId;

View File

@ -86,6 +86,7 @@ NS_ASSUME_NONNULL_BEGIN
NSString *const kLocalProfileUniqueId = @"kLocalProfileUniqueId";
NSString *const kNSNotificationName_LocalProfileDidChange = @"kNSNotificationName_LocalProfileDidChange";
NSString *const kNSNotificationName_OtherUsersProfileWillChange = @"kNSNotificationName_OtherUsersProfileWillChange";
NSString *const kNSNotificationName_OtherUsersProfileDidChange = @"kNSNotificationName_OtherUsersProfileDidChange";
NSString *const kNSNotificationName_ProfileWhitelistDidChange = @"kNSNotificationName_ProfileWhitelistDidChange";
NSString *const kNSNotificationKey_ProfileRecipientId = @"kNSNotificationKey_ProfileRecipientId";
@ -239,6 +240,12 @@ const NSUInteger kOWSProfileManager_MaxAvatarDiameter = 640;
object:nil
userInfo:nil];
} else {
[[NSNotificationCenter defaultCenter]
postNotificationName:kNSNotificationName_OtherUsersProfileWillChange
object:nil
userInfo:@{
kNSNotificationKey_ProfileRecipientId : userProfile.recipientId,
}];
[[NSNotificationCenter defaultCenter]
postNotificationName:kNSNotificationName_OtherUsersProfileDidChange
object:nil

View File

@ -212,10 +212,10 @@ const NSUInteger kAvatarViewDiameter = 52;
name:kNSNotificationName_OtherUsersProfileDidChange
object:nil];
[self updateNameLabel];
[self updateAvatarView];
self.snippetLabel.attributedText = snippetText;
self.timeLabel.attributedText = attributedDate;
self.avatarView.image = nil;
self.separatorInset = UIEdgeInsetsMake(0, self.avatarSize * 1.5f, 0, 0);
@ -229,11 +229,27 @@ const NSUInteger kAvatarViewDiameter = 52;
self.unreadBadge.hidden = YES;
self.unreadLabel.hidden = YES;
}
}
- (void)updateAvatarView
{
OWSContactsManager *contactsManager = self.contactsManager;
if (contactsManager == nil) {
OWSFail(@"%@ contactsManager should not be nil", self.logTag);
self.avatarView.image = nil;
return;
}
TSThread *thread = self.thread;
if (thread == nil) {
OWSFail(@"%@ thread should not be nil", self.logTag);
self.avatarView.image = nil;
return;
}
self.avatarView.image =
[OWSAvatarBuilder buildImageForThread:thread diameter:kAvatarViewDiameter contactsManager:contactsManager];
}
#pragma mark - Date formatting
- (NSAttributedString *)dateAttributedString:(NSDate *)date {
@ -286,6 +302,7 @@ const NSUInteger kAvatarViewDiameter = 52;
}
[self updateNameLabel];
[self updateAvatarView];
}
-(void)updateNameLabel

View File

@ -11,13 +11,20 @@ extern NSString *const OWSContactsManagerSignalAccountsDidChangeNotification;
@class UIFont;
@class SignalAccount;
@class ImageCache;
/**
* Get latest Signal contacts, and be notified when they change.
*/
@interface OWSContactsManager : NSObject <ContactsManagerProtocol>
@property (nonnull, readonly) NSCache<NSString *, UIImage *> *avatarCache;
#pragma mark - Setup
- (void)startObserving;
#pragma mark - Accessors
@property (nonnull, readonly) ImageCache *avatarCache;
@property (atomic, readonly) NSArray<Contact *> *allContacts;
@ -54,7 +61,14 @@ extern NSString *const OWSContactsManagerSignalAccountsDidChangeNotification;
- (BOOL)hasNameInSystemContactsForRecipientId:(NSString *)recipientId;
- (NSString *)displayNameForPhoneIdentifier:(nullable NSString *)identifier;
- (NSString *)displayNameForSignalAccount:(SignalAccount *)signalAccount;
// Generally we prefer the formattedProfileName over the raw profileName so as to
// distinguish a profile name apart from a name pulled from the system's contacts.
// This helps clarify when the remote person chooses a potentially confusing profile name.
- (nullable NSString *)formattedProfileNameForRecipientId:(NSString *)recipientId;
- (nullable NSString *)profileNameForRecipientId:(NSString *)recipientId;
- (nullable NSString *)nameFromSystemContactsForRecipientId:(NSString *)recipientId;
- (nullable UIImage *)imageForPhoneIdentifier:(nullable NSString *)identifier;
- (NSAttributedString *)formattedDisplayNameForSignalAccount:(SignalAccount *)signalAccount font:(UIFont *_Nonnull)font;
- (NSAttributedString *)formattedFullNameForRecipientId:(NSString *)recipientId font:(UIFont *)font;

View File

@ -50,7 +50,7 @@ NSString *const kTSStorageManager_AccountLastNames = @"kTSStorageManager_Account
}
// TODO: We need to configure the limits of this cache.
_avatarCache = [NSCache new];
_avatarCache = [ImageCache new];
_allContacts = @[];
_signalAccountMap = @{};
_signalAccounts = @[];
@ -136,6 +136,24 @@ NSString *const kTSStorageManager_AccountLastNames = @"kTSStorageManager_Account
failure:failure];
}
- (void)startObserving
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(otherUsersProfileWillChange:)
name:kNSNotificationName_OtherUsersProfileWillChange
object:nil];
}
- (void)otherUsersProfileWillChange:(NSNotification *)notification
{
OWSAssert([NSThread isMainThread]);
NSString *recipientId = notification.userInfo[kNSNotificationKey_ProfileRecipientId];
OWSAssert(recipientId.length > 0);
[self.avatarCache removeAllImagesForKey:recipientId];
}
- (void)updateWithContacts:(NSArray<Contact *> *)contacts
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@ -154,7 +172,7 @@ NSString *const kTSStorageManager_AccountLastNames = @"kTSStorageManager_Account
self.allContacts = contacts;
self.allContactsMap = [allContactsMap copy];
[self.avatarCache removeAllObjects];
[self.avatarCache removeAllImages];
[self intersectContacts];
@ -423,16 +441,25 @@ NSString *const kTSStorageManager_AccountLastNames = @"kTSStorageManager_Account
return [NSString stringWithFormat:profileNameFormatString, profileName];
}
- (nullable NSString *)profileNameForRecipientId:(NSString *)recipientId
{
return [self.profileManager profileNameForRecipientId:recipientId];
}
- (nullable NSString *)nameFromSystemContactsForRecipientId:(NSString *)recipientId
{
return [self cachedDisplayNameForRecipientId:recipientId];
}
- (NSString *_Nonnull)displayNameForPhoneIdentifier:(NSString *_Nullable)recipientId
{
if (!recipientId) {
return self.unknownContactName;
}
// Prefer a saved name from system contacts, if available
NSString *_Nullable displayName = [self cachedDisplayNameForRecipientId:recipientId];
NSString *_Nullable displayName = [self nameFromSystemContactsForRecipientId:recipientId];
// Else fall back to just using their recipientId
// Fall back to just using their recipientId
if (displayName.length < 1) {
displayName = recipientId;
}

View File

@ -0,0 +1,53 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
import Foundation
import UIKit
class ImageCacheRecord: NSObject {
var variations: [CGFloat: UIImage]
init(variations: [CGFloat: UIImage]) {
self.variations = variations
}
}
/**
* A two dimensional hash, allowing you to store variations under a single key.
* This is useful because we generate multiple diameters of an image, but when we
* want to clear out the images for a key we want to clear out *all* variations.
*/
@objc
class ImageCache: NSObject {
let backingCache: NSCache<AnyObject, ImageCacheRecord>
override init() {
self.backingCache = NSCache()
}
func image(forKey key: AnyObject, diameter: CGFloat) -> UIImage? {
guard let record = backingCache.object(forKey: key) else {
return nil
}
return record.variations[diameter]
}
func setImage(_ image: UIImage, forKey key: AnyObject, diameter: CGFloat) {
if let existingRecord = backingCache.object(forKey: key) {
existingRecord.variations[diameter] = image
backingCache.setObject(existingRecord, forKey: key)
} else {
let newRecord = ImageCacheRecord(variations: [diameter: image])
backingCache.setObject(newRecord, forKey: key)
}
}
func removeAllImages() {
backingCache.removeAllObjects()
}
func removeAllImages(forKey key: AnyObject) {
backingCache.removeObject(forKey: key)
}
}

View File

@ -129,6 +129,7 @@ const NSUInteger kContactTableViewCellAvatarSize = 40;
name:kNSNotificationName_OtherUsersProfileDidChange
object:nil];
[self updateProfileName];
[self updateAvatar];
if (self.accessoryMessage) {
UILabel *blockedLabel = [[UILabel alloc] init];
@ -141,10 +142,6 @@ const NSUInteger kContactTableViewCellAvatarSize = 40;
self.accessoryView = blockedLabel;
}
self.avatarView.image = [[[OWSContactAvatarBuilder alloc] initWithSignalId:recipientId
diameter:kContactTableViewCellAvatarSize
contactsManager:contactsManager] build];
// Force layout, since imageView isn't being initally rendered on App Store optimized build.
[self layoutSubviews];
}
@ -197,7 +194,26 @@ const NSUInteger kContactTableViewCellAvatarSize = 40;
return [text copy];
}
- (void)updateAvatar
{
OWSContactsManager *contactsManager = self.contactsManager;
if (contactsManager == nil) {
OWSFail(@"%@ contactsManager should not be nil", self.logTag);
self.avatarView.image = nil;
return;
}
NSString *recipientId = self.recipientId;
if (recipientId.length == 0) {
OWSFail(@"%@ recipientId should not be nil", self.logTag);
self.avatarView.image = nil;
return;
}
self.avatarView.image = [[[OWSContactAvatarBuilder alloc] initWithSignalId:recipientId
diameter:kContactTableViewCellAvatarSize
contactsManager:contactsManager] build];
}
- (void)updateProfileName
{
OWSContactsManager *contactsManager = self.contactsManager;
@ -246,6 +262,7 @@ const NSUInteger kContactTableViewCellAvatarSize = 40;
if (recipientId.length > 0 && [self.recipientId isEqualToString:recipientId]) {
[self updateProfileName];
[self updateAvatar];
}
}

View File

@ -0,0 +1,60 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
import XCTest
class ImageCacheTest: XCTestCase {
var imageCache: ImageCache!
let firstVariation = UIImage()
let secondVariation = UIImage()
let otherImage = UIImage()
let cacheKey1 = "cache-key-1" as NSString
let cacheKey2 = "cache-key-2" as NSString
override func setUp() {
super.setUp()
self.imageCache = ImageCache()
imageCache.setImage(firstVariation, forKey:cacheKey1, diameter:100)
imageCache.setImage(secondVariation, forKey:cacheKey1, diameter:200)
imageCache.setImage(otherImage, forKey:cacheKey2, diameter:100)
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
func testSetGet() {
XCTAssertEqual(firstVariation, imageCache.image(forKey:cacheKey1, diameter: 100))
XCTAssertEqual(secondVariation, imageCache.image(forKey:cacheKey1, diameter: 200))
XCTAssertNotEqual(secondVariation, imageCache.image(forKey:cacheKey1, diameter: 100))
XCTAssertEqual(otherImage, imageCache.image(forKey:cacheKey2, diameter: 100))
XCTAssertNil(imageCache.image(forKey:cacheKey2, diameter: 200))
}
func testRemoveAllForKey() {
// sanity check
XCTAssertEqual(firstVariation, imageCache.image(forKey:cacheKey1, diameter: 100))
XCTAssertEqual(otherImage, imageCache.image(forKey:cacheKey2, diameter: 100))
imageCache.removeAllImages(forKey:cacheKey1)
XCTAssertNil(imageCache.image(forKey:cacheKey1, diameter: 100))
XCTAssertNil(imageCache.image(forKey:cacheKey1, diameter: 200))
XCTAssertEqual(otherImage, imageCache.image(forKey:cacheKey2, diameter: 100))
}
func testRemoveAll() {
XCTAssertEqual(firstVariation, imageCache.image(forKey:cacheKey1, diameter: 100))
imageCache.removeAllImages()
XCTAssertNil(imageCache.image(forKey:cacheKey1, diameter: 100))
XCTAssertNil(imageCache.image(forKey:cacheKey1, diameter: 200))
XCTAssertNil(imageCache.image(forKey:cacheKey2, diameter: 100))
}
}

View File

@ -171,7 +171,7 @@ NSString *const OWSMessageProcessingJobFinderExtensionGroup = @"OWSMessageProces
{
YapDatabaseView *existingView = [database registeredExtension:OWSMessageProcessingJobFinderExtensionName];
if (existingView) {
OWSFail(@"%@ was already initailized.", OWSMessageProcessingJobFinderExtensionName);
OWSFail(@"%@ was already initialized.", OWSMessageProcessingJobFinderExtensionName);
// already initialized
return;
}