From 7ea4b85a2a31760c1c76f5580f3ca06e2dfd73f9 Mon Sep 17 00:00:00 2001
From: Michael Kirk <michael.code@endoftheworl.de>
Date: Wed, 13 Dec 2017 16:59:57 -0500
Subject: [PATCH] Persist signal accounts (and their embedded Contact)

// FREEBIE
---
 Signal/src/AppDelegate.m                      |  1 -
 .../OWSConversationSettingsViewController.m   |  2 +-
 Signal/src/contact/OWSContactsManager.h       | 25 +---------
 Signal/src/contact/OWSContactsManager.m       | 50 +++++++------------
 Signal/src/util/ThreadUtil.m                  |  2 +-
 SignalServiceKit/src/Contacts/Contact.h       |  5 +-
 SignalServiceKit/src/Contacts/SignalAccount.h |  7 ++-
 SignalServiceKit/src/Contacts/SignalAccount.m |  5 ++
 8 files changed, 31 insertions(+), 66 deletions(-)

diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m
index eeda565f1..d9c475323 100644
--- a/Signal/src/AppDelegate.m
+++ b/Signal/src/AppDelegate.m
@@ -881,7 +881,6 @@ static NSString *const kURLHostVerifyPrefix             = @"verify";
 
     [OWSProfileManager.sharedManager fetchLocalUsersProfile];
     [[OWSReadReceiptManager sharedManager] prepareCachedValues];
-    [[Environment getCurrent].contactsManager loadLastKnownContactRecipientIds];
 }
 
 - (void)registrationStateDidChange
diff --git a/Signal/src/ViewControllers/OWSConversationSettingsViewController.m b/Signal/src/ViewControllers/OWSConversationSettingsViewController.m
index 73e7b1ee7..4eaece5fb 100644
--- a/Signal/src/ViewControllers/OWSConversationSettingsViewController.m
+++ b/Signal/src/ViewControllers/OWSConversationSettingsViewController.m
@@ -171,7 +171,7 @@ NS_ASSUME_NONNULL_BEGIN
     OWSAssert([self.thread isKindOfClass:[TSContactThread class]]);
     TSContactThread *contactThread = (TSContactThread *)self.thread;
     NSString *recipientId = contactThread.contactIdentifier;
-    return [self.contactsManager.lastKnownContactRecipientIds containsObject:recipientId];
+    return [self.contactsManager signalAccountForRecipientId:recipientId] != nil;
 }
 
 #pragma mark - ContactEditingDelegate
diff --git a/Signal/src/contact/OWSContactsManager.h b/Signal/src/contact/OWSContactsManager.h
index fadb35f2b..aafde01aa 100644
--- a/Signal/src/contact/OWSContactsManager.h
+++ b/Signal/src/contact/OWSContactsManager.h
@@ -30,33 +30,10 @@ extern NSString *const OWSContactsManagerSignalAccountsDidChangeNotification;
 
 @property (atomic, readonly) NSDictionary<NSString *, Contact *> *allContactsMap;
 
-// signalAccountMap and signalAccounts hold the same data.
-// signalAccountMap is for lookup. signalAccounts contains the accounts
-// ordered by display order.
-@property (atomic, readonly) NSDictionary<NSString *, SignalAccount *> *signalAccountMap;
+// order of the signalAccounts array respects the systems contact sorting preference
 @property (atomic, readonly) NSArray<SignalAccount *> *signalAccounts;
-
-// This value is cached and is available immediately, before system contacts
-// fetch or contacts intersection.
-//
-// In some cases, its better if our UI reflects these values
-// which haven't been updated yet rather than assume that
-// we have no contacts until the first contacts intersection
-// successfully completes.
-//
-// This significantly improves the user experience when:
-//
-// * No contacts intersection has completed because the app has just launched.
-// * Contacts intersection can't complete due to an unreliable connection or
-//   the contacts intersection rate limit.
-@property (atomic, readonly) NSArray<NSString *> *lastKnownContactRecipientIds;
-
 - (nullable SignalAccount *)signalAccountForRecipientId:(NSString *)recipientId;
 
-- (Contact *)getOrBuildContactForPhoneIdentifier:(NSString *)identifier;
-
-- (void)loadLastKnownContactRecipientIds;
-
 #pragma mark - System Contact Fetching
 
 // Must call `requestSystemContactsOnce` before accessing this method
