From fea40d571c8b0d79309c4003778fa8ad80a0d3b9 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 13 Dec 2018 09:12:41 -0500 Subject: [PATCH] Move contact offers to Conversation view model. --- .../Cells/OWSContactOffersCell.m | 43 ++- .../ConversationView/ConversationViewModel.m | 266 ++++++++++++++++-- .../Models/OWSContactOffersInteraction.h | 15 +- .../Models/OWSContactOffersInteraction.m | 22 +- SignalMessaging/utils/ThreadUtil.m | 186 +----------- 5 files changed, 306 insertions(+), 226 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSContactOffersCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSContactOffersCell.m index d6c7b12f3..48b9e4783 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSContactOffersCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSContactOffersCell.m @@ -20,6 +20,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) UIButton *blockButton; @property (nonatomic) NSArray *layoutConstraints; @property (nonatomic) UIStackView *stackView; +@property (nonatomic) UIStackView *buttonStackView; @end @@ -65,13 +66,10 @@ NS_ASSUME_NONNULL_BEGIN @"Message shown in conversation view that offers to block an unknown user.") selector:@selector(block)]; - UIStackView *buttonStackView = [[UIStackView alloc] initWithArrangedSubviews:@[ - self.addToContactsButton, - self.addToProfileWhitelistButton, - self.blockButton, - ]]; + UIStackView *buttonStackView = [[UIStackView alloc] initWithArrangedSubviews:self.buttons]; buttonStackView.axis = UILayoutConstraintAxisVertical; buttonStackView.spacing = self.vSpacing; + self.buttonStackView = buttonStackView; self.stackView = [[UIStackView alloc] initWithArrangedSubviews:@[ self.titleLabel, @@ -121,11 +119,7 @@ NS_ASSUME_NONNULL_BEGIN [self configureFonts]; self.titleLabel.textColor = Theme.secondaryColor; - for (UIButton *button in @[ - self.addToContactsButton, - self.addToProfileWhitelistButton, - self.blockButton, - ]) { + for (UIButton *button in self.buttons) { [button setTitleColor:[UIColor ows_signalBlueColor] forState:UIControlStateNormal]; [button setBackgroundColor:Theme.conversationButtonBackgroundColor]; } @@ -152,6 +146,35 @@ NS_ASSUME_NONNULL_BEGIN [self.stackView autoPinEdgeToSuperviewEdge:ALEdgeTrailing withInset:self.conversationStyle.fullWidthGutterTrailing], ]; + + // This hack fixes a bug that I don't understand. + // + // On an iPhone 5C running iOS 10.3.3, + // + // * Alice is a contact for which we should show some but not all contact offer buttons. + // * Delete thread with Alice. + // * Send yourself a message from Alice. + // * Open conversation with Alice. + // + // Expected: Some (but not all) offer buttons are displayed. + // Observed: All offer buttons are displayed, in a cramped layout. + for (UIButton *button in self.buttons) { + [button removeFromSuperview]; + } + for (UIButton *button in self.buttons) { + if (!button.hidden) { + [self.buttonStackView addArrangedSubview:button]; + } + } +} + +- (NSArray *)buttons +{ + return @[ + self.addToContactsButton, + self.addToProfileWhitelistButton, + self.blockButton, + ]; } - (CGFloat)topVMargin diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewModel.m b/Signal/src/ViewControllers/ConversationView/ConversationViewModel.m index 17ccc9c4f..07fca0c5c 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewModel.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewModel.m @@ -9,6 +9,7 @@ #import "OWSQuotedReplyModel.h" #import "Signal-Swift.h" #import +#import #import #import #import @@ -206,7 +207,19 @@ static const int kYapDatabaseRangeMinLength = 0; return SSKEnvironment.shared.typingIndicators; } -#pragma mark +- (TSAccountManager *)tsAccountManager +{ + OWSAssertDebug(SSKEnvironment.shared.tsAccountManager); + + return SSKEnvironment.shared.tsAccountManager; +} + +- (OWSProfileManager *)profileManager +{ + return [OWSProfileManager sharedManager]; +} + +#pragma mark - - (void)addNotificationListeners { @@ -222,6 +235,14 @@ static const int kYapDatabaseRangeMinLength = 0; selector:@selector(typingIndicatorStateDidChange:) name:[OWSTypingIndicatorsImpl typingIndicatorStateDidChange] object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(profileWhitelistDidChange:) + name:kNSNotificationName_ProfileWhitelistDidChange + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(blockListDidChange:) + name:kNSNotificationName_BlockListDidChange + object:nil]; } - (void)signalAccountsDidChange:(NSNotification *)notification @@ -231,6 +252,30 @@ static const int kYapDatabaseRangeMinLength = 0; [self ensureDynamicInteractions]; } +- (void)profileWhitelistDidChange:(NSNotification *)notification +{ + OWSAssertIsOnMainThread(); + + // If profile whitelist just changed, we may want to hide a profile whitelist offer. + NSString *_Nullable recipientId = notification.userInfo[kNSNotificationKey_ProfileRecipientId]; + NSData *_Nullable groupId = notification.userInfo[kNSNotificationKey_ProfileGroupId]; + if (recipientId.length > 0 && [self.thread.recipientIdentifiers containsObject:recipientId]) { + [self updateForTransientItems]; + } else if (groupId.length > 0 && self.thread.isGroupThread) { + TSGroupThread *groupThread = (TSGroupThread *)self.thread; + if ([groupThread.groupModel.groupId isEqualToData:groupId]) { + [self updateForTransientItems]; + } + } +} + +- (void)blockListDidChange:(id)notification +{ + OWSAssertIsOnMainThread(); + + [self updateForTransientItems]; +} + - (void)configure { OWSLogInfo(@""); @@ -868,6 +913,165 @@ static const int kYapDatabaseRangeMinLength = 0; #pragma mark - View Items +- (nullable OWSContactOffersInteraction *)tryToBuildContactOffersInteraction +{ + + // Many OWSProfileManager methods aren't safe to call from inside a database + // transaction, so do this work now. + // + // TODO: It'd be nice if these methods took a transaction. + BOOL hasLocalProfile = [self.profileManager hasLocalProfile]; + BOOL isThreadInProfileWhitelist = [self.profileManager isThreadInProfileWhitelist:self.thread]; + BOOL hasUnwhitelistedMember = NO; + for (NSString *recipientId in self.thread.recipientIdentifiers) { + if (![self.profileManager isUserInProfileWhitelist:recipientId]) { + hasUnwhitelistedMember = YES; + break; + } + } + + __block OWSContactOffersInteraction *_Nullable offers = nil; + [self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + offers = [self tryToBuildContactOffersInteractionWithTransaction:transaction + hasLocalProfile:hasLocalProfile + isThreadInProfileWhitelist:isThreadInProfileWhitelist + hasUnwhitelistedMember:hasUnwhitelistedMember]; + }]; + return offers; +} + +- (nullable OWSContactOffersInteraction *) + tryToBuildContactOffersInteractionWithTransaction:(YapDatabaseReadTransaction *)transaction + hasLocalProfile:(BOOL)hasLocalProfile + isThreadInProfileWhitelist:(BOOL)isThreadInProfileWhitelist + hasUnwhitelistedMember:(BOOL)hasUnwhitelistedMember +{ + OWSAssertDebug(transaction); + + TSThread *thread = self.thread; + BOOL isContactThread = [thread isKindOfClass:[TSContactThread class]]; + if (!isContactThread) { + return nil; + } + TSContactThread *contactThread = (TSContactThread *)thread; + if (contactThread.hasDismissedOffers) { + return nil; + } + + NSString *localNumber = [self.tsAccountManager localNumber]; + OWSAssertDebug(localNumber.length > 0); + + const int kMaxBlockOfferOutgoingMessageCount = 10; + + __block TSInteraction *firstCallOrMessage = nil; + [[transaction ext:TSMessageDatabaseViewExtensionName] + enumerateRowsInGroup:thread.uniqueId + usingBlock:^( + NSString *collection, NSString *key, id object, id metadata, NSUInteger index, BOOL *stop) { + OWSAssertDebug([object isKindOfClass:[TSInteraction class]]); + + if ([object isKindOfClass:[TSIncomingMessage class]] || + [object isKindOfClass:[TSOutgoingMessage class]] || [object isKindOfClass:[TSCall class]]) { + firstCallOrMessage = object; + *stop = YES; + } + }]; + if (!firstCallOrMessage) { + return nil; + } + + NSUInteger outgoingMessageCount = + [[TSDatabaseView threadOutgoingMessageDatabaseView:transaction] numberOfItemsInGroup:thread.uniqueId]; + + BOOL shouldHaveBlockOffer = YES; + BOOL shouldHaveAddToContactsOffer = YES; + BOOL shouldHaveAddToProfileWhitelistOffer = YES; + + NSString *recipientId = ((TSContactThread *)thread).contactIdentifier; + + if ([recipientId isEqualToString:localNumber]) { + // Don't add self to contacts. + shouldHaveAddToContactsOffer = NO; + // Don't bother to block self. + shouldHaveBlockOffer = NO; + // Don't bother adding self to profile whitelist. + shouldHaveAddToProfileWhitelistOffer = NO; + } else { + if ([[self.blockingManager blockedPhoneNumbers] containsObject:recipientId]) { + // Only create "add to contacts" offers for users which are not already blocked. + shouldHaveAddToContactsOffer = NO; + // Only create block offers for users which are not already blocked. + shouldHaveBlockOffer = NO; + // Don't create profile whitelist offers for users which are not already blocked. + shouldHaveAddToProfileWhitelistOffer = NO; + } + + if ([self.contactsManager hasSignalAccountForRecipientId:recipientId]) { + // Only create "add to contacts" offers for non-contacts. + shouldHaveAddToContactsOffer = NO; + // Only create block offers for non-contacts. + shouldHaveBlockOffer = NO; + // Don't create profile whitelist offers for non-contacts. + shouldHaveAddToProfileWhitelistOffer = NO; + } + } + + if (outgoingMessageCount > kMaxBlockOfferOutgoingMessageCount) { + // If the user has sent more than N messages, don't show a block offer. + shouldHaveBlockOffer = NO; + } + + BOOL hasOutgoingBeforeIncomingInteraction = [firstCallOrMessage isKindOfClass:[TSOutgoingMessage class]]; + if ([firstCallOrMessage isKindOfClass:[TSCall class]]) { + TSCall *call = (TSCall *)firstCallOrMessage; + hasOutgoingBeforeIncomingInteraction + = (call.callType == RPRecentCallTypeOutgoing || call.callType == RPRecentCallTypeOutgoingIncomplete); + } + if (hasOutgoingBeforeIncomingInteraction) { + // If there is an outgoing message before an incoming message + // the local user initiated this conversation, don't show a block offer. + shouldHaveBlockOffer = NO; + } + + if (!hasLocalProfile || isThreadInProfileWhitelist) { + // Don't show offer if thread is local user hasn't configured their profile. + // Don't show offer if thread is already in profile whitelist. + shouldHaveAddToProfileWhitelistOffer = NO; + } else if (thread.isGroupThread && !hasUnwhitelistedMember) { + // Don't show offer in group thread if all members are already individually + // whitelisted. + shouldHaveAddToProfileWhitelistOffer = NO; + } + + BOOL shouldHaveContactOffers + = (shouldHaveBlockOffer || shouldHaveAddToContactsOffer || shouldHaveAddToProfileWhitelistOffer); + if (!shouldHaveContactOffers) { + return nil; + } + + // We want the offers to be the first interactions in their + // conversation's timeline, so we back-date them to slightly before + // the first message - or at an arbitrary old timestamp if the + // conversation has no messages. + uint64_t contactOffersTimestamp = firstCallOrMessage.timestamp - 1; + // This view model uses the "unique id" to identify this interaction, + // but the interaction is never saved in the database so the specific + // value doesn't matter. + NSString *uniqueId = @"contact-offers"; + OWSContactOffersInteraction *offersMessage = + [[OWSContactOffersInteraction alloc] initInteractionWithUniqueId:uniqueId + timestamp:contactOffersTimestamp + thread:thread + hasBlockOffer:shouldHaveBlockOffer + hasAddToContactsOffer:shouldHaveAddToContactsOffer + hasAddToProfileWhitelistOffer:shouldHaveAddToProfileWhitelistOffer + recipientId:recipientId + beforeInteractionId:firstCallOrMessage.uniqueId]; + + OWSLogInfo(@"Creating contact offers: %@ (%llu)", offersMessage.uniqueId, offersMessage.timestampForSorting); + return offersMessage; +} + // This is a key method. It builds or rebuilds the list of // cell view models. // @@ -881,8 +1085,29 @@ static const int kYapDatabaseRangeMinLength = 0; BOOL isGroupThread = self.thread.isGroupThread; ConversationStyle *conversationStyle = self.delegate.conversationStyle; + OWSContactOffersInteraction *_Nullable offers = [self tryToBuildContactOffersInteraction]; + __block BOOL hasError = NO; [self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + id (^tryToAddViewItem)(TSInteraction *) = ^(TSInteraction *interaction) { + OWSAssertDebug(interaction.uniqueId.length > 0); + + id _Nullable viewItem = self.viewItemCache[interaction.uniqueId]; + if (!viewItem) { + viewItem = [[ConversationInteractionViewItem alloc] initWithInteraction:interaction + isGroupThread:isGroupThread + transaction:transaction + conversationStyle:conversationStyle]; + } + [viewItems addObject:viewItem]; + OWSAssertDebug(!viewItemCache[interaction.uniqueId]); + viewItemCache[interaction.uniqueId] = viewItem; + return viewItem; + }; + + NSMutableArray *interactions = [NSMutableArray new]; + NSMutableSet *interactionIds = [NSMutableSet new]; + YapDatabaseViewTransaction *viewTransaction = [transaction ext:TSMessageDatabaseViewExtensionName]; OWSAssertDebug(viewTransaction); for (NSUInteger row = 0; row < count; row++) { @@ -904,17 +1129,27 @@ static const int kYapDatabaseRangeMinLength = 0; hasError = YES; continue; } + [interactions addObject:interaction]; + [interactionIds addObject:interaction.uniqueId]; + } - id _Nullable viewItem = self.viewItemCache[interaction.uniqueId]; - if (!viewItem) { - viewItem = [[ConversationInteractionViewItem alloc] initWithInteraction:interaction - isGroupThread:isGroupThread - transaction:transaction - conversationStyle:conversationStyle]; + if (offers && [interactionIds containsObject:offers.beforeInteractionId]) { + id offersItem = tryToAddViewItem(offers); + if ([offersItem.interaction isKindOfClass:[OWSContactOffersInteraction class]]) { + OWSContactOffersInteraction *oldOffers = (OWSContactOffersInteraction *)offersItem.interaction; + BOOL didChange = (oldOffers.hasBlockOffer != offers.hasBlockOffer + || oldOffers.hasAddToContactsOffer != offers.hasAddToContactsOffer + || oldOffers.hasAddToProfileWhitelistOffer != offers.hasAddToProfileWhitelistOffer); + if (didChange) { + [offersItem clearCachedLayoutState]; + } + } else { + OWSFailDebug(@"Unexpected offers item: %@", offersItem.interaction.class); } - [viewItems addObject:viewItem]; - OWSAssertDebug(!viewItemCache[interaction.uniqueId]); - viewItemCache[interaction.uniqueId] = viewItem; + } + + for (TSInteraction *interaction in interactions) { + tryToAddViewItem(interaction); } if (self.typingIndicatorsSender) { @@ -924,16 +1159,7 @@ static const int kYapDatabaseRangeMinLength = 0; [[OWSTypingIndicatorInteraction alloc] initWithThread:self.thread timestamp:typingIndicatorTimestamp recipientId:self.typingIndicatorsSender]; - id _Nullable viewItem = self.viewItemCache[interaction.uniqueId]; - if (!viewItem) { - viewItem = [[ConversationInteractionViewItem alloc] initWithInteraction:interaction - isGroupThread:isGroupThread - transaction:transaction - conversationStyle:conversationStyle]; - } - [viewItems addObject:viewItem]; - OWSAssertDebug(!viewItemCache[interaction.uniqueId]); - viewItemCache[interaction.uniqueId] = viewItem; + tryToAddViewItem(interaction); } }]; diff --git a/SignalMessaging/Models/OWSContactOffersInteraction.h b/SignalMessaging/Models/OWSContactOffersInteraction.h index 2f3878123..f0bf70509 100644 --- a/SignalMessaging/Models/OWSContactOffersInteraction.h +++ b/SignalMessaging/Models/OWSContactOffersInteraction.h @@ -12,17 +12,20 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) BOOL hasAddToContactsOffer; @property (nonatomic, readonly) BOOL hasAddToProfileWhitelistOffer; @property (nonatomic, readonly) NSString *recipientId; +@property (nonatomic, readonly) NSString *beforeInteractionId; - (instancetype)initInteractionWithTimestamp:(uint64_t)timestamp inThread:(TSThread *)thread NS_UNAVAILABLE; - (instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER; -- (instancetype)initContactOffersWithTimestamp:(uint64_t)timestamp - thread:(TSThread *)thread - hasBlockOffer:(BOOL)hasBlockOffer - hasAddToContactsOffer:(BOOL)hasAddToContactsOffer - hasAddToProfileWhitelistOffer:(BOOL)hasAddToProfileWhitelistOffer - recipientId:(NSString *)recipientId NS_DESIGNATED_INITIALIZER; +- (instancetype)initInteractionWithUniqueId:(NSString *)uniqueId + timestamp:(uint64_t)timestamp + thread:(TSThread *)thread + hasBlockOffer:(BOOL)hasBlockOffer + hasAddToContactsOffer:(BOOL)hasAddToContactsOffer + hasAddToProfileWhitelistOffer:(BOOL)hasAddToProfileWhitelistOffer + recipientId:(NSString *)recipientId + beforeInteractionId:(NSString *)beforeInteractionId NS_DESIGNATED_INITIALIZER; @end diff --git a/SignalMessaging/Models/OWSContactOffersInteraction.m b/SignalMessaging/Models/OWSContactOffersInteraction.m index 6be86f2e1..7d3ca110d 100644 --- a/SignalMessaging/Models/OWSContactOffersInteraction.m +++ b/SignalMessaging/Models/OWSContactOffersInteraction.m @@ -13,14 +13,16 @@ NS_ASSUME_NONNULL_BEGIN return [super initWithCoder:coder]; } -- (instancetype)initContactOffersWithTimestamp:(uint64_t)timestamp - thread:(TSThread *)thread - hasBlockOffer:(BOOL)hasBlockOffer - hasAddToContactsOffer:(BOOL)hasAddToContactsOffer - hasAddToProfileWhitelistOffer:(BOOL)hasAddToProfileWhitelistOffer - recipientId:(NSString *)recipientId +- (instancetype)initInteractionWithUniqueId:(NSString *)uniqueId + timestamp:(uint64_t)timestamp + thread:(TSThread *)thread + hasBlockOffer:(BOOL)hasBlockOffer + hasAddToContactsOffer:(BOOL)hasAddToContactsOffer + hasAddToProfileWhitelistOffer:(BOOL)hasAddToProfileWhitelistOffer + recipientId:(NSString *)recipientId + beforeInteractionId:(NSString *)beforeInteractionId { - self = [super initInteractionWithTimestamp:timestamp inThread:thread]; + self = [super initInteractionWithUniqueId:uniqueId timestamp:timestamp inThread:thread]; if (!self) { return self; @@ -31,6 +33,7 @@ NS_ASSUME_NONNULL_BEGIN _hasAddToProfileWhitelistOffer = hasAddToProfileWhitelistOffer; OWSAssertDebug(recipientId.length > 0); _recipientId = recipientId; + _beforeInteractionId = beforeInteractionId; return self; } @@ -52,6 +55,11 @@ NS_ASSUME_NONNULL_BEGIN return OWSInteractionType_Offer; } +- (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSFailDebug(@"This interaction should never be saved to the database."); +} + @end NS_ASSUME_NONNULL_END diff --git a/SignalMessaging/utils/ThreadUtil.m b/SignalMessaging/utils/ThreadUtil.m index 068532fa1..0274d1b56 100644 --- a/SignalMessaging/utils/ThreadUtil.m +++ b/SignalMessaging/utils/ThreadUtil.m @@ -336,31 +336,12 @@ NS_ASSUME_NONNULL_BEGIN OWSAssertDebug(blockingManager); OWSAssertDebug(maxRangeSize > 0); - NSString *localNumber = [TSAccountManager localNumber]; - OWSAssertDebug(localNumber.length > 0); - - // Many OWSProfileManager methods aren't safe to call from inside a database - // transaction, so do this work now. - OWSProfileManager *profileManager = OWSProfileManager.sharedManager; - BOOL hasLocalProfile = [profileManager hasLocalProfile]; - BOOL isThreadInProfileWhitelist = [profileManager isThreadInProfileWhitelist:thread]; - BOOL hasUnwhitelistedMember = NO; - for (NSString *recipientId in thread.recipientIdentifiers) { - if (![profileManager isUserInProfileWhitelist:recipientId]) { - hasUnwhitelistedMember = YES; - break; - } - } - ThreadDynamicInteractions *result = [ThreadDynamicInteractions new]; [dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - const int kMaxBlockOfferOutgoingMessageCount = 10; - // Find any "dynamic" interactions and safety number changes. // // We use different views for performance reasons. - __block OWSContactOffersInteraction *existingContactOffers = nil; NSMutableArray *blockingSafetyNumberChanges = [NSMutableArray new]; NSMutableArray *nonBlockingSafetyNumberChanges = [NSMutableArray new]; // We want to delete legacy and duplicate interactions. @@ -383,16 +364,11 @@ NS_ASSUME_NONNULL_BEGIN // the OWSContactOffersInteraction. [interactionsToDelete addObject:object]; } else if ([object isKindOfClass:[TSUnreadIndicatorInteraction class]]) { - // Remove obsolete unread indicator interactions; + // Remove obsolete unread indicator interactions. [interactionsToDelete addObject:object]; } else if ([object isKindOfClass:[OWSContactOffersInteraction class]]) { - OWSAssertDebug(!existingContactOffers); - if (existingContactOffers) { - // There should never be more than one "contact offers" in - // a given thread, but if there is, discard all but one. - [interactionsToDelete addObject:existingContactOffers]; - } - existingContactOffers = (OWSContactOffersInteraction *)object; + // Remove obsolete contact offers. + [interactionsToDelete addObject:object]; } else if ([object isKindOfClass:[TSInvalidIdentityKeyErrorMessage class]]) { [blockingSafetyNumberChanges addObject:object]; } else if ([object isKindOfClass:[TSErrorMessage class]]) { @@ -427,161 +403,9 @@ NS_ASSUME_NONNULL_BEGIN } } - __block TSInteraction *firstCallOrMessage = nil; - [[transaction ext:TSMessageDatabaseViewExtensionName] - enumerateRowsInGroup:thread.uniqueId - usingBlock:^( - NSString *collection, NSString *key, id object, id metadata, NSUInteger index, BOOL *stop) { - - OWSAssertDebug([object isKindOfClass:[TSInteraction class]]); - - if ([object isKindOfClass:[TSIncomingMessage class]] || - [object isKindOfClass:[TSOutgoingMessage class]] || - [object isKindOfClass:[TSCall class]]) { - firstCallOrMessage = object; - *stop = YES; - } - }]; - - NSUInteger outgoingMessageCount = - [[TSDatabaseView threadOutgoingMessageDatabaseView:transaction] numberOfItemsInGroup:thread.uniqueId]; - - BOOL shouldHaveBlockOffer = YES; - BOOL shouldHaveAddToContactsOffer = YES; - BOOL shouldHaveAddToProfileWhitelistOffer = YES; - - BOOL isContactThread = [thread isKindOfClass:[TSContactThread class]]; - if (!isContactThread) { - // Only create "add to contacts" offers in 1:1 conversations. - shouldHaveAddToContactsOffer = NO; - // Only create block offers in 1:1 conversations. - shouldHaveBlockOffer = NO; - - // MJK TODO - any conditions under which we'd make a block offer for groups? - - // Only create profile whitelist offers in 1:1 conversations. - shouldHaveAddToProfileWhitelistOffer = NO; - } else { - NSString *recipientId = ((TSContactThread *)thread).contactIdentifier; - - if ([recipientId isEqualToString:localNumber]) { - // Don't add self to contacts. - shouldHaveAddToContactsOffer = NO; - // Don't bother to block self. - shouldHaveBlockOffer = NO; - // Don't bother adding self to profile whitelist. - shouldHaveAddToProfileWhitelistOffer = NO; - } else { - if ([[blockingManager blockedPhoneNumbers] containsObject:recipientId]) { - // Only create "add to contacts" offers for users which are not already blocked. - shouldHaveAddToContactsOffer = NO; - // Only create block offers for users which are not already blocked. - shouldHaveBlockOffer = NO; - // Don't create profile whitelist offers for users which are not already blocked. - shouldHaveAddToProfileWhitelistOffer = NO; - } - - if ([contactsManager hasSignalAccountForRecipientId:recipientId]) { - // Only create "add to contacts" offers for non-contacts. - shouldHaveAddToContactsOffer = NO; - // Only create block offers for non-contacts. - shouldHaveBlockOffer = NO; - // Don't create profile whitelist offers for non-contacts. - shouldHaveAddToProfileWhitelistOffer = NO; - } - } - } - - if (!firstCallOrMessage) { - shouldHaveAddToContactsOffer = NO; - shouldHaveBlockOffer = NO; - shouldHaveAddToProfileWhitelistOffer = NO; - } - - if (outgoingMessageCount > kMaxBlockOfferOutgoingMessageCount) { - // If the user has sent more than N messages, don't show a block offer. - shouldHaveBlockOffer = NO; - } - - BOOL hasOutgoingBeforeIncomingInteraction = [firstCallOrMessage isKindOfClass:[TSOutgoingMessage class]]; - if ([firstCallOrMessage isKindOfClass:[TSCall class]]) { - TSCall *call = (TSCall *)firstCallOrMessage; - hasOutgoingBeforeIncomingInteraction - = (call.callType == RPRecentCallTypeOutgoing || call.callType == RPRecentCallTypeOutgoingIncomplete); - } - if (hasOutgoingBeforeIncomingInteraction) { - // If there is an outgoing message before an incoming message - // the local user initiated this conversation, don't show a block offer. - shouldHaveBlockOffer = NO; - } - - if (!hasLocalProfile || isThreadInProfileWhitelist) { - // Don't show offer if thread is local user hasn't configured their profile. - // Don't show offer if thread is already in profile whitelist. - shouldHaveAddToProfileWhitelistOffer = NO; - } else if (thread.isGroupThread && !hasUnwhitelistedMember) { - // Don't show offer in group thread if all members are already individually - // whitelisted. - shouldHaveAddToProfileWhitelistOffer = NO; - } - - BOOL shouldHaveContactOffers - = (shouldHaveBlockOffer || shouldHaveAddToContactsOffer || shouldHaveAddToProfileWhitelistOffer); - if (isContactThread) { - TSContactThread *contactThread = (TSContactThread *)thread; - if (contactThread.hasDismissedOffers) { - shouldHaveContactOffers = NO; - } - } - - // We want the offers to be the first interactions in their - // conversation's timeline, so we back-date them to slightly before - // the first message - or at an aribtrary old timestamp if the - // conversation has no messages. - uint64_t contactOffersTimestamp = [NSDate ows_millisecondTimeStamp]; - - // If the contact offers' properties have changed, discard the current - // one and create a new one. - if (existingContactOffers) { - if (existingContactOffers.hasBlockOffer != shouldHaveBlockOffer - || existingContactOffers.hasAddToContactsOffer != shouldHaveAddToContactsOffer - || existingContactOffers.hasAddToProfileWhitelistOffer != shouldHaveAddToProfileWhitelistOffer) { - OWSLogInfo(@"Removing stale contact offers: %@ (%llu)", - existingContactOffers.uniqueId, - existingContactOffers.timestampForSorting); - // Preserve the timestamp of the existing "contact offers" so that - // we replace it in the same position in the timeline. - contactOffersTimestamp = existingContactOffers.timestamp; - [existingContactOffers removeWithTransaction:transaction]; - existingContactOffers = nil; - } - } - - if (existingContactOffers && !shouldHaveContactOffers) { - OWSLogInfo(@"Removing contact offers: %@ (%llu)", - existingContactOffers.uniqueId, - existingContactOffers.timestampForSorting); - [existingContactOffers removeWithTransaction:transaction]; - } else if (!existingContactOffers && shouldHaveContactOffers) { - NSString *recipientId = ((TSContactThread *)thread).contactIdentifier; - - TSInteraction *offersMessage = - [[OWSContactOffersInteraction alloc] initContactOffersWithTimestamp:contactOffersTimestamp - thread:thread - hasBlockOffer:shouldHaveBlockOffer - hasAddToContactsOffer:shouldHaveAddToContactsOffer - hasAddToProfileWhitelistOffer:shouldHaveAddToProfileWhitelistOffer - recipientId:recipientId]; - [offersMessage saveWithTransaction:transaction]; - - OWSLogInfo( - @"Creating contact offers: %@ (%llu)", offersMessage.uniqueId, offersMessage.timestampForSorting); - } - [self ensureUnreadIndicator:result thread:thread transaction:transaction - shouldHaveContactOffers:shouldHaveContactOffers maxRangeSize:maxRangeSize blockingSafetyNumberChanges:blockingSafetyNumberChanges nonBlockingSafetyNumberChanges:nonBlockingSafetyNumberChanges @@ -602,7 +426,6 @@ NS_ASSUME_NONNULL_BEGIN + (void)ensureUnreadIndicator:(ThreadDynamicInteractions *)dynamicInteractions thread:(TSThread *)thread transaction:(YapDatabaseReadWriteTransaction *)transaction - shouldHaveContactOffers:(BOOL)shouldHaveContactOffers maxRangeSize:(int)maxRangeSize blockingSafetyNumberChanges:(NSArray *)blockingSafetyNumberChanges nonBlockingSafetyNumberChanges:(NSArray *)nonBlockingSafetyNumberChanges @@ -717,9 +540,6 @@ NS_ASSUME_NONNULL_BEGIN } NSInteger unreadIndicatorPosition = visibleUnseenMessageCount; - if (shouldHaveContactOffers) { - unreadIndicatorPosition++; - } dynamicInteractions.unreadIndicator = [[OWSUnreadIndicator alloc] initUnreadIndicatorWithTimestamp:interactionAfterUnreadIndicator.timestampForSorting