Move contact offers to Conversation view model.

This commit is contained in:
Matthew Chen 2018-12-13 09:12:41 -05:00
parent 98210e92d8
commit fea40d571c
5 changed files with 306 additions and 226 deletions

View file

@ -20,6 +20,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic) UIButton *blockButton;
@property (nonatomic) NSArray<NSLayoutConstraint *> *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<UIButton *> *)buttons
{
return @[
self.addToContactsButton,
self.addToProfileWhitelistButton,
self.blockButton,
];
}
- (CGFloat)topVMargin

View file

@ -9,6 +9,7 @@
#import "OWSQuotedReplyModel.h"
#import "Signal-Swift.h"
#import <SignalCoreKit/NSDate+OWS.h>
#import <SignalMessaging/OWSContactOffersInteraction.h>
#import <SignalMessaging/OWSContactsManager.h>
#import <SignalMessaging/OWSUnreadIndicator.h>
#import <SignalMessaging/ThreadUtil.h>
@ -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<ConversationViewItem> (^tryToAddViewItem)(TSInteraction *) = ^(TSInteraction *interaction) {
OWSAssertDebug(interaction.uniqueId.length > 0);
id<ConversationViewItem> _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<TSInteraction *> *interactions = [NSMutableArray new];
NSMutableSet<NSString *> *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<ConversationViewItem> _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<ConversationViewItem> 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<ConversationViewItem> _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);
}
}];

View file

@ -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

View file

@ -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

View file

@ -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<TSInvalidIdentityKeyErrorMessage *> *blockingSafetyNumberChanges = [NSMutableArray new];
NSMutableArray<TSInteraction *> *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<TSInvalidIdentityKeyErrorMessage *> *)blockingSafetyNumberChanges
nonBlockingSafetyNumberChanges:(NSArray<TSInteraction *> *)nonBlockingSafetyNumberChanges
@ -717,9 +540,6 @@ NS_ASSUME_NONNULL_BEGIN
}
NSInteger unreadIndicatorPosition = visibleUnseenMessageCount;
if (shouldHaveContactOffers) {
unreadIndicatorPosition++;
}
dynamicInteractions.unreadIndicator = [[OWSUnreadIndicator alloc]
initUnreadIndicatorWithTimestamp:interactionAfterUnreadIndicator.timestampForSorting