diff --git a/Signal/src/contact/OWSContactsManager.m b/Signal/src/contact/OWSContactsManager.m
index cc49c0a9d..8909aea8c 100644
--- a/Signal/src/contact/OWSContactsManager.m
+++ b/Signal/src/contact/OWSContactsManager.m
@@ -23,7 +23,6 @@ NSString *const kTSStorageManager_AccountDisplayNames = @"kTSStorageManager_Acco
 NSString *const kTSStorageManager_AccountFirstNames = @"kTSStorageManager_AccountFirstNames";
 NSString *const kTSStorageManager_AccountLastNames = @"kTSStorageManager_AccountLastNames";
 NSString *const kTSStorageManager_OWSContactsManager = @"kTSStorageManager_OWSContactsManager";
-NSString *const kTSStorageManager_lastKnownContactRecipientIds = @"lastKnownContactRecipientIds";
 
 @interface OWSContactsManager () <SystemContactsFetcherDelegate>
 
@@ -34,7 +33,6 @@ NSString *const kTSStorageManager_lastKnownContactRecipientIds = @"lastKnownCont
 @property (atomic) NSDictionary<NSString *, Contact *> *allContactsMap;
 @property (atomic) NSArray<SignalAccount *> *signalAccounts;
 @property (atomic) NSDictionary<NSString *, SignalAccount *> *signalAccountMap;
-@property (atomic) NSArray<NSString *> *lastKnownContactRecipientIds;
 @property (nonatomic, readonly) SystemContactsFetcher *systemContactsFetcher;
 
 @property (atomic) NSDictionary<NSString *, NSString *> *cachedAccountNameMap;
@@ -57,7 +55,6 @@ NSString *const kTSStorageManager_lastKnownContactRecipientIds = @"lastKnownCont
     _allContactsMap = @{};
     _signalAccountMap = @{};
     _signalAccounts = @[];
-    _lastKnownContactRecipientIds = @[];
     _systemContactsFetcher = [SystemContactsFetcher new];
     _systemContactsFetcher.delegate = self;
 
@@ -68,18 +65,6 @@ NSString *const kTSStorageManager_lastKnownContactRecipientIds = @"lastKnownCont
     return self;
 }
 
-- (void)loadLastKnownContactRecipientIds
-{
-    [TSStorageManager.sharedManager.newDatabaseConnection readWithBlock:^(
-        YapDatabaseReadTransaction *_Nonnull transaction) {
-        NSArray<NSString *> *_Nullable value = [transaction objectForKey:kTSStorageManager_lastKnownContactRecipientIds
-                                                            inCollection:kTSStorageManager_OWSContactsManager];
-        if (value) {
-            self.lastKnownContactRecipientIds = value;
-        }
-    }];
-}
-
 #pragma mark - System Contact Fetching
 
 // Request contacts access if you haven't asked recently.
@@ -245,16 +230,18 @@ NSString *const kTSStorageManager_lastKnownContactRecipientIds = @"lastKnownCont
             }
         }
 
