2018-10-31 15:05:24 +01:00
|
|
|
|
//
|
2019-01-03 20:39:32 +01:00
|
|
|
|
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
2018-10-31 15:05:24 +01:00
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
#import "ConversationViewModel.h"
|
|
|
|
|
#import "ConversationViewItem.h"
|
|
|
|
|
#import "DateUtil.h"
|
|
|
|
|
#import "OWSMessageBubbleView.h"
|
|
|
|
|
#import "OWSQuotedReplyModel.h"
|
|
|
|
|
#import "Signal-Swift.h"
|
|
|
|
|
#import <SignalCoreKit/NSDate+OWS.h>
|
2018-12-13 15:12:41 +01:00
|
|
|
|
#import <SignalMessaging/OWSContactOffersInteraction.h>
|
2018-10-31 15:05:24 +01:00
|
|
|
|
#import <SignalMessaging/OWSContactsManager.h>
|
|
|
|
|
#import <SignalMessaging/OWSUnreadIndicator.h>
|
2018-12-13 17:52:15 +01:00
|
|
|
|
#import <SignalMessaging/SignalMessaging-Swift.h>
|
2018-10-31 15:05:24 +01:00
|
|
|
|
#import <SignalMessaging/ThreadUtil.h>
|
|
|
|
|
#import <SignalServiceKit/OWSBlockingManager.h>
|
|
|
|
|
#import <SignalServiceKit/OWSPrimaryStorage.h>
|
|
|
|
|
#import <SignalServiceKit/SSKEnvironment.h>
|
|
|
|
|
#import <SignalServiceKit/TSDatabaseView.h>
|
|
|
|
|
#import <SignalServiceKit/TSIncomingMessage.h>
|
|
|
|
|
#import <SignalServiceKit/TSOutgoingMessage.h>
|
|
|
|
|
#import <SignalServiceKit/TSThread.h>
|
|
|
|
|
#import <YapDatabase/YapDatabase.h>
|
|
|
|
|
#import <YapDatabase/YapDatabaseAutoView.h>
|
|
|
|
|
#import <YapDatabase/YapDatabaseViewChange.h>
|
2018-12-14 18:15:26 +01:00
|
|
|
|
#import <YapDatabase/YapDatabaseViewChangePrivate.h>
|
2018-10-31 15:05:24 +01:00
|
|
|
|
|
|
|
|
|
NS_ASSUME_NONNULL_BEGIN
|
|
|
|
|
|
2018-12-13 15:33:45 +01:00
|
|
|
|
@interface ConversationProfileState : NSObject
|
|
|
|
|
|
|
|
|
|
@property (nonatomic) BOOL hasLocalProfile;
|
|
|
|
|
@property (nonatomic) BOOL isThreadInProfileWhitelist;
|
|
|
|
|
@property (nonatomic) BOOL hasUnwhitelistedMember;
|
|
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
|
|
#pragma mark -
|
|
|
|
|
|
|
|
|
|
@implementation ConversationProfileState
|
|
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
|
|
#pragma mark -
|
|
|
|
|
|
2018-10-31 15:05:24 +01:00
|
|
|
|
@implementation ConversationUpdateItem
|
|
|
|
|
|
|
|
|
|
- (instancetype)initWithUpdateItemType:(ConversationUpdateItemType)updateItemType
|
|
|
|
|
oldIndex:(NSUInteger)oldIndex
|
|
|
|
|
newIndex:(NSUInteger)newIndex
|
|
|
|
|
viewItem:(nullable id<ConversationViewItem>)viewItem
|
|
|
|
|
{
|
|
|
|
|
self = [super init];
|
|
|
|
|
if (!self) {
|
|
|
|
|
return self;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_updateItemType = updateItemType;
|
|
|
|
|
_oldIndex = oldIndex;
|
|
|
|
|
_newIndex = newIndex;
|
|
|
|
|
_viewItem = viewItem;
|
|
|
|
|
|
|
|
|
|
return self;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
|
|
#pragma mark -
|
|
|
|
|
|
|
|
|
|
@implementation ConversationUpdate
|
|
|
|
|
|
|
|
|
|
- (instancetype)initWithConversationUpdateType:(ConversationUpdateType)conversationUpdateType
|
|
|
|
|
updateItems:(nullable NSArray<ConversationUpdateItem *> *)updateItems
|
|
|
|
|
shouldAnimateUpdates:(BOOL)shouldAnimateUpdates
|
|
|
|
|
{
|
|
|
|
|
self = [super init];
|
|
|
|
|
if (!self) {
|
|
|
|
|
return self;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_conversationUpdateType = conversationUpdateType;
|
|
|
|
|
_updateItems = updateItems;
|
|
|
|
|
_shouldAnimateUpdates = shouldAnimateUpdates;
|
|
|
|
|
|
|
|
|
|
return self;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
+ (ConversationUpdate *)minorUpdate
|
|
|
|
|
{
|
|
|
|
|
return [[ConversationUpdate alloc] initWithConversationUpdateType:ConversationUpdateType_Minor
|
|
|
|
|
updateItems:nil
|
|
|
|
|
shouldAnimateUpdates:NO];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
+ (ConversationUpdate *)reloadUpdate
|
|
|
|
|
{
|
|
|
|
|
return [[ConversationUpdate alloc] initWithConversationUpdateType:ConversationUpdateType_Reload
|
|
|
|
|
updateItems:nil
|
|
|
|
|
shouldAnimateUpdates:NO];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
+ (ConversationUpdate *)diffUpdateWithUpdateItems:(nullable NSArray<ConversationUpdateItem *> *)updateItems
|
|
|
|
|
shouldAnimateUpdates:(BOOL)shouldAnimateUpdates
|
|
|
|
|
{
|
|
|
|
|
return [[ConversationUpdate alloc] initWithConversationUpdateType:ConversationUpdateType_Diff
|
|
|
|
|
updateItems:updateItems
|
|
|
|
|
shouldAnimateUpdates:shouldAnimateUpdates];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
|
|
#pragma mark -
|
|
|
|
|
|
|
|
|
|
// Always load up to n messages when user arrives.
|
|
|
|
|
//
|
|
|
|
|
// The smaller this number is, the faster the conversation can display.
|
|
|
|
|
// To test, shrink you accessability font as much as possible, then count how many 1-line system info messages (our
|
|
|
|
|
// shortest cells) can fit on screen at a time on an iPhoneX
|
|
|
|
|
//
|
|
|
|
|
// PERF: we could do less messages on shorter (older, slower) devices
|
|
|
|
|
// PERF: we could cache the cell height, since some messages will be much taller.
|
2018-12-12 18:07:30 +01:00
|
|
|
|
static const int kYapDatabasePageSize = 18;
|
2018-10-31 15:05:24 +01:00
|
|
|
|
|
|
|
|
|
// Never show more than n messages in conversation view when user arrives.
|
|
|
|
|
static const int kConversationInitialMaxRangeSize = 300;
|
|
|
|
|
|
|
|
|
|
// Never show more than n messages in conversation view at a time.
|
|
|
|
|
static const int kYapDatabaseRangeMaxLength = 25000;
|
|
|
|
|
|
|
|
|
|
#pragma mark -
|
|
|
|
|
|
|
|
|
|
@interface ConversationViewModel ()
|
|
|
|
|
|
|
|
|
|
@property (nonatomic, weak) id<ConversationViewModelDelegate> delegate;
|
|
|
|
|
|
|
|
|
|
@property (nonatomic, readonly) TSThread *thread;
|
|
|
|
|
|
|
|
|
|
// The mapping must be updated in lockstep with the uiDatabaseConnection.
|
|
|
|
|
//
|
|
|
|
|
// * The first (required) step is to update uiDatabaseConnection using beginLongLivedReadTransaction.
|
2019-01-03 20:39:32 +01:00
|
|
|
|
// * The second (required) step is to update messageMapping. The desired length of the mapping
|
|
|
|
|
// can be modified at this time.
|
|
|
|
|
// * The third (optional) step is to update the view items using reloadViewItems.
|
2018-10-31 15:05:24 +01:00
|
|
|
|
// * The steps must be done in strict order.
|
|
|
|
|
// * If we do any of the steps, we must do all of the required steps.
|
2019-01-03 20:39:32 +01:00
|
|
|
|
// * We can't use messageMapping or viewItems after the first step until we've
|
2018-10-31 15:05:24 +01:00
|
|
|
|
// done the last step; i.e.. we can't do any layout, since that uses the view
|
|
|
|
|
// items which haven't been updated yet.
|
2019-01-03 20:39:32 +01:00
|
|
|
|
// * Afterward, we must prod the view controller to update layout & view state.
|
|
|
|
|
@property (nonatomic) ConversationMessageMapping *messageMapping;
|
2018-10-31 15:05:24 +01:00
|
|
|
|
|
2018-12-14 18:15:26 +01:00
|
|
|
|
@property (nonatomic) NSArray<id<ConversationViewItem>> *viewItems;
|
2018-10-31 15:05:24 +01:00
|
|
|
|
@property (nonatomic) NSMutableDictionary<NSString *, id<ConversationViewItem>> *viewItemCache;
|
|
|
|
|
|
|
|
|
|
@property (nonatomic, nullable) ThreadDynamicInteractions *dynamicInteractions;
|
|
|
|
|
@property (nonatomic) BOOL hasClearedUnreadMessagesIndicator;
|
|
|
|
|
@property (nonatomic, nullable) NSDate *collapseCutoffDate;
|
2018-11-01 21:19:03 +01:00
|
|
|
|
@property (nonatomic, nullable) NSString *typingIndicatorsSender;
|
2018-10-31 15:05:24 +01:00
|
|
|
|
|
2018-12-13 15:33:45 +01:00
|
|
|
|
@property (nonatomic, nullable) ConversationProfileState *conversationProfileState;
|
|
|
|
|
@property (nonatomic) BOOL hasTooManyOutgoingMessagesToBlockCached;
|
2018-12-14 18:15:26 +01:00
|
|
|
|
|
2018-12-13 22:30:50 +01:00
|
|
|
|
@property (nonatomic) NSArray<id<ConversationViewItem>> *persistedViewItems;
|
2018-12-14 18:15:26 +01:00
|
|
|
|
@property (nonatomic) NSArray<TSOutgoingMessage *> *unsavedOutgoingMessages;
|
2018-12-13 15:33:45 +01:00
|
|
|
|
|
2018-10-31 15:05:24 +01:00
|
|
|
|
@end
|
|
|
|
|
|
|
|
|
|
#pragma mark -
|
|
|
|
|
|
|
|
|
|
@implementation ConversationViewModel
|
|
|
|
|
|
|
|
|
|
- (instancetype)initWithThread:(TSThread *)thread
|
|
|
|
|
focusMessageIdOnOpen:(nullable NSString *)focusMessageIdOnOpen
|
|
|
|
|
delegate:(id<ConversationViewModelDelegate>)delegate
|
|
|
|
|
{
|
|
|
|
|
self = [super init];
|
|
|
|
|
if (!self) {
|
|
|
|
|
return self;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
OWSAssertDebug(thread);
|
|
|
|
|
OWSAssertDebug(delegate);
|
|
|
|
|
|
|
|
|
|
_thread = thread;
|
|
|
|
|
_delegate = delegate;
|
2018-12-14 18:15:26 +01:00
|
|
|
|
_persistedViewItems = @[];
|
|
|
|
|
_unsavedOutgoingMessages = @[];
|
2018-10-31 15:05:24 +01:00
|
|
|
|
self.focusMessageIdOnOpen = focusMessageIdOnOpen;
|
|
|
|
|
|
|
|
|
|
[self configure];
|
|
|
|
|
|
|
|
|
|
return self;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#pragma mark - Dependencies
|
|
|
|
|
|
|
|
|
|
- (OWSPrimaryStorage *)primaryStorage
|
|
|
|
|
{
|
|
|
|
|
OWSAssertDebug(SSKEnvironment.shared.primaryStorage);
|
|
|
|
|
|
|
|
|
|
return SSKEnvironment.shared.primaryStorage;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (YapDatabaseConnection *)uiDatabaseConnection
|
|
|
|
|
{
|
|
|
|
|
return self.primaryStorage.uiDatabaseConnection;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (YapDatabaseConnection *)editingDatabaseConnection
|
|
|
|
|
{
|
|
|
|
|
return self.primaryStorage.dbReadWriteConnection;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (OWSContactsManager *)contactsManager
|
|
|
|
|
{
|
|
|
|
|
return (OWSContactsManager *)SSKEnvironment.shared.contactsManager;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (OWSBlockingManager *)blockingManager
|
|
|
|
|
{
|
|
|
|
|
return OWSBlockingManager.sharedManager;
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-01 16:39:04 +01:00
|
|
|
|
- (id<OWSTypingIndicators>)typingIndicators
|
|
|
|
|
{
|
|
|
|
|
return SSKEnvironment.shared.typingIndicators;
|
|
|
|
|
}
|
|
|
|
|
|
2018-12-13 15:12:41 +01:00
|
|
|
|
- (TSAccountManager *)tsAccountManager
|
|
|
|
|
{
|
|
|
|
|
OWSAssertDebug(SSKEnvironment.shared.tsAccountManager);
|
|
|
|
|
|
|
|
|
|
return SSKEnvironment.shared.tsAccountManager;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (OWSProfileManager *)profileManager
|
|
|
|
|
{
|
|
|
|
|
return [OWSProfileManager sharedManager];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#pragma mark -
|
2018-10-31 15:05:24 +01:00
|
|
|
|
|
|
|
|
|
- (void)addNotificationListeners
|
|
|
|
|
{
|
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
|
|
|
selector:@selector(applicationDidEnterBackground:)
|
|
|
|
|
name:OWSApplicationDidEnterBackgroundNotification
|
|
|
|
|
object:nil];
|
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
|
|
|
selector:@selector(signalAccountsDidChange:)
|
|
|
|
|
name:OWSContactsManagerSignalAccountsDidChangeNotification
|
|
|
|
|
object:nil];
|
2018-11-01 16:39:04 +01:00
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
|
|
|
selector:@selector(typingIndicatorStateDidChange:)
|
|
|
|
|
name:[OWSTypingIndicatorsImpl typingIndicatorStateDidChange]
|
|
|
|
|
object:nil];
|
2018-12-13 15:12:41 +01:00
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
|
|
|
selector:@selector(profileWhitelistDidChange:)
|
|
|
|
|
name:kNSNotificationName_ProfileWhitelistDidChange
|
|
|
|
|
object:nil];
|
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
|
|
|
selector:@selector(blockListDidChange:)
|
|
|
|
|
name:kNSNotificationName_BlockListDidChange
|
|
|
|
|
object:nil];
|
2018-12-13 15:33:45 +01:00
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
|
|
|
selector:@selector(localProfileDidChange:)
|
|
|
|
|
name:kNSNotificationName_LocalProfileDidChange
|
|
|
|
|
object:nil];
|
2018-10-31 15:05:24 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)signalAccountsDidChange:(NSNotification *)notification
|
|
|
|
|
{
|
|
|
|
|
OWSAssertIsOnMainThread();
|
|
|
|
|
|
|
|
|
|
[self ensureDynamicInteractions];
|
|
|
|
|
}
|
|
|
|
|
|
2018-12-13 15:12:41 +01:00
|
|
|
|
- (void)profileWhitelistDidChange:(NSNotification *)notification
|
|
|
|
|
{
|
|
|
|
|
OWSAssertIsOnMainThread();
|
|
|
|
|
|
2018-12-13 15:33:45 +01:00
|
|
|
|
self.conversationProfileState = nil;
|
|
|
|
|
[self updateForTransientItems];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)localProfileDidChange:(NSNotification *)notification
|
|
|
|
|
{
|
|
|
|
|
OWSAssertIsOnMainThread();
|
|
|
|
|
|
|
|
|
|
self.conversationProfileState = nil;
|
|
|
|
|
[self updateForTransientItems];
|
2018-12-13 15:12:41 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)blockListDidChange:(id)notification
|
|
|
|
|
{
|
|
|
|
|
OWSAssertIsOnMainThread();
|
|
|
|
|
|
|
|
|
|
[self updateForTransientItems];
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-31 15:05:24 +01:00
|
|
|
|
- (void)configure
|
|
|
|
|
{
|
|
|
|
|
OWSLogInfo(@"");
|
|
|
|
|
|
|
|
|
|
// We need to update the "unread indicator" _before_ we determine the initial range
|
|
|
|
|
// size, since it depends on where the unread indicator is placed.
|
2018-11-01 21:19:03 +01:00
|
|
|
|
self.typingIndicatorsSender = [self.typingIndicators typingRecipientIdForThread:self.thread];
|
2019-01-03 20:39:32 +01:00
|
|
|
|
self.collapseCutoffDate = [NSDate new];
|
2018-10-31 15:05:24 +01:00
|
|
|
|
|
|
|
|
|
[self ensureDynamicInteractions];
|
|
|
|
|
[self.primaryStorage updateUIDatabaseConnectionToLatest];
|
|
|
|
|
|
2019-01-03 20:39:32 +01:00
|
|
|
|
[self createNewMessageMapping];
|
2018-10-31 15:05:24 +01:00
|
|
|
|
if (![self reloadViewItems]) {
|
|
|
|
|
OWSFailDebug(@"failed to reload view items in configureForThread.");
|
|
|
|
|
}
|
2018-11-02 20:38:59 +01:00
|
|
|
|
|
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
|
|
|
selector:@selector(uiDatabaseDidUpdateExternally:)
|
|
|
|
|
name:OWSUIDatabaseConnectionDidUpdateExternallyNotification
|
|
|
|
|
object:self.primaryStorage.dbNotificationObject];
|
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
|
|
|
selector:@selector(uiDatabaseWillUpdate:)
|
|
|
|
|
name:OWSUIDatabaseConnectionWillUpdateNotification
|
|
|
|
|
object:self.primaryStorage.dbNotificationObject];
|
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
|
|
|
selector:@selector(uiDatabaseDidUpdate:)
|
|
|
|
|
name:OWSUIDatabaseConnectionDidUpdateNotification
|
|
|
|
|
object:self.primaryStorage.dbNotificationObject];
|
2019-01-03 20:39:32 +01:00
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
|
|
|
selector:@selector(applicationWillEnterForeground:)
|
|
|
|
|
name:OWSApplicationWillEnterForegroundNotification
|
|
|
|
|
object:nil];
|
2018-10-31 15:05:24 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)viewDidLoad
|
|
|
|
|
{
|
|
|
|
|
[self addNotificationListeners];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)dealloc
|
|
|
|
|
{
|
|
|
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (BOOL)canLoadMoreItems
|
|
|
|
|
{
|
2019-01-03 20:39:32 +01:00
|
|
|
|
if (self.messageMapping.desiredLength >= kYapDatabaseRangeMaxLength) {
|
2018-10-31 15:05:24 +01:00
|
|
|
|
return NO;
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-03 20:39:32 +01:00
|
|
|
|
return self.messageMapping.canLoadMore;
|
2018-10-31 15:05:24 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)applicationDidEnterBackground:(NSNotification *)notification
|
|
|
|
|
{
|
|
|
|
|
if (self.hasClearedUnreadMessagesIndicator) {
|
|
|
|
|
self.hasClearedUnreadMessagesIndicator = NO;
|
|
|
|
|
[self.dynamicInteractions clearUnreadIndicatorState];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)viewDidResetContentAndLayout
|
|
|
|
|
{
|
|
|
|
|
self.collapseCutoffDate = [NSDate new];
|
|
|
|
|
if (![self reloadViewItems]) {
|
|
|
|
|
OWSFailDebug(@"failed to reload view items in resetContentAndLayout.");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)loadAnotherPageOfMessages
|
|
|
|
|
{
|
|
|
|
|
BOOL hasEarlierUnseenMessages = self.dynamicInteractions.unreadIndicator.hasMoreUnseenMessages;
|
|
|
|
|
|
2018-12-13 18:49:59 +01:00
|
|
|
|
// Now that we're using a "minimal" page size, we should
|
|
|
|
|
// increase the load window by 2 pages at a time.
|
|
|
|
|
[self loadNMoreMessages:kYapDatabasePageSize * 2];
|
2018-10-31 15:05:24 +01:00
|
|
|
|
|
|
|
|
|
// Don’t auto-scroll after “loading more messages” unless we have “more unseen messages”.
|
|
|
|
|
//
|
|
|
|
|
// Otherwise, tapping on "load more messages" autoscrolls you downward which is completely wrong.
|
|
|
|
|
if (hasEarlierUnseenMessages && !self.focusMessageIdOnOpen) {
|
|
|
|
|
// Ensure view items are updated before trying to scroll to the
|
|
|
|
|
// unread indicator.
|
|
|
|
|
//
|
2019-01-03 20:39:32 +01:00
|
|
|
|
// loadNMoreMessages calls resetMapping which calls ensureDynamicInteractions,
|
2018-10-31 15:05:24 +01:00
|
|
|
|
// which may move the unread indicator, and for scrollToUnreadIndicatorAnimated
|
|
|
|
|
// to work properly, the view items need to be updated to reflect that change.
|
|
|
|
|
[self.primaryStorage updateUIDatabaseConnectionToLatest];
|
|
|
|
|
|
|
|
|
|
[self.delegate conversationViewModelDidLoadPrevPage];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)loadNMoreMessages:(NSUInteger)numberOfMessagesToLoad
|
|
|
|
|
{
|
|
|
|
|
[self.delegate conversationViewModelWillLoadMoreItems];
|
|
|
|
|
|
2019-01-03 20:39:32 +01:00
|
|
|
|
[self resetMappingWithAdditionalLength:numberOfMessagesToLoad];
|
2018-10-31 15:05:24 +01:00
|
|
|
|
|
|
|
|
|
[self.delegate conversationViewModelDidLoadMoreItems];
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-03 20:39:32 +01:00
|
|
|
|
- (NSUInteger)initialMessageMappingLength
|
2018-10-31 15:05:24 +01:00
|
|
|
|
{
|
2019-01-03 20:39:32 +01:00
|
|
|
|
NSUInteger rangeLength = kYapDatabasePageSize;
|
|
|
|
|
|
|
|
|
|
// If this is the first time we're configuring the range length,
|
|
|
|
|
// try to take into account the position of the unread indicator
|
|
|
|
|
// and the "focus message".
|
|
|
|
|
OWSAssertDebug(self.dynamicInteractions);
|
|
|
|
|
|
|
|
|
|
if (self.focusMessageIdOnOpen) {
|
|
|
|
|
OWSAssertDebug(self.dynamicInteractions.focusMessagePosition);
|
|
|
|
|
if (self.dynamicInteractions.focusMessagePosition) {
|
|
|
|
|
OWSLogVerbose(@"ensuring load of focus message: %@", self.dynamicInteractions.focusMessagePosition);
|
|
|
|
|
rangeLength = MAX(rangeLength, 1 + self.dynamicInteractions.focusMessagePosition.unsignedIntegerValue);
|
2018-10-31 15:05:24 +01:00
|
|
|
|
}
|
2019-01-03 20:39:32 +01:00
|
|
|
|
}
|
2018-10-31 15:05:24 +01:00
|
|
|
|
|
2019-01-03 20:39:32 +01:00
|
|
|
|
if (self.dynamicInteractions.unreadIndicator) {
|
|
|
|
|
NSUInteger unreadIndicatorPosition
|
|
|
|
|
= (NSUInteger)self.dynamicInteractions.unreadIndicator.unreadIndicatorPosition;
|
2018-10-31 15:05:24 +01:00
|
|
|
|
|
2019-01-03 20:39:32 +01:00
|
|
|
|
// If there is an unread indicator, increase the initial load window
|
|
|
|
|
// to include it.
|
|
|
|
|
OWSAssertDebug(unreadIndicatorPosition > 0);
|
|
|
|
|
OWSAssertDebug(unreadIndicatorPosition <= kYapDatabaseRangeMaxLength);
|
2018-10-31 15:05:24 +01:00
|
|
|
|
|
2019-01-03 20:39:32 +01:00
|
|
|
|
// We'd like to include at least N seen messages,
|
|
|
|
|
// to give the user the context of where they left off the conversation.
|
|
|
|
|
const NSUInteger kPreferredSeenMessageCount = 1;
|
|
|
|
|
rangeLength = MAX(rangeLength, unreadIndicatorPosition + kPreferredSeenMessageCount);
|
2018-10-31 15:05:24 +01:00
|
|
|
|
}
|
|
|
|
|
|
2019-01-03 20:39:32 +01:00
|
|
|
|
return rangeLength;
|
|
|
|
|
}
|
2018-10-31 15:05:24 +01:00
|
|
|
|
|
2019-01-03 20:39:32 +01:00
|
|
|
|
- (void)updateMessageMappingWithAdditionalLength:(NSUInteger)additionalLength
|
|
|
|
|
{
|
2018-10-31 15:05:24 +01:00
|
|
|
|
// Range size should monotonically increase.
|
2019-01-03 20:39:32 +01:00
|
|
|
|
NSUInteger rangeLength = self.messageMapping.desiredLength + additionalLength;
|
|
|
|
|
|
|
|
|
|
// Always try to load at least a single page of messages.
|
|
|
|
|
rangeLength = MAX(rangeLength, kYapDatabasePageSize);
|
2018-10-31 15:05:24 +01:00
|
|
|
|
|
|
|
|
|
// Enforce max range size.
|
|
|
|
|
rangeLength = MIN(rangeLength, kYapDatabaseRangeMaxLength);
|
|
|
|
|
|
2019-01-03 20:39:32 +01:00
|
|
|
|
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
|
|
|
|
[self.messageMapping updateWithDesiredLength:rangeLength transaction:transaction];
|
|
|
|
|
}];
|
2018-10-31 15:05:24 +01:00
|
|
|
|
|
|
|
|
|
[self.delegate conversationViewModelRangeDidChange];
|
|
|
|
|
self.collapseCutoffDate = [NSDate new];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)ensureDynamicInteractions
|
|
|
|
|
{
|
|
|
|
|
OWSAssertIsOnMainThread();
|
|
|
|
|
|
2019-01-03 20:39:32 +01:00
|
|
|
|
const int currentMaxRangeSize = (int)self.messageMapping.desiredLength;
|
2018-10-31 15:05:24 +01:00
|
|
|
|
const int maxRangeSize = MAX(kConversationInitialMaxRangeSize, currentMaxRangeSize);
|
|
|
|
|
|
|
|
|
|
self.dynamicInteractions = [ThreadUtil ensureDynamicInteractionsForThread:self.thread
|
|
|
|
|
contactsManager:self.contactsManager
|
|
|
|
|
blockingManager:self.blockingManager
|
|
|
|
|
dbConnection:self.editingDatabaseConnection
|
|
|
|
|
hideUnreadMessagesIndicator:self.hasClearedUnreadMessagesIndicator
|
|
|
|
|
lastUnreadIndicator:self.dynamicInteractions.unreadIndicator
|
|
|
|
|
focusMessageId:self.focusMessageIdOnOpen
|
|
|
|
|
maxRangeSize:maxRangeSize];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (nullable id<ConversationViewItem>)viewItemForUnreadMessagesIndicator
|
|
|
|
|
{
|
2018-12-14 18:15:26 +01:00
|
|
|
|
for (id<ConversationViewItem> viewItem in self.viewItems) {
|
2018-10-31 15:05:24 +01:00
|
|
|
|
if (viewItem.unreadIndicator) {
|
|
|
|
|
return viewItem;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)clearUnreadMessagesIndicator
|
|
|
|
|
{
|
|
|
|
|
OWSAssertIsOnMainThread();
|
|
|
|
|
|
|
|
|
|
// TODO: Remove by making unread indicator a view model concern.
|
|
|
|
|
id<ConversationViewItem> _Nullable oldIndicatorItem = [self viewItemForUnreadMessagesIndicator];
|
|
|
|
|
if (oldIndicatorItem) {
|
|
|
|
|
// TODO ideally this would be happening within the *same* transaction that caused the unreadMessageIndicator
|
|
|
|
|
// to be cleared.
|
|
|
|
|
[self.editingDatabaseConnection
|
|
|
|
|
asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
|
|
|
|
|
[oldIndicatorItem.interaction touchWithTransaction:transaction];
|
|
|
|
|
}];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (self.hasClearedUnreadMessagesIndicator) {
|
|
|
|
|
// ensureDynamicInteractionsForThread is somewhat expensive
|
|
|
|
|
// so we don't want to call it unnecessarily.
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Once we've cleared the unread messages indicator,
|
|
|
|
|
// make sure we don't show it again.
|
|
|
|
|
self.hasClearedUnreadMessagesIndicator = YES;
|
|
|
|
|
|
|
|
|
|
if (self.dynamicInteractions.unreadIndicator) {
|
|
|
|
|
// If we've just cleared the "unread messages" indicator,
|
|
|
|
|
// update the dynamic interactions.
|
|
|
|
|
[self ensureDynamicInteractions];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#pragma mark - Storage access
|
|
|
|
|
|
|
|
|
|
- (void)uiDatabaseDidUpdateExternally:(NSNotification *)notification
|
|
|
|
|
{
|
|
|
|
|
OWSAssertIsOnMainThread();
|
|
|
|
|
|
|
|
|
|
OWSLogVerbose(@"");
|
|
|
|
|
|
2018-11-02 20:38:59 +01:00
|
|
|
|
if (!self.delegate.isObservingVMUpdates) {
|
|
|
|
|
return;
|
2018-10-31 15:05:24 +01:00
|
|
|
|
}
|
2018-11-02 20:38:59 +01:00
|
|
|
|
|
2019-01-03 20:39:32 +01:00
|
|
|
|
// External database modifications (e.g. changes from another process such as the SAE)
|
|
|
|
|
// are "flushed" using touchDbAsync when the app re-enters the foreground.
|
2018-10-31 15:05:24 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)uiDatabaseWillUpdate:(NSNotification *)notification
|
|
|
|
|
{
|
2018-11-02 20:38:59 +01:00
|
|
|
|
if (!self.delegate.isObservingVMUpdates) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2018-10-31 15:05:24 +01:00
|
|
|
|
[self.delegate conversationViewModelWillUpdate];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)uiDatabaseDidUpdate:(NSNotification *)notification
|
|
|
|
|
{
|
|
|
|
|
OWSAssertIsOnMainThread();
|
|
|
|
|
|
|
|
|
|
OWSLogVerbose(@"");
|
|
|
|
|
|
2019-01-03 20:39:32 +01:00
|
|
|
|
NSArray<NSNotification *> *notifications = notification.userInfo[OWSUIDatabaseConnectionNotificationsKey];
|
2018-10-31 15:05:24 +01:00
|
|
|
|
OWSAssertDebug([notifications isKindOfClass:[NSArray class]]);
|
|
|
|
|
|
2018-12-13 16:14:15 +01:00
|
|
|
|
YapDatabaseAutoViewConnection *messageDatabaseView =
|
|
|
|
|
[self.uiDatabaseConnection ext:TSMessageDatabaseViewExtensionName];
|
|
|
|
|
OWSAssertDebug([messageDatabaseView isKindOfClass:[YapDatabaseAutoViewConnection class]]);
|
|
|
|
|
if (![messageDatabaseView hasChangesForGroup:self.thread.uniqueId inNotifications:notifications]) {
|
2018-10-31 15:05:24 +01:00
|
|
|
|
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
2019-01-03 20:39:32 +01:00
|
|
|
|
[self.messageMapping updateWithTransaction:transaction];
|
2018-10-31 15:05:24 +01:00
|
|
|
|
}];
|
|
|
|
|
|
|
|
|
|
[self.delegate conversationViewModelDidUpdate:ConversationUpdate.minorUpdate];
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-03 20:39:32 +01:00
|
|
|
|
__block ConversationMessageMappingDiff *_Nullable diff = nil;
|
|
|
|
|
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
|
|
|
|
diff = [self.messageMapping updateAndCalculateDiffWithTransaction:transaction notifications:notifications];
|
|
|
|
|
}];
|
|
|
|
|
if (!diff) {
|
|
|
|
|
OWSFailDebug(@"Could not determine diff");
|
|
|
|
|
// resetMapping will call delegate.conversationViewModelDidUpdate.
|
|
|
|
|
[self resetMapping];
|
|
|
|
|
[self.delegate conversationViewModelDidReset];
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (diff.addedItemIds.count < 1 && diff.removedItemIds.count < 1 && diff.updatedItemIds.count < 1) {
|
|
|
|
|
OWSFailDebug(@"Unexpectedly empty diff.");
|
2018-10-31 15:05:24 +01:00
|
|
|
|
}
|
|
|
|
|
|
2019-01-03 20:39:32 +01:00
|
|
|
|
NSMutableSet<NSString *> *diffAddedItemIds = [diff.addedItemIds mutableCopy];
|
|
|
|
|
NSMutableSet<NSString *> *diffRemovedItemIds = [diff.removedItemIds mutableCopy];
|
|
|
|
|
NSMutableSet<NSString *> *diffUpdatedItemIds = [diff.updatedItemIds mutableCopy];
|
2018-12-14 18:15:26 +01:00
|
|
|
|
for (TSOutgoingMessage *unsavedOutgoingMessage in self.unsavedOutgoingMessages) {
|
2018-12-17 18:34:12 +01:00
|
|
|
|
// unsavedOutgoingMessages should only exist for a short period (usually 30-50ms) before
|
|
|
|
|
// they are saved and moved into the `persistedViewItems`
|
2018-12-17 20:54:44 +01:00
|
|
|
|
OWSAssertDebug(unsavedOutgoingMessage.timestamp >= ([NSDate ows_millisecondTimeStamp] - 1 * kSecondInMs));
|
2018-12-14 18:15:26 +01:00
|
|
|
|
|
2019-01-03 20:39:32 +01:00
|
|
|
|
BOOL isFound = ([diff.addedItemIds containsObject:unsavedOutgoingMessage.uniqueId] ||
|
|
|
|
|
[diff.removedItemIds containsObject:unsavedOutgoingMessage.uniqueId] ||
|
|
|
|
|
[diff.updatedItemIds containsObject:unsavedOutgoingMessage.uniqueId]);
|
|
|
|
|
if (isFound) {
|
|
|
|
|
// Convert the "insert" to an "update".
|
|
|
|
|
if ([diffAddedItemIds containsObject:unsavedOutgoingMessage.uniqueId]) {
|
|
|
|
|
OWSLogVerbose(@"Converting insert to update: %@", unsavedOutgoingMessage.uniqueId);
|
|
|
|
|
[diffAddedItemIds removeObject:unsavedOutgoingMessage.uniqueId];
|
|
|
|
|
[diffUpdatedItemIds removeObject:unsavedOutgoingMessage.uniqueId];
|
|
|
|
|
}
|
2018-12-14 18:15:26 +01:00
|
|
|
|
|
|
|
|
|
// Remove the unsavedOutgoingViewItem since it now exists as a persistedViewItem
|
|
|
|
|
NSMutableArray<TSOutgoingMessage *> *unsavedOutgoingMessages = [self.unsavedOutgoingMessages mutableCopy];
|
|
|
|
|
[unsavedOutgoingMessages removeObject:unsavedOutgoingMessage];
|
|
|
|
|
self.unsavedOutgoingMessages = [unsavedOutgoingMessages copy];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-31 15:05:24 +01:00
|
|
|
|
NSMutableArray<NSString *> *oldItemIdList = [NSMutableArray new];
|
2018-12-14 18:15:26 +01:00
|
|
|
|
for (id<ConversationViewItem> viewItem in self.viewItems) {
|
2018-10-31 15:05:24 +01:00
|
|
|
|
[oldItemIdList addObject:viewItem.itemId];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We need to reload any modified interactions _before_ we call
|
|
|
|
|
// reloadViewItems.
|
|
|
|
|
BOOL hasMalformedRowChange = NO;
|
|
|
|
|
NSMutableSet<NSString *> *updatedItemSet = [NSMutableSet new];
|
2019-01-03 20:39:32 +01:00
|
|
|
|
for (NSString *uniqueId in diffUpdatedItemIds) {
|
|
|
|
|
id<ConversationViewItem> _Nullable viewItem = self.viewItemCache[uniqueId];
|
|
|
|
|
if (viewItem) {
|
|
|
|
|
[self reloadInteractionForViewItem:viewItem];
|
|
|
|
|
[updatedItemSet addObject:viewItem.itemId];
|
|
|
|
|
} else {
|
|
|
|
|
OWSFailDebug(@"Update is missing view item");
|
|
|
|
|
hasMalformedRowChange = YES;
|
2018-10-31 15:05:24 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
2019-01-03 20:39:32 +01:00
|
|
|
|
for (NSString *uniqueId in diffRemovedItemIds) {
|
|
|
|
|
[self.viewItemCache removeObjectForKey:uniqueId];
|
|
|
|
|
}
|
2018-10-31 15:05:24 +01:00
|
|
|
|
|
|
|
|
|
if (hasMalformedRowChange) {
|
|
|
|
|
// These errors seems to be very rare; they can only be reproduced
|
|
|
|
|
// using the more extreme actions in the debug UI.
|
|
|
|
|
OWSFailDebug(@"hasMalformedRowChange");
|
2019-01-03 20:39:32 +01:00
|
|
|
|
// resetMapping will call delegate.conversationViewModelDidUpdate.
|
|
|
|
|
[self resetMapping];
|
|
|
|
|
[self.delegate conversationViewModelDidReset];
|
2018-10-31 15:05:24 +01:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (![self reloadViewItems]) {
|
|
|
|
|
// These errors are rare.
|
2019-01-03 20:39:32 +01:00
|
|
|
|
OWSFailDebug(@"could not reload view items; hard resetting message mapping.");
|
|
|
|
|
// resetMapping will call delegate.conversationViewModelDidUpdate.
|
|
|
|
|
[self resetMapping];
|
|
|
|
|
[self.delegate conversationViewModelDidReset];
|
2018-10-31 15:05:24 +01:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2018-12-14 18:15:26 +01:00
|
|
|
|
OWSLogVerbose(@"self.viewItems.count: %zd -> %zd", oldItemIdList.count, self.viewItems.count);
|
2018-10-31 15:05:24 +01:00
|
|
|
|
|
2018-12-10 17:59:00 +01:00
|
|
|
|
[self updateViewWithOldItemIdList:oldItemIdList updatedItemSet:updatedItemSet];
|
2018-10-31 15:05:24 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-01 20:00:01 +01:00
|
|
|
|
// A simpler version of the update logic we use when
|
2018-11-01 16:39:04 +01:00
|
|
|
|
// only transient items have changed.
|
|
|
|
|
- (void)updateForTransientItems
|
|
|
|
|
{
|
|
|
|
|
OWSAssertIsOnMainThread();
|
|
|
|
|
|
|
|
|
|
OWSLogVerbose(@"");
|
|
|
|
|
|
|
|
|
|
NSMutableArray<NSString *> *oldItemIdList = [NSMutableArray new];
|
2018-12-14 18:15:26 +01:00
|
|
|
|
for (id<ConversationViewItem> viewItem in self.viewItems) {
|
2018-11-01 16:39:04 +01:00
|
|
|
|
[oldItemIdList addObject:viewItem.itemId];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (![self reloadViewItems]) {
|
|
|
|
|
// These errors are rare.
|
2019-01-03 20:39:32 +01:00
|
|
|
|
OWSFailDebug(@"could not reload view items; hard resetting message mapping.");
|
|
|
|
|
// resetMapping will call delegate.conversationViewModelDidUpdate.
|
|
|
|
|
[self resetMapping];
|
2018-11-01 16:39:04 +01:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2018-12-14 18:15:26 +01:00
|
|
|
|
OWSLogVerbose(@"self.viewItems.count: %zd -> %zd", oldItemIdList.count, self.viewItems.count);
|
2018-11-01 16:39:04 +01:00
|
|
|
|
|
2018-12-10 17:59:00 +01:00
|
|
|
|
[self updateViewWithOldItemIdList:oldItemIdList updatedItemSet:[NSSet set]];
|
2018-11-01 16:39:04 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-01 19:51:40 +01:00
|
|
|
|
- (void)updateViewWithOldItemIdList:(NSArray<NSString *> *)oldItemIdList
|
2018-12-10 17:59:00 +01:00
|
|
|
|
updatedItemSet:(NSSet<NSString *> *)updatedItemSetParam {
|
2018-10-31 15:05:24 +01:00
|
|
|
|
OWSAssertDebug(oldItemIdList);
|
2018-12-10 17:59:00 +01:00
|
|
|
|
OWSAssertDebug(updatedItemSetParam);
|
2018-10-31 15:05:24 +01:00
|
|
|
|
|
2018-11-02 20:38:59 +01:00
|
|
|
|
if (!self.delegate.isObservingVMUpdates) {
|
2018-11-05 22:44:05 +01:00
|
|
|
|
OWSLogVerbose(@"Skipping VM update.");
|
2018-11-02 20:38:59 +01:00
|
|
|
|
// We fire this event, but it will be ignored.
|
|
|
|
|
[self.delegate conversationViewModelDidUpdate:ConversationUpdate.minorUpdate];
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-31 15:05:24 +01:00
|
|
|
|
if (oldItemIdList.count != [NSSet setWithArray:oldItemIdList].count) {
|
|
|
|
|
OWSFailDebug(@"Old view item list has duplicates.");
|
|
|
|
|
[self.delegate conversationViewModelDidUpdate:ConversationUpdate.reloadUpdate];
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
NSMutableArray<NSString *> *newItemIdList = [NSMutableArray new];
|
|
|
|
|
NSMutableDictionary<NSString *, id<ConversationViewItem>> *newViewItemMap = [NSMutableDictionary new];
|
2018-12-14 18:15:26 +01:00
|
|
|
|
for (id<ConversationViewItem> viewItem in self.viewItems) {
|
2018-10-31 15:05:24 +01:00
|
|
|
|
[newItemIdList addObject:viewItem.itemId];
|
|
|
|
|
newViewItemMap[viewItem.itemId] = viewItem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (newItemIdList.count != [NSSet setWithArray:newItemIdList].count) {
|
|
|
|
|
OWSFailDebug(@"New view item list has duplicates.");
|
|
|
|
|
[self.delegate conversationViewModelDidUpdate:ConversationUpdate.reloadUpdate];
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
NSSet<NSString *> *oldItemIdSet = [NSSet setWithArray:oldItemIdList];
|
|
|
|
|
NSSet<NSString *> *newItemIdSet = [NSSet setWithArray:newItemIdList];
|
|
|
|
|
|
|
|
|
|
// We use sets and dictionaries here to ensure perf.
|
2018-11-05 15:01:56 +01:00
|
|
|
|
// We use NSMutableOrderedSet to preserve item ordering.
|
2018-11-05 14:58:22 +01:00
|
|
|
|
NSMutableOrderedSet<NSString *> *deletedItemIdSet = [NSMutableOrderedSet orderedSetWithArray:oldItemIdList];
|
2018-10-31 15:05:24 +01:00
|
|
|
|
[deletedItemIdSet minusSet:newItemIdSet];
|
2018-11-05 14:58:22 +01:00
|
|
|
|
NSMutableOrderedSet<NSString *> *insertedItemIdSet = [NSMutableOrderedSet orderedSetWithArray:newItemIdList];
|
2018-10-31 15:05:24 +01:00
|
|
|
|
[insertedItemIdSet minusSet:oldItemIdSet];
|
2018-11-05 14:58:22 +01:00
|
|
|
|
NSArray<NSString *> *deletedItemIdList = [deletedItemIdSet.array copy];
|
|
|
|
|
NSArray<NSString *> *insertedItemIdList = [insertedItemIdSet.array copy];
|
2018-10-31 15:05:24 +01:00
|
|
|
|
|
|
|
|
|
// Try to generate a series of "update items" that safely transform
|
|
|
|
|
// the "old item list" into the "new item list".
|
|
|
|
|
NSMutableArray<ConversationUpdateItem *> *updateItems = [NSMutableArray new];
|
|
|
|
|
NSMutableArray<NSString *> *transformedItemList = [oldItemIdList mutableCopy];
|
|
|
|
|
|
|
|
|
|
// 1. Deletes - Always perform deletes before inserts and updates.
|
|
|
|
|
//
|
|
|
|
|
// NOTE: We use reverseObjectEnumerator to ensure that items
|
|
|
|
|
// are deleted in reverse order, to avoid confusion around
|
|
|
|
|
// each deletion affecting the indices of subsequent deletions.
|
|
|
|
|
for (NSString *itemId in deletedItemIdList.reverseObjectEnumerator) {
|
|
|
|
|
OWSAssertDebug([oldItemIdSet containsObject:itemId]);
|
|
|
|
|
OWSAssertDebug(![newItemIdSet containsObject:itemId]);
|
|
|
|
|
|
|
|
|
|
NSUInteger oldIndex = [oldItemIdList indexOfObject:itemId];
|
|
|
|
|
if (oldIndex == NSNotFound) {
|
|
|
|
|
OWSFailDebug(@"Can't find index of deleted view item.");
|
|
|
|
|
return [self.delegate conversationViewModelDidUpdate:ConversationUpdate.reloadUpdate];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[updateItems addObject:[[ConversationUpdateItem alloc] initWithUpdateItemType:ConversationUpdateItemType_Delete
|
|
|
|
|
oldIndex:oldIndex
|
|
|
|
|
newIndex:NSNotFound
|
|
|
|
|
viewItem:nil]];
|
|
|
|
|
[transformedItemList removeObject:itemId];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. Inserts - Always perform inserts before updates.
|
|
|
|
|
//
|
|
|
|
|
// NOTE: We DO NOT use reverseObjectEnumerator.
|
|
|
|
|
for (NSString *itemId in insertedItemIdList) {
|
|
|
|
|
OWSAssertDebug(![oldItemIdSet containsObject:itemId]);
|
|
|
|
|
OWSAssertDebug([newItemIdSet containsObject:itemId]);
|
|
|
|
|
|
|
|
|
|
NSUInteger newIndex = [newItemIdList indexOfObject:itemId];
|
|
|
|
|
if (newIndex == NSNotFound) {
|
|
|
|
|
OWSFailDebug(@"Can't find index of inserted view item.");
|
|
|
|
|
return [self.delegate conversationViewModelDidUpdate:ConversationUpdate.reloadUpdate];
|
|
|
|
|
}
|
|
|
|
|
id<ConversationViewItem> _Nullable viewItem = newViewItemMap[itemId];
|
|
|
|
|
if (!viewItem) {
|
|
|
|
|
OWSFailDebug(@"Can't find inserted view item.");
|
|
|
|
|
return [self.delegate conversationViewModelDidUpdate:ConversationUpdate.reloadUpdate];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[updateItems addObject:[[ConversationUpdateItem alloc] initWithUpdateItemType:ConversationUpdateItemType_Insert
|
|
|
|
|
oldIndex:NSNotFound
|
|
|
|
|
newIndex:newIndex
|
|
|
|
|
viewItem:viewItem]];
|
|
|
|
|
[transformedItemList insertObject:itemId atIndex:newIndex];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (![newItemIdList isEqualToArray:transformedItemList]) {
|
|
|
|
|
// We should be able to represent all transformations as a series of
|
|
|
|
|
// inserts, updates and deletes - moves should not be necessary.
|
|
|
|
|
//
|
|
|
|
|
// TODO: The unread indicator might end up being an exception.
|
|
|
|
|
OWSLogWarn(@"New and updated view item lists don't match.");
|
|
|
|
|
return [self.delegate conversationViewModelDidUpdate:ConversationUpdate.reloadUpdate];
|
|
|
|
|
}
|
|
|
|
|
|
2018-12-10 18:02:18 +01:00
|
|
|
|
// In addition to "update" items from the database change notification,
|
|
|
|
|
// we may need to update other items. One example is neighbors of modified
|
|
|
|
|
// cells. Another is cells whose appearance has changed due to the passage
|
|
|
|
|
// of time. We detect "dirty" items by whether or not they have cached layout
|
|
|
|
|
// state, since that is cleared whenever we change the properties of the
|
|
|
|
|
// item that affect its appearance.
|
|
|
|
|
//
|
|
|
|
|
// This replaces the setCellDrawingDependencyOffsets/
|
|
|
|
|
// YapDatabaseViewChangedDependency logic offered by YDB mappings,
|
|
|
|
|
// which only reflects changes in the data store, not at the view
|
|
|
|
|
// level.
|
2018-12-10 17:59:00 +01:00
|
|
|
|
NSMutableSet<NSString *> *updatedItemSet = [updatedItemSetParam mutableCopy];
|
|
|
|
|
NSMutableSet<NSString *> *updatedNeighborItemSet = [NSMutableSet new];
|
|
|
|
|
for (NSString *itemId in newItemIdSet) {
|
|
|
|
|
if (![oldItemIdSet containsObject:itemId]) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if ([insertedItemIdSet containsObject:itemId] || [updatedItemSet containsObject:itemId]) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
OWSAssertDebug(![deletedItemIdSet containsObject:itemId]);
|
|
|
|
|
|
|
|
|
|
NSUInteger newIndex = [newItemIdList indexOfObject:itemId];
|
|
|
|
|
if (newIndex == NSNotFound) {
|
|
|
|
|
OWSFailDebug(@"Can't find index of holdover view item.");
|
|
|
|
|
return [self.delegate conversationViewModelDidUpdate:ConversationUpdate.reloadUpdate];
|
|
|
|
|
}
|
|
|
|
|
id<ConversationViewItem> _Nullable viewItem = newViewItemMap[itemId];
|
|
|
|
|
if (!viewItem) {
|
|
|
|
|
OWSFailDebug(@"Can't find holdover view item.");
|
|
|
|
|
return [self.delegate conversationViewModelDidUpdate:ConversationUpdate.reloadUpdate];
|
|
|
|
|
}
|
|
|
|
|
if (!viewItem.hasCachedLayoutState) {
|
|
|
|
|
[updatedItemSet addObject:itemId];
|
|
|
|
|
[updatedNeighborItemSet addObject:itemId];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-31 15:05:24 +01:00
|
|
|
|
// 3. Updates.
|
|
|
|
|
//
|
|
|
|
|
// NOTE: Order doesn't matter.
|
|
|
|
|
for (NSString *itemId in updatedItemSet) {
|
|
|
|
|
if (![newItemIdList containsObject:itemId]) {
|
|
|
|
|
OWSFailDebug(@"Updated view item not in new view item list.");
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if ([insertedItemIdList containsObject:itemId]) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
NSUInteger oldIndex = [oldItemIdList indexOfObject:itemId];
|
|
|
|
|
if (oldIndex == NSNotFound) {
|
|
|
|
|
OWSFailDebug(@"Can't find old index of updated view item.");
|
|
|
|
|
return [self.delegate conversationViewModelDidUpdate:ConversationUpdate.reloadUpdate];
|
|
|
|
|
}
|
|
|
|
|
NSUInteger newIndex = [newItemIdList indexOfObject:itemId];
|
|
|
|
|
if (newIndex == NSNotFound) {
|
|
|
|
|
OWSFailDebug(@"Can't find new index of updated view item.");
|
|
|
|
|
return [self.delegate conversationViewModelDidUpdate:ConversationUpdate.reloadUpdate];
|
|
|
|
|
}
|
|
|
|
|
id<ConversationViewItem> _Nullable viewItem = newViewItemMap[itemId];
|
|
|
|
|
if (!viewItem) {
|
|
|
|
|
OWSFailDebug(@"Can't find inserted view item.");
|
|
|
|
|
return [self.delegate conversationViewModelDidUpdate:ConversationUpdate.reloadUpdate];
|
|
|
|
|
}
|
|
|
|
|
[updateItems addObject:[[ConversationUpdateItem alloc] initWithUpdateItemType:ConversationUpdateItemType_Update
|
|
|
|
|
oldIndex:oldIndex
|
|
|
|
|
newIndex:newIndex
|
|
|
|
|
viewItem:viewItem]];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
BOOL shouldAnimateUpdates = [self shouldAnimateUpdateItems:updateItems
|
2018-11-01 19:51:40 +01:00
|
|
|
|
oldViewItemCount:oldItemIdList.count
|
2018-10-31 15:05:24 +01:00
|
|
|
|
updatedNeighborItemSet:updatedNeighborItemSet];
|
|
|
|
|
|
|
|
|
|
return [self.delegate
|
|
|
|
|
conversationViewModelDidUpdate:[ConversationUpdate diffUpdateWithUpdateItems:updateItems
|
|
|
|
|
shouldAnimateUpdates:shouldAnimateUpdates]];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (BOOL)shouldAnimateUpdateItems:(NSArray<ConversationUpdateItem *> *)updateItems
|
|
|
|
|
oldViewItemCount:(NSUInteger)oldViewItemCount
|
|
|
|
|
updatedNeighborItemSet:(nullable NSMutableSet<NSString *> *)updatedNeighborItemSet
|
|
|
|
|
{
|
|
|
|
|
OWSAssertDebug(updateItems);
|
|
|
|
|
|
|
|
|
|
// If user sends a new outgoing message, don't animate the change.
|
|
|
|
|
BOOL isOnlyModifyingLastMessage = YES;
|
|
|
|
|
for (ConversationUpdateItem *updateItem in updateItems) {
|
|
|
|
|
switch (updateItem.updateItemType) {
|
|
|
|
|
case ConversationUpdateItemType_Delete:
|
|
|
|
|
isOnlyModifyingLastMessage = NO;
|
|
|
|
|
break;
|
|
|
|
|
case ConversationUpdateItemType_Insert: {
|
|
|
|
|
id<ConversationViewItem> viewItem = updateItem.viewItem;
|
|
|
|
|
OWSAssertDebug(viewItem);
|
2019-01-03 20:39:32 +01:00
|
|
|
|
switch (viewItem.interaction.interactionType) {
|
|
|
|
|
case OWSInteractionType_IncomingMessage:
|
|
|
|
|
case OWSInteractionType_OutgoingMessage:
|
|
|
|
|
case OWSInteractionType_TypingIndicator:
|
|
|
|
|
if (updateItem.newIndex < oldViewItemCount) {
|
|
|
|
|
isOnlyModifyingLastMessage = NO;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
isOnlyModifyingLastMessage = NO;
|
|
|
|
|
break;
|
2018-10-31 15:05:24 +01:00
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case ConversationUpdateItemType_Update: {
|
|
|
|
|
id<ConversationViewItem> viewItem = updateItem.viewItem;
|
|
|
|
|
if ([updatedNeighborItemSet containsObject:viewItem.itemId]) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
OWSAssertDebug(viewItem);
|
2019-01-03 20:39:32 +01:00
|
|
|
|
switch (viewItem.interaction.interactionType) {
|
|
|
|
|
case OWSInteractionType_IncomingMessage:
|
|
|
|
|
case OWSInteractionType_OutgoingMessage:
|
|
|
|
|
case OWSInteractionType_TypingIndicator:
|
|
|
|
|
if (updateItem.newIndex < updateItems.count - 1) {
|
|
|
|
|
isOnlyModifyingLastMessage = NO;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
isOnlyModifyingLastMessage = NO;
|
|
|
|
|
break;
|
2018-10-31 15:05:24 +01:00
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
BOOL shouldAnimateRowUpdates = !isOnlyModifyingLastMessage;
|
|
|
|
|
return shouldAnimateRowUpdates;
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-03 20:39:32 +01:00
|
|
|
|
- (void)createNewMessageMapping
|
2018-10-31 15:05:24 +01:00
|
|
|
|
{
|
2019-01-03 20:39:32 +01:00
|
|
|
|
if (self.thread.uniqueId.length < 1) {
|
2018-10-31 15:05:24 +01:00
|
|
|
|
OWSFailDebug(@"uniqueId unexpectedly empty for thread: %@", self.thread);
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-03 20:39:32 +01:00
|
|
|
|
self.messageMapping = [[ConversationMessageMapping alloc] initWithGroup:self.thread.uniqueId
|
|
|
|
|
desiredLength:self.initialMessageMappingLength];
|
|
|
|
|
|
2018-10-31 15:05:24 +01:00
|
|
|
|
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
2019-01-03 20:39:32 +01:00
|
|
|
|
[self.messageMapping updateWithTransaction:transaction];
|
2018-10-31 15:05:24 +01:00
|
|
|
|
}];
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-03 20:39:32 +01:00
|
|
|
|
// This is more expensive than incremental updates.
|
|
|
|
|
//
|
|
|
|
|
// It can be used to get back to a known good state, or to respon
|
|
|
|
|
// It can be done to get back to a known good state
|
|
|
|
|
- (void)resetMapping
|
2018-10-31 15:05:24 +01:00
|
|
|
|
{
|
2019-01-03 20:39:32 +01:00
|
|
|
|
// Don't extend the mapping's desired length.
|
|
|
|
|
[self resetMappingWithAdditionalLength:0];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)resetMappingWithAdditionalLength:(NSUInteger)additionalLength
|
|
|
|
|
{
|
|
|
|
|
OWSAssertDebug(self.messageMapping);
|
|
|
|
|
|
|
|
|
|
[self updateMessageMappingWithAdditionalLength:additionalLength];
|
2018-11-01 19:51:40 +01:00
|
|
|
|
|
2018-10-31 15:05:24 +01:00
|
|
|
|
self.collapseCutoffDate = [NSDate new];
|
|
|
|
|
|
|
|
|
|
[self ensureDynamicInteractions];
|
|
|
|
|
|
2019-01-03 20:39:32 +01:00
|
|
|
|
if (![self reloadViewItems]) {
|
|
|
|
|
OWSFailDebug(@"failed to reload view items in resetMapping.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[self.delegate conversationViewModelDidUpdate:ConversationUpdate.reloadUpdate];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)touchDbAsync
|
|
|
|
|
{
|
|
|
|
|
OWSLogInfo(@"");
|
|
|
|
|
|
2018-10-31 15:05:24 +01:00
|
|
|
|
// There appears to be a bug in YapDatabase that sometimes delays modifications
|
|
|
|
|
// made in another process (e.g. the SAE) from showing up in other processes.
|
|
|
|
|
// There's a simple workaround: a trivial write to the database flushes changes
|
|
|
|
|
// made from other processes.
|
|
|
|
|
[self.editingDatabaseConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
|
|
|
|
[transaction setObject:[NSUUID UUID].UUIDString forKey:@"conversation_view_noop_mod" inCollection:@"temp"];
|
|
|
|
|
}];
|
2019-01-03 20:39:32 +01:00
|
|
|
|
}
|
2018-10-31 15:05:24 +01:00
|
|
|
|
|
2019-01-03 20:39:32 +01:00
|
|
|
|
- (void)applicationWillEnterForeground:(NSNotification *)notification
|
|
|
|
|
{
|
|
|
|
|
[self touchDbAsync];
|
2018-10-31 15:05:24 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#pragma mark - View Items
|
|
|
|
|
|
2018-12-13 15:33:45 +01:00
|
|
|
|
- (void)ensureConversationProfileState
|
2018-12-13 15:12:41 +01:00
|
|
|
|
{
|
2018-12-13 15:33:45 +01:00
|
|
|
|
if (self.conversationProfileState) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2018-12-13 15:12:41 +01:00
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-12-13 15:33:45 +01:00
|
|
|
|
ConversationProfileState *conversationProfileState = [ConversationProfileState new];
|
|
|
|
|
conversationProfileState.hasLocalProfile = hasLocalProfile;
|
|
|
|
|
conversationProfileState.isThreadInProfileWhitelist = isThreadInProfileWhitelist;
|
|
|
|
|
conversationProfileState.hasUnwhitelistedMember = hasUnwhitelistedMember;
|
|
|
|
|
self.conversationProfileState = conversationProfileState;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (nullable TSInteraction *)firstCallOrMessageForLoadedInteractions:(NSArray<TSInteraction *> *)loadedInteractions
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
for (TSInteraction *interaction in loadedInteractions) {
|
|
|
|
|
switch (interaction.interactionType) {
|
|
|
|
|
case OWSInteractionType_Unknown:
|
|
|
|
|
OWSFailDebug(@"Unknown interaction type.");
|
|
|
|
|
return nil;
|
|
|
|
|
case OWSInteractionType_IncomingMessage:
|
|
|
|
|
case OWSInteractionType_OutgoingMessage:
|
|
|
|
|
return interaction;
|
|
|
|
|
case OWSInteractionType_Error:
|
|
|
|
|
case OWSInteractionType_Info:
|
|
|
|
|
break;
|
|
|
|
|
case OWSInteractionType_Call:
|
|
|
|
|
case OWSInteractionType_Offer:
|
|
|
|
|
case OWSInteractionType_TypingIndicator:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil;
|
2018-12-13 15:12:41 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (nullable OWSContactOffersInteraction *)
|
|
|
|
|
tryToBuildContactOffersInteractionWithTransaction:(YapDatabaseReadTransaction *)transaction
|
2018-12-13 15:33:45 +01:00
|
|
|
|
loadedInteractions:(NSArray<TSInteraction *> *)loadedInteractions
|
2018-12-13 16:14:15 +01:00
|
|
|
|
canLoadMoreItems:(BOOL)canLoadMoreItems
|
2018-12-13 15:12:41 +01:00
|
|
|
|
{
|
|
|
|
|
OWSAssertDebug(transaction);
|
2018-12-13 15:33:45 +01:00
|
|
|
|
OWSAssertDebug(self.conversationProfileState);
|
|
|
|
|
|
2018-12-13 16:14:15 +01:00
|
|
|
|
if (canLoadMoreItems) {
|
|
|
|
|
// Only show contact offers at the start of the conversation.
|
|
|
|
|
return nil;
|
|
|
|
|
}
|
|
|
|
|
|
2018-12-13 15:33:45 +01:00
|
|
|
|
BOOL hasLocalProfile = self.conversationProfileState.hasLocalProfile;
|
|
|
|
|
BOOL isThreadInProfileWhitelist = self.conversationProfileState.isThreadInProfileWhitelist;
|
|
|
|
|
BOOL hasUnwhitelistedMember = self.conversationProfileState.hasUnwhitelistedMember;
|
2018-12-13 15:12:41 +01:00
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
2018-12-13 15:33:45 +01:00
|
|
|
|
TSInteraction *firstCallOrMessage = [self firstCallOrMessageForLoadedInteractions:loadedInteractions];
|
2018-12-13 15:12:41 +01:00
|
|
|
|
if (!firstCallOrMessage) {
|
|
|
|
|
return nil;
|
|
|
|
|
}
|
|
|
|
|
|
2018-12-13 15:33:45 +01:00
|
|
|
|
BOOL hasTooManyOutgoingMessagesToBlock;
|
|
|
|
|
if (self.hasTooManyOutgoingMessagesToBlockCached) {
|
|
|
|
|
hasTooManyOutgoingMessagesToBlock = YES;
|
|
|
|
|
} else {
|
|
|
|
|
NSUInteger outgoingMessageCount =
|
|
|
|
|
[[TSDatabaseView threadOutgoingMessageDatabaseView:transaction] numberOfItemsInGroup:thread.uniqueId];
|
|
|
|
|
|
|
|
|
|
const int kMaxBlockOfferOutgoingMessageCount = 10;
|
|
|
|
|
hasTooManyOutgoingMessagesToBlock = (outgoingMessageCount > kMaxBlockOfferOutgoingMessageCount);
|
|
|
|
|
self.hasTooManyOutgoingMessagesToBlockCached = hasTooManyOutgoingMessagesToBlock;
|
|
|
|
|
}
|
2018-12-13 15:12:41 +01:00
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-12-13 15:33:45 +01:00
|
|
|
|
if (hasTooManyOutgoingMessagesToBlock) {
|
2018-12-13 15:12:41 +01:00
|
|
|
|
// 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];
|
|
|
|
|
|
2018-12-17 20:54:44 +01:00
|
|
|
|
OWSLogInfo(@"Creating contact offers: %@ (%llu)", offersMessage.uniqueId, offersMessage.sortId);
|
2018-12-13 15:12:41 +01:00
|
|
|
|
return offersMessage;
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-31 15:05:24 +01:00
|
|
|
|
// This is a key method. It builds or rebuilds the list of
|
|
|
|
|
// cell view models.
|
|
|
|
|
//
|
|
|
|
|
// Returns NO on error.
|
|
|
|
|
- (BOOL)reloadViewItems
|
|
|
|
|
{
|
|
|
|
|
NSMutableArray<id<ConversationViewItem>> *viewItems = [NSMutableArray new];
|
|
|
|
|
NSMutableDictionary<NSString *, id<ConversationViewItem>> *viewItemCache = [NSMutableDictionary new];
|
|
|
|
|
|
2019-01-03 20:39:32 +01:00
|
|
|
|
NSArray<NSString *> *loadedUniqueIds = [self.messageMapping loadedUniqueIds];
|
2018-10-31 15:05:24 +01:00
|
|
|
|
BOOL isGroupThread = self.thread.isGroupThread;
|
|
|
|
|
ConversationStyle *conversationStyle = self.delegate.conversationStyle;
|
|
|
|
|
|
2018-12-13 15:33:45 +01:00
|
|
|
|
[self ensureConversationProfileState];
|
2018-12-13 15:12:41 +01:00
|
|
|
|
|
2018-10-31 15:05:24 +01:00
|
|
|
|
__block BOOL hasError = NO;
|
2018-12-13 22:30:50 +01:00
|
|
|
|
id<ConversationViewItem> (^tryToAddViewItem)(TSInteraction *, YapDatabaseReadTransaction *)
|
|
|
|
|
= ^(TSInteraction *interaction, YapDatabaseReadTransaction *transaction) {
|
2018-12-14 18:15:26 +01:00
|
|
|
|
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];
|
|
|
|
|
}
|
|
|
|
|
OWSAssertDebug(!viewItemCache[interaction.uniqueId]);
|
|
|
|
|
viewItemCache[interaction.uniqueId] = viewItem;
|
2019-01-03 20:39:32 +01:00
|
|
|
|
[viewItems addObject:viewItem];
|
2018-12-14 18:15:26 +01:00
|
|
|
|
|
|
|
|
|
return viewItem;
|
2018-12-13 22:30:50 +01:00
|
|
|
|
};
|
2018-12-13 15:12:41 +01:00
|
|
|
|
|
2019-01-03 20:39:32 +01:00
|
|
|
|
NSMutableSet<NSString *> *interactionIds = [NSMutableSet new];
|
|
|
|
|
BOOL canLoadMoreItems = self.messageMapping.canLoadMore;
|
2018-12-13 21:09:27 +01:00
|
|
|
|
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
2018-12-13 15:12:41 +01:00
|
|
|
|
NSMutableArray<TSInteraction *> *interactions = [NSMutableArray new];
|
|
|
|
|
|
2019-01-03 20:39:32 +01:00
|
|
|
|
YapDatabaseViewTransaction *viewTransaction = [transaction ext:TSMessageDatabaseViewExtensionName];
|
|
|
|
|
OWSAssertDebug(viewTransaction);
|
|
|
|
|
for (NSString *uniqueId in loadedUniqueIds) {
|
|
|
|
|
TSInteraction *_Nullable interaction =
|
|
|
|
|
[TSInteraction fetchObjectWithUniqueID:uniqueId transaction:transaction];
|
2018-10-31 15:05:24 +01:00
|
|
|
|
if (!interaction) {
|
2019-01-03 20:39:32 +01:00
|
|
|
|
OWSFailDebug(@"missing interaction in message mapping: %@.", uniqueId);
|
2018-10-31 15:05:24 +01:00
|
|
|
|
// TODO: Add analytics.
|
|
|
|
|
hasError = YES;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (!interaction.uniqueId) {
|
2019-01-03 20:39:32 +01:00
|
|
|
|
OWSFailDebug(@"invalid interaction in message mapping: %@.", interaction);
|
2018-10-31 15:05:24 +01:00
|
|
|
|
// TODO: Add analytics.
|
|
|
|
|
hasError = YES;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2018-12-13 15:12:41 +01:00
|
|
|
|
[interactions addObject:interaction];
|
2019-01-03 20:39:32 +01:00
|
|
|
|
if ([interactionIds containsObject:interaction.uniqueId]) {
|
|
|
|
|
OWSFailDebug(@"Duplicate interaction: %@", interaction.uniqueId);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2018-12-13 15:12:41 +01:00
|
|
|
|
[interactionIds addObject:interaction.uniqueId];
|
|
|
|
|
}
|
2018-10-31 15:05:24 +01:00
|
|
|
|
|
2018-12-13 15:33:45 +01:00
|
|
|
|
OWSContactOffersInteraction *_Nullable offers =
|
2018-12-13 16:14:15 +01:00
|
|
|
|
[self tryToBuildContactOffersInteractionWithTransaction:transaction
|
|
|
|
|
loadedInteractions:interactions
|
|
|
|
|
canLoadMoreItems:canLoadMoreItems];
|
2018-12-13 15:12:41 +01:00
|
|
|
|
if (offers && [interactionIds containsObject:offers.beforeInteractionId]) {
|
2018-12-13 21:09:27 +01:00
|
|
|
|
id<ConversationViewItem> offersItem = tryToAddViewItem(offers, transaction);
|
2018-12-13 15:12:41 +01:00
|
|
|
|
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);
|
2018-10-31 15:05:24 +01:00
|
|
|
|
}
|
2018-12-13 15:12:41 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (TSInteraction *interaction in interactions) {
|
2018-12-13 21:09:27 +01:00
|
|
|
|
tryToAddViewItem(interaction, transaction);
|
2018-11-01 16:39:04 +01:00
|
|
|
|
}
|
2018-10-31 15:05:24 +01:00
|
|
|
|
}];
|
|
|
|
|
|
2018-12-12 21:24:36 +01:00
|
|
|
|
// This will usually be redundant, but this will resolve one of the symptoms
|
|
|
|
|
// of the "corrupt YDB view" issue caused by multi-process writes.
|
|
|
|
|
[viewItems sortUsingComparator:^NSComparisonResult(id<ConversationViewItem> left, id<ConversationViewItem> right) {
|
|
|
|
|
return [left.interaction compareForSorting:right.interaction];
|
|
|
|
|
}];
|
|
|
|
|
|
2018-12-14 18:15:26 +01:00
|
|
|
|
if (self.unsavedOutgoingMessages.count > 0) {
|
|
|
|
|
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
|
2019-01-03 20:39:32 +01:00
|
|
|
|
for (TSOutgoingMessage *outgoingMessage in [self.unsavedOutgoingMessages copy]) {
|
|
|
|
|
if ([interactionIds containsObject:outgoingMessage.uniqueId]) {
|
|
|
|
|
OWSFailDebug(@"Duplicate interaction: %@", outgoingMessage.uniqueId);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2018-12-14 18:15:26 +01:00
|
|
|
|
tryToAddViewItem(outgoingMessage, transaction);
|
2019-01-03 20:39:32 +01:00
|
|
|
|
[interactionIds addObject:outgoingMessage.uniqueId];
|
2018-12-14 18:15:26 +01:00
|
|
|
|
}
|
|
|
|
|
}];
|
2018-10-31 15:05:24 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-12-13 22:30:50 +01:00
|
|
|
|
if (self.typingIndicatorsSender) {
|
2018-12-14 18:15:26 +01:00
|
|
|
|
OWSTypingIndicatorInteraction *typingIndicatorInteraction =
|
|
|
|
|
[[OWSTypingIndicatorInteraction alloc] initWithThread:self.thread
|
|
|
|
|
timestamp:[NSDate ows_millisecondTimeStamp]
|
|
|
|
|
recipientId:self.typingIndicatorsSender];
|
2018-12-13 22:30:50 +01:00
|
|
|
|
|
2018-12-14 18:15:26 +01:00
|
|
|
|
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
|
|
|
|
|
tryToAddViewItem(typingIndicatorInteraction, transaction);
|
2018-12-13 22:30:50 +01:00
|
|
|
|
}];
|
2018-12-14 18:15:26 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Flag to ensure that we only increment once per launch.
|
|
|
|
|
if (hasError) {
|
|
|
|
|
OWSLogWarn(@"incrementing version of: %@", TSMessageDatabaseViewExtensionName);
|
|
|
|
|
[OWSPrimaryStorage incrementVersionOfDatabaseExtension:TSMessageDatabaseViewExtensionName];
|
2018-12-13 22:30:50 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-10-31 15:05:24 +01:00
|
|
|
|
// Update the "break" properties (shouldShowDate and unreadIndicator) of the view items.
|
|
|
|
|
BOOL shouldShowDateOnNextViewItem = YES;
|
|
|
|
|
uint64_t previousViewItemTimestamp = 0;
|
|
|
|
|
OWSUnreadIndicator *_Nullable unreadIndicator = self.dynamicInteractions.unreadIndicator;
|
|
|
|
|
uint64_t collapseCutoffTimestamp = [NSDate ows_millisecondsSince1970ForDate:self.collapseCutoffDate];
|
|
|
|
|
|
|
|
|
|
BOOL hasPlacedUnreadIndicator = NO;
|
|
|
|
|
for (id<ConversationViewItem> viewItem in viewItems) {
|
|
|
|
|
BOOL canShowDate = NO;
|
|
|
|
|
switch (viewItem.interaction.interactionType) {
|
|
|
|
|
case OWSInteractionType_Unknown:
|
|
|
|
|
case OWSInteractionType_Offer:
|
2018-11-01 16:39:04 +01:00
|
|
|
|
case OWSInteractionType_TypingIndicator:
|
2018-10-31 15:05:24 +01:00
|
|
|
|
canShowDate = NO;
|
|
|
|
|
break;
|
|
|
|
|
case OWSInteractionType_IncomingMessage:
|
|
|
|
|
case OWSInteractionType_OutgoingMessage:
|
|
|
|
|
case OWSInteractionType_Error:
|
|
|
|
|
case OWSInteractionType_Info:
|
|
|
|
|
case OWSInteractionType_Call:
|
|
|
|
|
canShowDate = YES;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
2018-12-17 20:54:44 +01:00
|
|
|
|
uint64_t viewItemTimestamp = viewItem.interaction.timestamp;
|
2018-10-31 15:05:24 +01:00
|
|
|
|
OWSAssertDebug(viewItemTimestamp > 0);
|
|
|
|
|
|
|
|
|
|
BOOL shouldShowDate = NO;
|
|
|
|
|
if (previousViewItemTimestamp == 0) {
|
|
|
|
|
shouldShowDateOnNextViewItem = YES;
|
|
|
|
|
} else if (![DateUtil isSameDayWithTimestamp:previousViewItemTimestamp timestamp:viewItemTimestamp]) {
|
|
|
|
|
shouldShowDateOnNextViewItem = YES;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (shouldShowDateOnNextViewItem && canShowDate) {
|
|
|
|
|
shouldShowDate = YES;
|
|
|
|
|
shouldShowDateOnNextViewItem = NO;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
viewItem.shouldShowDate = shouldShowDate;
|
|
|
|
|
|
|
|
|
|
previousViewItemTimestamp = viewItemTimestamp;
|
|
|
|
|
|
|
|
|
|
// When a conversation without unread messages receives an incoming message,
|
|
|
|
|
// we call ensureDynamicInteractions to ensure that the unread indicator (etc.)
|
|
|
|
|
// state is updated accordingly. However this is done in a separate transaction.
|
|
|
|
|
// We don't want to show the incoming message _without_ an unread indicator and
|
|
|
|
|
// then immediately re-render it _with_ an unread indicator.
|
|
|
|
|
//
|
|
|
|
|
// To avoid this, we use a temporary instance of OWSUnreadIndicator whenever
|
|
|
|
|
// we find an unread message that _should_ have an unread indicator, but no
|
|
|
|
|
// unread indicator exists yet on dynamicInteractions.
|
|
|
|
|
BOOL isItemUnread = ([viewItem.interaction conformsToProtocol:@protocol(OWSReadTracking)]
|
|
|
|
|
&& !((id<OWSReadTracking>)viewItem.interaction).wasRead);
|
|
|
|
|
if (isItemUnread && !unreadIndicator && !hasPlacedUnreadIndicator && !self.hasClearedUnreadMessagesIndicator) {
|
2018-12-17 20:54:44 +01:00
|
|
|
|
unreadIndicator = [[OWSUnreadIndicator alloc] initWithFirstUnseenSortId:viewItem.interaction.sortId
|
|
|
|
|
hasMoreUnseenMessages:NO
|
|
|
|
|
missingUnseenSafetyNumberChangeCount:0
|
|
|
|
|
unreadIndicatorPosition:0];
|
2018-10-31 15:05:24 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Place the unread indicator onto the first appropriate view item,
|
|
|
|
|
// if any.
|
2018-12-17 20:54:44 +01:00
|
|
|
|
if (unreadIndicator && viewItem.interaction.sortId >= unreadIndicator.firstUnseenSortId) {
|
2018-10-31 15:05:24 +01:00
|
|
|
|
viewItem.unreadIndicator = unreadIndicator;
|
|
|
|
|
unreadIndicator = nil;
|
|
|
|
|
hasPlacedUnreadIndicator = YES;
|
|
|
|
|
} else {
|
|
|
|
|
viewItem.unreadIndicator = nil;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (unreadIndicator) {
|
|
|
|
|
// This isn't necessarily a bug - all of the interactions after the
|
|
|
|
|
// unread indicator may have disappeared or been deleted.
|
|
|
|
|
OWSLogWarn(@"Couldn't find an interaction to hang the unread indicator on.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update the properties of the view items.
|
|
|
|
|
//
|
|
|
|
|
// NOTE: This logic uses the break properties which are set in the previous pass.
|
|
|
|
|
for (NSUInteger i = 0; i < viewItems.count; i++) {
|
|
|
|
|
id<ConversationViewItem> viewItem = viewItems[i];
|
|
|
|
|
id<ConversationViewItem> _Nullable previousViewItem = (i > 0 ? viewItems[i - 1] : nil);
|
|
|
|
|
id<ConversationViewItem> _Nullable nextViewItem = (i + 1 < viewItems.count ? viewItems[i + 1] : nil);
|
|
|
|
|
BOOL shouldShowSenderAvatar = NO;
|
|
|
|
|
BOOL shouldHideFooter = NO;
|
|
|
|
|
BOOL isFirstInCluster = YES;
|
|
|
|
|
BOOL isLastInCluster = YES;
|
|
|
|
|
NSAttributedString *_Nullable senderName = nil;
|
|
|
|
|
|
|
|
|
|
OWSInteractionType interactionType = viewItem.interaction.interactionType;
|
|
|
|
|
NSString *timestampText = [DateUtil formatTimestampShort:viewItem.interaction.timestamp];
|
|
|
|
|
|
|
|
|
|
if (interactionType == OWSInteractionType_OutgoingMessage) {
|
|
|
|
|
TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)viewItem.interaction;
|
|
|
|
|
MessageReceiptStatus receiptStatus =
|
|
|
|
|
[MessageRecipientStatusUtils recipientStatusWithOutgoingMessage:outgoingMessage];
|
|
|
|
|
BOOL isDisappearingMessage = outgoingMessage.isExpiringMessage;
|
|
|
|
|
|
|
|
|
|
if (nextViewItem && nextViewItem.interaction.interactionType == interactionType) {
|
|
|
|
|
TSOutgoingMessage *nextOutgoingMessage = (TSOutgoingMessage *)nextViewItem.interaction;
|
|
|
|
|
MessageReceiptStatus nextReceiptStatus =
|
|
|
|
|
[MessageRecipientStatusUtils recipientStatusWithOutgoingMessage:nextOutgoingMessage];
|
|
|
|
|
NSString *nextTimestampText = [DateUtil formatTimestampShort:nextViewItem.interaction.timestamp];
|
|
|
|
|
|
|
|
|
|
// We can skip the "outgoing message status" footer if the next message
|
|
|
|
|
// has the same footer and no "date break" separates us...
|
|
|
|
|
// ...but always show "failed to send" status
|
|
|
|
|
// ...and always show the "disappearing messages" animation.
|
|
|
|
|
shouldHideFooter
|
|
|
|
|
= ([timestampText isEqualToString:nextTimestampText] && receiptStatus == nextReceiptStatus
|
|
|
|
|
&& outgoingMessage.messageState != TSOutgoingMessageStateFailed
|
|
|
|
|
&& outgoingMessage.messageState != TSOutgoingMessageStateSending && !nextViewItem.hasCellHeader
|
|
|
|
|
&& !isDisappearingMessage);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// clustering
|
|
|
|
|
if (previousViewItem == nil) {
|
|
|
|
|
isFirstInCluster = YES;
|
|
|
|
|
} else if (viewItem.hasCellHeader) {
|
|
|
|
|
isFirstInCluster = YES;
|
|
|
|
|
} else {
|
|
|
|
|
isFirstInCluster = previousViewItem.interaction.interactionType != OWSInteractionType_OutgoingMessage;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (nextViewItem == nil) {
|
|
|
|
|
isLastInCluster = YES;
|
|
|
|
|
} else if (nextViewItem.hasCellHeader) {
|
|
|
|
|
isLastInCluster = YES;
|
|
|
|
|
} else {
|
|
|
|
|
isLastInCluster = nextViewItem.interaction.interactionType != OWSInteractionType_OutgoingMessage;
|
|
|
|
|
}
|
|
|
|
|
} else if (interactionType == OWSInteractionType_IncomingMessage) {
|
|
|
|
|
|
|
|
|
|
TSIncomingMessage *incomingMessage = (TSIncomingMessage *)viewItem.interaction;
|
|
|
|
|
NSString *incomingSenderId = incomingMessage.authorId;
|
|
|
|
|
OWSAssertDebug(incomingSenderId.length > 0);
|
|
|
|
|
BOOL isDisappearingMessage = incomingMessage.isExpiringMessage;
|
|
|
|
|
|
|
|
|
|
NSString *_Nullable nextIncomingSenderId = nil;
|
|
|
|
|
if (nextViewItem && nextViewItem.interaction.interactionType == interactionType) {
|
|
|
|
|
TSIncomingMessage *nextIncomingMessage = (TSIncomingMessage *)nextViewItem.interaction;
|
|
|
|
|
nextIncomingSenderId = nextIncomingMessage.authorId;
|
|
|
|
|
OWSAssertDebug(nextIncomingSenderId.length > 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (nextViewItem && nextViewItem.interaction.interactionType == interactionType) {
|
|
|
|
|
NSString *nextTimestampText = [DateUtil formatTimestampShort:nextViewItem.interaction.timestamp];
|
|
|
|
|
// We can skip the "incoming message status" footer in a cluster if the next message
|
|
|
|
|
// has the same footer and no "date break" separates us.
|
|
|
|
|
// ...but always show the "disappearing messages" animation.
|
|
|
|
|
shouldHideFooter = ([timestampText isEqualToString:nextTimestampText] && !nextViewItem.hasCellHeader &&
|
|
|
|
|
[NSObject isNullableObject:nextIncomingSenderId equalTo:incomingSenderId]
|
|
|
|
|
&& !isDisappearingMessage);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// clustering
|
|
|
|
|
if (previousViewItem == nil) {
|
|
|
|
|
isFirstInCluster = YES;
|
|
|
|
|
} else if (viewItem.hasCellHeader) {
|
|
|
|
|
isFirstInCluster = YES;
|
|
|
|
|
} else if (previousViewItem.interaction.interactionType != OWSInteractionType_IncomingMessage) {
|
|
|
|
|
isFirstInCluster = YES;
|
|
|
|
|
} else {
|
|
|
|
|
TSIncomingMessage *previousIncomingMessage = (TSIncomingMessage *)previousViewItem.interaction;
|
|
|
|
|
isFirstInCluster = ![incomingSenderId isEqual:previousIncomingMessage.authorId];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (nextViewItem == nil) {
|
|
|
|
|
isLastInCluster = YES;
|
|
|
|
|
} else if (nextViewItem.interaction.interactionType != OWSInteractionType_IncomingMessage) {
|
|
|
|
|
isLastInCluster = YES;
|
|
|
|
|
} else if (nextViewItem.hasCellHeader) {
|
|
|
|
|
isLastInCluster = YES;
|
|
|
|
|
} else {
|
|
|
|
|
TSIncomingMessage *nextIncomingMessage = (TSIncomingMessage *)nextViewItem.interaction;
|
|
|
|
|
isLastInCluster = ![incomingSenderId isEqual:nextIncomingMessage.authorId];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (viewItem.isGroupThread) {
|
|
|
|
|
// Show the sender name for incoming group messages unless
|
|
|
|
|
// the previous message has the same sender name and
|
|
|
|
|
// no "date break" separates us.
|
|
|
|
|
BOOL shouldShowSenderName = YES;
|
|
|
|
|
if (previousViewItem && previousViewItem.interaction.interactionType == interactionType) {
|
|
|
|
|
|
|
|
|
|
TSIncomingMessage *previousIncomingMessage = (TSIncomingMessage *)previousViewItem.interaction;
|
|
|
|
|
NSString *previousIncomingSenderId = previousIncomingMessage.authorId;
|
|
|
|
|
OWSAssertDebug(previousIncomingSenderId.length > 0);
|
|
|
|
|
|
|
|
|
|
shouldShowSenderName
|
|
|
|
|
= (![NSObject isNullableObject:previousIncomingSenderId equalTo:incomingSenderId]
|
|
|
|
|
|| viewItem.hasCellHeader);
|
|
|
|
|
}
|
|
|
|
|
if (shouldShowSenderName) {
|
|
|
|
|
senderName = [self.contactsManager
|
|
|
|
|
attributedContactOrProfileNameForPhoneIdentifier:incomingSenderId
|
|
|
|
|
primaryAttributes:[OWSMessageBubbleView
|
|
|
|
|
senderNamePrimaryAttributes]
|
|
|
|
|
secondaryAttributes:[OWSMessageBubbleView
|
|
|
|
|
senderNameSecondaryAttributes]];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Show the sender avatar for incoming group messages unless
|
|
|
|
|
// the next message has the same sender avatar and
|
|
|
|
|
// no "date break" separates us.
|
|
|
|
|
shouldShowSenderAvatar = YES;
|
|
|
|
|
if (nextViewItem && nextViewItem.interaction.interactionType == interactionType) {
|
|
|
|
|
shouldShowSenderAvatar = (![NSObject isNullableObject:nextIncomingSenderId equalTo:incomingSenderId]
|
|
|
|
|
|| nextViewItem.hasCellHeader);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-12-17 20:54:44 +01:00
|
|
|
|
if (viewItem.interaction.receivedAtTimestamp > collapseCutoffTimestamp) {
|
2018-10-31 15:05:24 +01:00
|
|
|
|
shouldHideFooter = NO;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
viewItem.isFirstInCluster = isFirstInCluster;
|
|
|
|
|
viewItem.isLastInCluster = isLastInCluster;
|
|
|
|
|
viewItem.shouldShowSenderAvatar = shouldShowSenderAvatar;
|
|
|
|
|
viewItem.shouldHideFooter = shouldHideFooter;
|
|
|
|
|
viewItem.senderName = senderName;
|
|
|
|
|
}
|
|
|
|
|
|
2018-12-14 18:15:26 +01:00
|
|
|
|
self.viewItems = viewItems;
|
|
|
|
|
self.viewItemCache = viewItemCache;
|
|
|
|
|
|
|
|
|
|
return !hasError;
|
2018-10-31 15:05:24 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-12-13 21:27:01 +01:00
|
|
|
|
- (void)appendUnsavedOutgoingTextMessage:(TSOutgoingMessage *)outgoingMessage
|
2018-12-13 17:52:15 +01:00
|
|
|
|
{
|
2018-12-14 16:37:15 +01:00
|
|
|
|
// Because the message isn't yet saved, we don't have sufficient information to build
|
|
|
|
|
// in-memory placeholder for message types more complex than plain text.
|
|
|
|
|
OWSAssertDebug(outgoingMessage.attachmentIds.count == 0);
|
|
|
|
|
OWSAssertDebug(outgoingMessage.contactShare == nil);
|
|
|
|
|
|
2018-12-14 18:15:26 +01:00
|
|
|
|
NSMutableArray<TSOutgoingMessage *> *unsavedOutgoingMessages = [self.unsavedOutgoingMessages mutableCopy];
|
|
|
|
|
[unsavedOutgoingMessages addObject:outgoingMessage];
|
|
|
|
|
self.unsavedOutgoingMessages = unsavedOutgoingMessages;
|
2018-12-13 17:52:15 +01:00
|
|
|
|
|
2018-12-14 18:15:26 +01:00
|
|
|
|
[self updateForTransientItems];
|
2018-12-13 17:52:15 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-10-31 15:05:24 +01:00
|
|
|
|
// Whenever an interaction is modified, we need to reload it from the DB
|
|
|
|
|
// and update the corresponding view item.
|
|
|
|
|
- (void)reloadInteractionForViewItem:(id<ConversationViewItem>)viewItem
|
|
|
|
|
{
|
|
|
|
|
OWSAssertIsOnMainThread();
|
|
|
|
|
OWSAssertDebug(viewItem);
|
|
|
|
|
|
|
|
|
|
// This should never happen, but don't crash in production if we have a bug.
|
|
|
|
|
if (!viewItem) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
|
|
|
|
TSInteraction *_Nullable interaction =
|
|
|
|
|
[TSInteraction fetchObjectWithUniqueID:viewItem.interaction.uniqueId transaction:transaction];
|
|
|
|
|
if (!interaction) {
|
|
|
|
|
OWSFailDebug(@"could not reload interaction");
|
|
|
|
|
} else {
|
|
|
|
|
[viewItem replaceInteraction:interaction transaction:transaction];
|
|
|
|
|
}
|
|
|
|
|
}];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (nullable NSIndexPath *)ensureLoadWindowContainsQuotedReply:(OWSQuotedReplyModel *)quotedReply
|
|
|
|
|
{
|
|
|
|
|
OWSAssertIsOnMainThread();
|
|
|
|
|
OWSAssertDebug(quotedReply);
|
|
|
|
|
OWSAssertDebug(quotedReply.timestamp > 0);
|
|
|
|
|
OWSAssertDebug(quotedReply.authorId.length > 0);
|
|
|
|
|
|
|
|
|
|
if (quotedReply.isRemotelySourced) {
|
|
|
|
|
return nil;
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-03 20:39:32 +01:00
|
|
|
|
__block NSIndexPath *_Nullable indexPath = nil;
|
2018-10-31 15:05:24 +01:00
|
|
|
|
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
2019-01-03 20:39:32 +01:00
|
|
|
|
TSInteraction *_Nullable quotedInteraction =
|
|
|
|
|
[ThreadUtil findInteractionInThreadByTimestamp:quotedReply.timestamp
|
|
|
|
|
authorId:quotedReply.authorId
|
|
|
|
|
threadUniqueId:self.thread.uniqueId
|
|
|
|
|
transaction:transaction];
|
2018-10-31 15:05:24 +01:00
|
|
|
|
if (!quotedInteraction) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-03 20:39:32 +01:00
|
|
|
|
indexPath = [self.messageMapping ensureLoadWindowContainsWithUniqueId:quotedInteraction.uniqueId
|
|
|
|
|
transaction:transaction];
|
2018-10-31 15:05:24 +01:00
|
|
|
|
}];
|
|
|
|
|
|
2019-01-03 20:39:32 +01:00
|
|
|
|
return indexPath;
|
2018-10-31 15:05:24 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (nullable NSNumber *)findGroupIndexOfThreadInteraction:(TSInteraction *)interaction
|
|
|
|
|
transaction:(YapDatabaseReadTransaction *)transaction
|
|
|
|
|
{
|
|
|
|
|
OWSAssertDebug(interaction);
|
|
|
|
|
OWSAssertDebug(transaction);
|
|
|
|
|
|
|
|
|
|
YapDatabaseAutoViewTransaction *_Nullable extension = [transaction extension:TSMessageDatabaseViewExtensionName];
|
|
|
|
|
if (!extension) {
|
|
|
|
|
OWSFailDebug(@"Couldn't load view.");
|
|
|
|
|
return nil;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
NSUInteger groupIndex = 0;
|
|
|
|
|
BOOL foundInGroup =
|
|
|
|
|
[extension getGroup:nil index:&groupIndex forKey:interaction.uniqueId inCollection:TSInteraction.collection];
|
|
|
|
|
if (!foundInGroup) {
|
|
|
|
|
OWSLogError(@"Couldn't find quoted message in group.");
|
|
|
|
|
return nil;
|
|
|
|
|
}
|
|
|
|
|
return @(groupIndex);
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-01 16:39:04 +01:00
|
|
|
|
- (void)typingIndicatorStateDidChange:(NSNotification *)notification
|
|
|
|
|
{
|
|
|
|
|
OWSAssertIsOnMainThread();
|
2018-11-01 21:24:45 +01:00
|
|
|
|
OWSAssertDebug(self.thread);
|
|
|
|
|
|
2018-12-05 04:09:23 +01:00
|
|
|
|
if (notification.object && ![notification.object isEqual:self.thread.uniqueId]) {
|
2018-11-01 21:24:45 +01:00
|
|
|
|
return;
|
|
|
|
|
}
|
2018-11-01 16:39:04 +01:00
|
|
|
|
|
2018-11-01 21:19:03 +01:00
|
|
|
|
self.typingIndicatorsSender = [self.typingIndicators typingRecipientIdForThread:self.thread];
|
2018-11-01 16:39:04 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-01 21:19:03 +01:00
|
|
|
|
- (void)setTypingIndicatorsSender:(nullable NSString *)typingIndicatorsSender
|
2018-11-01 16:39:04 +01:00
|
|
|
|
{
|
|
|
|
|
OWSAssertIsOnMainThread();
|
|
|
|
|
|
2018-11-01 21:19:03 +01:00
|
|
|
|
BOOL didChange = ![NSObject isNullableObject:typingIndicatorsSender equalTo:_typingIndicatorsSender];
|
2018-11-01 16:39:04 +01:00
|
|
|
|
|
2018-11-01 21:19:03 +01:00
|
|
|
|
_typingIndicatorsSender = typingIndicatorsSender;
|
2018-11-01 16:39:04 +01:00
|
|
|
|
|
|
|
|
|
// Update the view items if necessary.
|
|
|
|
|
// We don't have to do this if they haven't been configured yet.
|
2018-12-14 18:15:26 +01:00
|
|
|
|
if (didChange && self.viewItems != nil) {
|
2019-01-03 20:39:32 +01:00
|
|
|
|
// When we receive an incoming message, we clear any typing indicators
|
|
|
|
|
// from that sender. Ideally, we'd like both changes (disappearance of
|
|
|
|
|
// the typing indicators, appearance of the incoming message) to show up
|
|
|
|
|
// in the view at the same time, rather than as a "jerky" two-step
|
|
|
|
|
// visual change.
|
|
|
|
|
//
|
|
|
|
|
// Unfortunately, the view model learns of these changes by separate
|
|
|
|
|
// channels: the incoming message is a database modification and the
|
|
|
|
|
// typing indicator change arrives via this notification.
|
|
|
|
|
//
|
|
|
|
|
// Therefore we pause briefly before updating the view model to reflect
|
|
|
|
|
// typing indicators state changes so that the database modification
|
|
|
|
|
// can usually arrive first and update the view to reflect both changes.
|
|
|
|
|
__weak ConversationViewModel *weakSelf = self;
|
|
|
|
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
|
|
|
[weakSelf updateForTransientItems];
|
|
|
|
|
});
|
2018-11-01 16:39:04 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-31 15:05:24 +01:00
|
|
|
|
@end
|
|
|
|
|
|
|
|
|
|
NS_ASSUME_NONNULL_END
|