-        NSArray<NSString *> *lastKnownContactRecipientIds = [signalAccountMap allKeys];
         [TSStorageManager.sharedManager.newDatabaseConnection
             readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
-                [transaction setObject:lastKnownContactRecipientIds
-                                forKey:kTSStorageManager_lastKnownContactRecipientIds
-                          inCollection:kTSStorageManager_OWSContactsManager];
+                // TODO we can be more efficient here.
+                // - only save the ones that changed
+                // - only remove the ones which no longer exist
+                [transaction removeAllObjectsInCollection:[SignalAccount collection]];
+                for (SignalAccount *signalAccount in signalAccounts) {
+                    [signalAccount saveWithTransaction:transaction];
+                }
             }];
 
         dispatch_async(dispatch_get_main_queue(), ^{
-            self.lastKnownContactRecipientIds = lastKnownContactRecipientIds;
             self.signalAccountMap = [signalAccountMap copy];
             self.signalAccounts = [signalAccounts copy];
 
@@ -682,25 +669,22 @@ NSString *const kTSStorageManager_lastKnownContactRecipientIds = @"lastKnownCont
 {
     OWSAssert(recipientId.length > 0);
 
-    return self.signalAccountMap[recipientId];
-}
+    SignalAccount *signalAccount = self.signalAccountMap[recipientId];
 
-- (Contact *)getOrBuildContactForPhoneIdentifier:(NSString *)identifier
-{
-    Contact *savedContact = self.allContactsMap[identifier];
-    if (savedContact) {
-        return savedContact;
-    } else {
-        return [[Contact alloc] initWithContactWithFirstName:self.unknownContactName
-                                                 andLastName:nil
-                                     andUserTextPhoneNumbers:@[ identifier ]
-                                                    andImage:nil
-                                                andContactID:0];
+    // If contact intersection hasn't completed, it might exist on disk
+    // even if it doesn't exist in memory yet.
+    if (!signalAccount) {
+        signalAccount = [SignalAccount fetchObjectWithUniqueID:recipientId];
     }
+
+    return signalAccount;
 }
 
 - (UIImage * _Nullable)imageForPhoneIdentifier:(NSString * _Nullable)identifier {
     Contact *contact = self.allContactsMap[identifier];
+    if (!contact) {
+        contact = [self signalAccountForRecipientId:identifier].contact;
+    }
 
     // Prefer the contact image from the local address book if available
     UIImage *_Nullable image = contact.image;
diff --git a/Signal/src/util/ThreadUtil.m b/Signal/src/util/ThreadUtil.m
index 048e03a86..eeb0551b7 100644
--- a/Signal/src/util/ThreadUtil.m
+++ b/Signal/src/util/ThreadUtil.m
@@ -388,7 +388,7 @@ NS_ASSUME_NONNULL_BEGIN
                     shouldHaveAddToProfileWhitelistOffer = NO;
                 }
 
-                BOOL isContact = [contactsManager.lastKnownContactRecipientIds containsObject:recipientId];
+                BOOL isContact = [contactsManager signalAccountForRecipientId:recipientId] != nil;
                 if (isContact) {
                     // Only create "add to contacts" offers for non-contacts.
                     shouldHaveAddToContactsOffer = NO;
diff --git a/SignalServiceKit/src/Contacts/Contact.h b/SignalServiceKit/src/Contacts/Contact.h
index 194efe86e..94fbf42b4 100644
--- a/SignalServiceKit/src/Contacts/Contact.h
+++ b/SignalServiceKit/src/Contacts/Contact.h
@@ -3,6 +3,7 @@
 //
 
 #import <AddressBook/AddressBook.h>
+#import <Mantle/MTLModel.h>
 
 NS_ASSUME_NONNULL_BEGIN
 
@@ -19,7 +20,7 @@ NS_ASSUME_NONNULL_BEGIN
 @class SignalRecipient;
 @class YapDatabaseReadTransaction;
 
-@interface Contact : NSObject
+@interface Contact : MTLModel
 
 @property (nullable, readonly, nonatomic) NSString *firstName;
 @property (nullable, readonly, nonatomic) NSString *lastName;
@@ -30,13 +31,13 @@ NS_ASSUME_NONNULL_BEGIN
 @property (readonly, nonatomic) NSArray<NSString *> *userTextPhoneNumbers;
 @property (readonly, nonatomic) NSArray<NSString *> *emails;
 @property (readonly, nonatomic) NSString *uniqueId;
+@property (nonatomic, readonly) BOOL isSignalContact;
 #if TARGET_OS_IOS
 @property (nullable, readonly, nonatomic) UIImage *image;
 @property (readonly, nonatomic) ABRecordID recordID;
 @property (nullable, nonatomic, readonly) CNContact *cnContact;
 #endif // TARGET_OS_IOS
 
-- (BOOL)isSignalContact;
 - (NSArray<SignalRecipient *> *)signalRecipientsWithTransaction:(YapDatabaseReadTransaction *)transaction;
 // TODO: Remove this method.
 - (NSArray<NSString *> *)textSecureIdentifiers;
diff --git a/SignalServiceKit/src/Contacts/SignalAccount.h b/SignalServiceKit/src/Contacts/SignalAccount.h
index ea52d17b3..d0be75d07 100644
--- a/SignalServiceKit/src/Contacts/SignalAccount.h
+++ b/SignalServiceKit/src/Contacts/SignalAccount.h
@@ -2,6 +2,8 @@
 //  Copyright (c) 2017 Open Whisper Systems. All rights reserved.
 //
 
+#import "TSYapDatabaseObject.h"
+
 NS_ASSUME_NONNULL_BEGIN
 
 @class Contact;
@@ -14,10 +16,7 @@ NS_ASSUME_NONNULL_BEGIN
 //   multiple instances of SignalAccount.
 // * For non-contacts, the contact property will be nil.
 //
-// New instances of SignalAccount for active accounts are
-// created every time we do a contacts intersection (e.g.
-// in response to a change to the device contacts).
-@interface SignalAccount : NSObject
+@interface SignalAccount : TSYapDatabaseObject
 
 // An E164 value identifying the signal account.
 //
diff --git a/SignalServiceKit/src/Contacts/SignalAccount.m b/SignalServiceKit/src/Contacts/SignalAccount.m
index 8f68f9505..9cb948e55 100644
--- a/SignalServiceKit/src/Contacts/SignalAccount.m
+++ b/SignalServiceKit/src/Contacts/SignalAccount.m
@@ -46,6 +46,11 @@ NS_ASSUME_NONNULL_BEGIN
     return [SignalRecipient recipientWithTextSecureIdentifier:self.recipientId withTransaction:transaction];
 }
 
+- (nullable NSString *)uniqueId
+{
+    return _recipientId;
+}
+
 @end
 
 NS_ASSUME_NONNULL_END