mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
Merge branch 'charlesmchen/alwaysObserveCVM'
This commit is contained in:
commit
e985d2631a
|
@ -9,7 +9,7 @@
|
||||||
<key>OSXVersion</key>
|
<key>OSXVersion</key>
|
||||||
<string>10.14.3</string>
|
<string>10.14.3</string>
|
||||||
<key>WebRTCCommit</key>
|
<key>WebRTCCommit</key>
|
||||||
<string>55de5593cc261fa9368c5ccde98884ed1e278ba0 M72</string>
|
<string>1445d719bf05280270e9f77576f80f973fd847f8 M73</string>
|
||||||
</dict>
|
</dict>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>en</string>
|
<string>en</string>
|
||||||
|
|
|
@ -108,25 +108,6 @@ typedef enum : NSUInteger {
|
||||||
|
|
||||||
#pragma mark -
|
#pragma mark -
|
||||||
|
|
||||||
// We use snapshots to ensure that the view has a consistent
|
|
||||||
// representation of view model state which is not updated
|
|
||||||
// when the view is not observing view model changes.
|
|
||||||
@interface ConversationSnapshot : NSObject
|
|
||||||
|
|
||||||
@property (nonatomic) NSArray<id<ConversationViewItem>> *viewItems;
|
|
||||||
@property (nonatomic) ThreadDynamicInteractions *dynamicInteractions;
|
|
||||||
@property (nonatomic) BOOL canLoadMoreItems;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
#pragma mark -
|
|
||||||
|
|
||||||
@implementation ConversationSnapshot
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
#pragma mark -
|
|
||||||
|
|
||||||
@interface ConversationViewController () <AttachmentApprovalViewControllerDelegate,
|
@interface ConversationViewController () <AttachmentApprovalViewControllerDelegate,
|
||||||
ContactShareApprovalViewControllerDelegate,
|
ContactShareApprovalViewControllerDelegate,
|
||||||
AVAudioPlayerDelegate,
|
AVAudioPlayerDelegate,
|
||||||
|
@ -163,7 +144,6 @@ typedef enum : NSUInteger {
|
||||||
|
|
||||||
@property (nonatomic) TSThread *thread;
|
@property (nonatomic) TSThread *thread;
|
||||||
@property (nonatomic, readonly) ConversationViewModel *conversationViewModel;
|
@property (nonatomic, readonly) ConversationViewModel *conversationViewModel;
|
||||||
@property (nonatomic, readonly) ConversationSnapshot *conversationSnapshot;
|
|
||||||
|
|
||||||
@property (nonatomic, readonly) OWSAudioActivity *recordVoiceNoteAudioActivity;
|
@property (nonatomic, readonly) OWSAudioActivity *recordVoiceNoteAudioActivity;
|
||||||
@property (nonatomic, readonly) NSTimeInterval viewControllerCreatedAt;
|
@property (nonatomic, readonly) NSTimeInterval viewControllerCreatedAt;
|
||||||
|
@ -214,14 +194,10 @@ typedef enum : NSUInteger {
|
||||||
|
|
||||||
@property (nonatomic) BOOL isViewCompletelyAppeared;
|
@property (nonatomic) BOOL isViewCompletelyAppeared;
|
||||||
@property (nonatomic) BOOL isViewVisible;
|
@property (nonatomic) BOOL isViewVisible;
|
||||||
@property (nonatomic) BOOL shouldObserveVMUpdates;
|
|
||||||
@property (nonatomic) BOOL shouldAnimateKeyboardChanges;
|
@property (nonatomic) BOOL shouldAnimateKeyboardChanges;
|
||||||
@property (nonatomic) BOOL viewHasEverAppeared;
|
@property (nonatomic) BOOL viewHasEverAppeared;
|
||||||
@property (nonatomic) BOOL hasUnreadMessages;
|
@property (nonatomic) BOOL hasUnreadMessages;
|
||||||
@property (nonatomic) BOOL isPickingMediaAsDocument;
|
@property (nonatomic) BOOL isPickingMediaAsDocument;
|
||||||
@property (nonatomic, nullable) NSNumber *previousLastTimestamp;
|
|
||||||
@property (nonatomic, nullable) NSNumber *previousViewItemCount;
|
|
||||||
@property (nonatomic, nullable) NSNumber *previousViewTopToContentBottom;
|
|
||||||
@property (nonatomic, nullable) NSNumber *viewHorizonTimestamp;
|
@property (nonatomic, nullable) NSNumber *viewHorizonTimestamp;
|
||||||
@property (nonatomic) ContactShareViewHelper *contactShareViewHelper;
|
@property (nonatomic) ContactShareViewHelper *contactShareViewHelper;
|
||||||
@property (nonatomic) NSTimer *reloadTimer;
|
@property (nonatomic) NSTimer *reloadTimer;
|
||||||
|
@ -235,6 +211,8 @@ typedef enum : NSUInteger {
|
||||||
@property (nonatomic, readonly) ConversationSearchController *searchController;
|
@property (nonatomic, readonly) ConversationSearchController *searchController;
|
||||||
@property (nonatomic, nullable) NSString *lastSearchedText;
|
@property (nonatomic, nullable) NSString *lastSearchedText;
|
||||||
@property (nonatomic) BOOL isShowingSearchUI;
|
@property (nonatomic) BOOL isShowingSearchUI;
|
||||||
|
@property (nonatomic, nullable) MenuActionsViewController *menuActionsViewController;
|
||||||
|
@property (nonatomic) CGFloat extraContentInsetPadding;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
@ -516,13 +494,9 @@ typedef enum : NSUInteger {
|
||||||
_conversationViewModel =
|
_conversationViewModel =
|
||||||
[[ConversationViewModel alloc] initWithThread:thread focusMessageIdOnOpen:focusMessageId delegate:self];
|
[[ConversationViewModel alloc] initWithThread:thread focusMessageIdOnOpen:focusMessageId delegate:self];
|
||||||
|
|
||||||
[self updateConversationSnapshot];
|
|
||||||
|
|
||||||
_searchController = [[ConversationSearchController alloc] initWithThread:thread];
|
_searchController = [[ConversationSearchController alloc] initWithThread:thread];
|
||||||
_searchController.delegate = self;
|
_searchController.delegate = self;
|
||||||
|
|
||||||
[self updateShouldObserveVMUpdates];
|
|
||||||
|
|
||||||
self.reloadTimer = [NSTimer weakScheduledTimerWithTimeInterval:1.f
|
self.reloadTimer = [NSTimer weakScheduledTimerWithTimeInterval:1.f
|
||||||
target:self
|
target:self
|
||||||
selector:@selector(reloadTimerDidFire)
|
selector:@selector(reloadTimerDidFire)
|
||||||
|
@ -540,8 +514,9 @@ typedef enum : NSUInteger {
|
||||||
{
|
{
|
||||||
OWSAssertIsOnMainThread();
|
OWSAssertIsOnMainThread();
|
||||||
|
|
||||||
if (self.isUserScrolling || !self.isViewCompletelyAppeared || !self.isViewVisible || !self.shouldObserveVMUpdates
|
if (self.isUserScrolling || !self.isViewCompletelyAppeared || !self.isViewVisible
|
||||||
|| !self.viewHasEverAppeared) {
|
|| !CurrentAppContext().isAppForegroundAndActive || !self.viewHasEverAppeared
|
||||||
|
|| OWSWindowManager.sharedManager.isPresentingMenuActions) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -710,7 +685,6 @@ typedef enum : NSUInteger {
|
||||||
|
|
||||||
- (void)applicationWillResignActive:(NSNotification *)notification
|
- (void)applicationWillResignActive:(NSNotification *)notification
|
||||||
{
|
{
|
||||||
[self updateShouldObserveVMUpdates];
|
|
||||||
[self cancelVoiceMemo];
|
[self cancelVoiceMemo];
|
||||||
self.isUserScrolling = NO;
|
self.isUserScrolling = NO;
|
||||||
[self saveDraft];
|
[self saveDraft];
|
||||||
|
@ -722,7 +696,6 @@ typedef enum : NSUInteger {
|
||||||
|
|
||||||
- (void)applicationDidBecomeActive:(NSNotification *)notification
|
- (void)applicationDidBecomeActive:(NSNotification *)notification
|
||||||
{
|
{
|
||||||
[self updateShouldObserveVMUpdates];
|
|
||||||
[self startReadTimer];
|
[self startReadTimer];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -775,6 +748,8 @@ typedef enum : NSUInteger {
|
||||||
// We want to set the initial scroll state the first time we enter the view.
|
// We want to set the initial scroll state the first time we enter the view.
|
||||||
if (!self.viewHasEverAppeared) {
|
if (!self.viewHasEverAppeared) {
|
||||||
[self scrollToDefaultPosition];
|
[self scrollToDefaultPosition];
|
||||||
|
} else if (self.menuActionsViewController != nil) {
|
||||||
|
[self scrollToMenuActionInteraction:NO];
|
||||||
}
|
}
|
||||||
|
|
||||||
[self updateLastVisibleSortId];
|
[self updateLastVisibleSortId];
|
||||||
|
@ -788,24 +763,21 @@ typedef enum : NSUInteger {
|
||||||
|
|
||||||
- (NSArray<id<ConversationViewItem>> *)viewItems
|
- (NSArray<id<ConversationViewItem>> *)viewItems
|
||||||
{
|
{
|
||||||
return self.conversationSnapshot.viewItems;
|
return self.conversationViewModel.viewState.viewItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (ThreadDynamicInteractions *)dynamicInteractions
|
- (ThreadDynamicInteractions *)dynamicInteractions
|
||||||
{
|
{
|
||||||
return self.conversationSnapshot.dynamicInteractions;
|
return self.conversationViewModel.dynamicInteractions;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSIndexPath *_Nullable)indexPathOfUnreadMessagesIndicator
|
- (NSIndexPath *_Nullable)indexPathOfUnreadMessagesIndicator
|
||||||
{
|
{
|
||||||
NSInteger row = 0;
|
NSNumber *_Nullable unreadIndicatorIndex = self.conversationViewModel.viewState.unreadIndicatorIndex;
|
||||||
for (id<ConversationViewItem> viewItem in self.viewItems) {
|
if (unreadIndicatorIndex == nil) {
|
||||||
if (viewItem.unreadIndicator) {
|
return nil;
|
||||||
return [NSIndexPath indexPathForRow:row inSection:0];
|
|
||||||
}
|
|
||||||
row++;
|
|
||||||
}
|
}
|
||||||
return nil;
|
return [NSIndexPath indexPathForRow:unreadIndicatorIndex.integerValue inSection:0];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSIndexPath *_Nullable)indexPathOfMessageOnOpen
|
- (NSIndexPath *_Nullable)indexPathOfMessageOnOpen
|
||||||
|
@ -882,7 +854,6 @@ typedef enum : NSUInteger {
|
||||||
// Avoid layout corrupt issues and out-of-date message subtitles.
|
// Avoid layout corrupt issues and out-of-date message subtitles.
|
||||||
self.lastReloadDate = [NSDate new];
|
self.lastReloadDate = [NSDate new];
|
||||||
[self.conversationViewModel viewDidResetContentAndLayout];
|
[self.conversationViewModel viewDidResetContentAndLayout];
|
||||||
[self tryToUpdateConversationSnapshot];
|
|
||||||
[self.collectionView.collectionViewLayout invalidateLayout];
|
[self.collectionView.collectionViewLayout invalidateLayout];
|
||||||
[self.collectionView reloadData];
|
[self.collectionView reloadData];
|
||||||
|
|
||||||
|
@ -1287,7 +1258,7 @@ typedef enum : NSUInteger {
|
||||||
|
|
||||||
self.isViewCompletelyAppeared = NO;
|
self.isViewCompletelyAppeared = NO;
|
||||||
|
|
||||||
[[OWSWindowManager sharedManager] hideMenuActionsWindow];
|
[self dismissMenuActions];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)viewDidDisappear:(BOOL)animated
|
- (void)viewDidDisappear:(BOOL)animated
|
||||||
|
@ -1749,7 +1720,7 @@ typedef enum : NSUInteger {
|
||||||
{
|
{
|
||||||
OWSAssertDebug(self.conversationViewModel);
|
OWSAssertDebug(self.conversationViewModel);
|
||||||
|
|
||||||
self.showLoadMoreHeader = self.conversationSnapshot.canLoadMoreItems;
|
self.showLoadMoreHeader = self.conversationViewModel.canLoadMoreItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)setShowLoadMoreHeader:(BOOL)showLoadMoreHeader
|
- (void)setShowLoadMoreHeader:(BOOL)showLoadMoreHeader
|
||||||
|
@ -1995,63 +1966,151 @@ typedef enum : NSUInteger {
|
||||||
|
|
||||||
#pragma mark - MenuActionsViewControllerDelegate
|
#pragma mark - MenuActionsViewControllerDelegate
|
||||||
|
|
||||||
- (void)menuActionsDidHide:(MenuActionsViewController *)menuActionsViewController
|
- (void)menuActionsWillPresent:(MenuActionsViewController *)menuActionsViewController
|
||||||
{
|
{
|
||||||
|
OWSLogVerbose(@"");
|
||||||
|
|
||||||
|
// While the menu actions are presented, temporarily use extra content
|
||||||
|
// inset padding so that interactions near the top or bottom of the
|
||||||
|
// collection view can be scrolled anywhere within the viewport.
|
||||||
|
//
|
||||||
|
// e.g. In a new conversation, there might be only a single message
|
||||||
|
// which we might want to scroll to the bottom of the screen to
|
||||||
|
// pin above the menu actions popup.
|
||||||
|
CGSize mainScreenSize = UIScreen.mainScreen.bounds.size;
|
||||||
|
self.extraContentInsetPadding = MAX(mainScreenSize.width, mainScreenSize.height);
|
||||||
|
|
||||||
|
UIEdgeInsets contentInset = self.collectionView.contentInset;
|
||||||
|
contentInset.top += self.extraContentInsetPadding;
|
||||||
|
contentInset.bottom += self.extraContentInsetPadding;
|
||||||
|
self.collectionView.contentInset = contentInset;
|
||||||
|
|
||||||
|
self.menuActionsViewController = menuActionsViewController;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)menuActionsIsPresenting:(MenuActionsViewController *)menuActionsViewController
|
||||||
|
{
|
||||||
|
OWSLogVerbose(@"");
|
||||||
|
|
||||||
|
// Changes made in this "is presenting" callback are animated by the caller.
|
||||||
|
[self scrollToMenuActionInteraction:NO];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)menuActionsDidPresent:(MenuActionsViewController *)menuActionsViewController
|
||||||
|
{
|
||||||
|
OWSLogVerbose(@"");
|
||||||
|
|
||||||
|
[self scrollToMenuActionInteraction:NO];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)menuActionsIsDismissing:(MenuActionsViewController *)menuActionsViewController
|
||||||
|
{
|
||||||
|
OWSLogVerbose(@"");
|
||||||
|
|
||||||
|
// Changes made in this "is dismissing" callback are animated by the caller.
|
||||||
|
[self clearMenuActionsState];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)menuActionsDidDismiss:(MenuActionsViewController *)menuActionsViewController
|
||||||
|
{
|
||||||
|
OWSLogVerbose(@"");
|
||||||
|
|
||||||
|
[self dismissMenuActions];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)dismissMenuActions
|
||||||
|
{
|
||||||
|
OWSLogVerbose(@"");
|
||||||
|
|
||||||
|
[self clearMenuActionsState];
|
||||||
[[OWSWindowManager sharedManager] hideMenuActionsWindow];
|
[[OWSWindowManager sharedManager] hideMenuActionsWindow];
|
||||||
|
|
||||||
[self updateShouldObserveVMUpdates];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)menuActions:(MenuActionsViewController *)menuActionsViewController
|
- (void)clearMenuActionsState
|
||||||
isPresentingWithVerticalFocusChange:(CGFloat)verticalChange
|
|
||||||
{
|
{
|
||||||
UIEdgeInsets oldInset = self.collectionView.contentInset;
|
OWSLogVerbose(@"");
|
||||||
CGPoint oldOffset = self.collectionView.contentOffset;
|
|
||||||
|
|
||||||
UIEdgeInsets newInset = oldInset;
|
if (self.menuActionsViewController == nil) {
|
||||||
CGPoint newOffset = oldOffset;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// In case the message is at the very top or bottom edge of the conversation we have to have these additional
|
UIEdgeInsets contentInset = self.collectionView.contentInset;
|
||||||
// insets to be sure we can sufficiently scroll the contentOffset.
|
contentInset.top -= self.extraContentInsetPadding;
|
||||||
newInset.top += verticalChange;
|
contentInset.bottom -= self.extraContentInsetPadding;
|
||||||
newInset.bottom -= verticalChange;
|
self.collectionView.contentInset = contentInset;
|
||||||
newOffset.y -= verticalChange;
|
|
||||||
|
|
||||||
OWSLogDebug(@"verticalChange: %f, insets: %@ -> %@",
|
self.menuActionsViewController = nil;
|
||||||
verticalChange,
|
self.extraContentInsetPadding = 0;
|
||||||
NSStringFromUIEdgeInsets(oldInset),
|
|
||||||
NSStringFromUIEdgeInsets(newInset));
|
|
||||||
|
|
||||||
// Because we're in the context of the frame-changing animation, these adjustments should happen
|
|
||||||
// in lockstep with the messageActions frame change.
|
|
||||||
self.collectionView.contentOffset = newOffset;
|
|
||||||
self.collectionView.contentInset = newInset;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)menuActions:(MenuActionsViewController *)menuActionsViewController
|
- (void)scrollToMenuActionInteractionIfNecessary
|
||||||
isDismissingWithVerticalFocusChange:(CGFloat)verticalChange
|
|
||||||
{
|
{
|
||||||
UIEdgeInsets oldInset = self.collectionView.contentInset;
|
if (self.menuActionsViewController != nil) {
|
||||||
CGPoint oldOffset = self.collectionView.contentOffset;
|
[self scrollToMenuActionInteraction:NO];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
UIEdgeInsets newInset = oldInset;
|
- (void)scrollToMenuActionInteraction:(BOOL)animated
|
||||||
CGPoint newOffset = oldOffset;
|
{
|
||||||
|
OWSAssertDebug(self.menuActionsViewController);
|
||||||
|
|
||||||
// In case the message is at the very top or bottom edge of the conversation we have to have these additional
|
NSValue *_Nullable contentOffset = [self contentOffsetForMenuActionInteraction];
|
||||||
// insets to be sure we can sufficiently scroll the contentOffset.
|
if (contentOffset == nil) {
|
||||||
newInset.top -= verticalChange;
|
OWSFailDebug(@"Missing contentOffset.");
|
||||||
newInset.bottom += verticalChange;
|
return;
|
||||||
newOffset.y += verticalChange;
|
}
|
||||||
|
[self.collectionView setContentOffset:contentOffset.CGPointValue animated:animated];
|
||||||
|
}
|
||||||
|
|
||||||
OWSLogDebug(@"verticalChange: %f, insets: %@ -> %@",
|
- (nullable NSValue *)contentOffsetForMenuActionInteraction
|
||||||
verticalChange,
|
{
|
||||||
NSStringFromUIEdgeInsets(oldInset),
|
OWSAssertDebug(self.menuActionsViewController);
|
||||||
NSStringFromUIEdgeInsets(newInset));
|
|
||||||
|
|
||||||
// Because we're in the context of the frame-changing animation, these adjustments should happen
|
NSString *_Nullable menuActionInteractionId = self.menuActionsViewController.focusedInteraction.uniqueId;
|
||||||
// in lockstep with the messageActions frame change.
|
if (menuActionInteractionId == nil) {
|
||||||
self.collectionView.contentOffset = newOffset;
|
OWSFailDebug(@"Missing menu action interaction.");
|
||||||
self.collectionView.contentInset = newInset;
|
return nil;
|
||||||
|
}
|
||||||
|
CGPoint modalTopWindow = [self.menuActionsViewController.focusUI convertPoint:CGPointZero toView:nil];
|
||||||
|
CGPoint modalTopLocal = [self.view convertPoint:modalTopWindow fromView:nil];
|
||||||
|
CGPoint offset = modalTopLocal;
|
||||||
|
CGFloat focusTop = offset.y - self.menuActionsViewController.vSpacing;
|
||||||
|
|
||||||
|
NSNumber *_Nullable interactionIndex
|
||||||
|
= self.conversationViewModel.viewState.interactionIndexMap[menuActionInteractionId];
|
||||||
|
if (interactionIndex == nil) {
|
||||||
|
// This is expected if the menu action interaction is being deleted.
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:interactionIndex.integerValue inSection:0];
|
||||||
|
UICollectionViewLayoutAttributes *_Nullable layoutAttributes =
|
||||||
|
[self.layout layoutAttributesForItemAtIndexPath:indexPath];
|
||||||
|
if (layoutAttributes == nil) {
|
||||||
|
OWSFailDebug(@"Missing layoutAttributes.");
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
CGRect cellFrame = layoutAttributes.frame;
|
||||||
|
return [NSValue valueWithCGPoint:CGPointMake(0, CGRectGetMaxY(cellFrame) - focusTop)];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)dismissMenuActionsIfNecessary
|
||||||
|
{
|
||||||
|
if (self.shouldDismissMenuActions) {
|
||||||
|
[self dismissMenuActions];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)shouldDismissMenuActions
|
||||||
|
{
|
||||||
|
if (!OWSWindowManager.sharedManager.isPresentingMenuActions) {
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
NSString *_Nullable menuActionInteractionId = self.menuActionsViewController.focusedInteraction.uniqueId;
|
||||||
|
if (menuActionInteractionId == nil) {
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
// Check whether there is still a view item for this interaction.
|
||||||
|
return (self.conversationViewModel.viewState.interactionIndexMap[menuActionInteractionId] == nil);
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - ConversationViewCellDelegate
|
#pragma mark - ConversationViewCellDelegate
|
||||||
|
@ -2100,14 +2159,13 @@ typedef enum : NSUInteger {
|
||||||
- (void)presentMessageActions:(NSArray<MenuAction *> *)messageActions withFocusedCell:(ConversationViewCell *)cell
|
- (void)presentMessageActions:(NSArray<MenuAction *> *)messageActions withFocusedCell:(ConversationViewCell *)cell
|
||||||
{
|
{
|
||||||
MenuActionsViewController *menuActionsViewController =
|
MenuActionsViewController *menuActionsViewController =
|
||||||
[[MenuActionsViewController alloc] initWithFocusedView:cell actions:messageActions];
|
[[MenuActionsViewController alloc] initWithFocusedInteraction:cell.viewItem.interaction
|
||||||
|
focusedView:cell
|
||||||
|
actions:messageActions];
|
||||||
|
|
||||||
menuActionsViewController.delegate = self;
|
menuActionsViewController.delegate = self;
|
||||||
|
|
||||||
self.conversationViewModel.mostRecentMenuActionsViewItem = cell.viewItem;
|
|
||||||
[[OWSWindowManager sharedManager] showMenuActionsWindow:menuActionsViewController];
|
[[OWSWindowManager sharedManager] showMenuActionsWindow:menuActionsViewController];
|
||||||
|
|
||||||
[self updateShouldObserveVMUpdates];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSAttributedString *)attributedContactOrProfileNameForPhoneIdentifier:(NSString *)recipientId
|
- (NSAttributedString *)attributedContactOrProfileNameForPhoneIdentifier:(NSString *)recipientId
|
||||||
|
@ -3788,8 +3846,12 @@ typedef enum : NSUInteger {
|
||||||
//
|
//
|
||||||
// Always reserve room for the input accessory, which we display even
|
// Always reserve room for the input accessory, which we display even
|
||||||
// if the keyboard is not active.
|
// if the keyboard is not active.
|
||||||
|
newInsets.top = 0;
|
||||||
newInsets.bottom = MAX(0, self.view.height - self.bottomLayoutGuide.length - keyboardEndFrameConverted.origin.y);
|
newInsets.bottom = MAX(0, self.view.height - self.bottomLayoutGuide.length - keyboardEndFrameConverted.origin.y);
|
||||||
|
|
||||||
|
newInsets.top += self.extraContentInsetPadding;
|
||||||
|
newInsets.bottom += self.extraContentInsetPadding;
|
||||||
|
|
||||||
BOOL wasScrolledToBottom = [self isScrolledToBottom];
|
BOOL wasScrolledToBottom = [self isScrolledToBottom];
|
||||||
|
|
||||||
void (^adjustInsets)(void) = ^(void) {
|
void (^adjustInsets)(void) = ^(void) {
|
||||||
|
@ -4308,7 +4370,6 @@ typedef enum : NSUInteger {
|
||||||
{
|
{
|
||||||
_isViewVisible = isViewVisible;
|
_isViewVisible = isViewVisible;
|
||||||
|
|
||||||
[self updateShouldObserveVMUpdates];
|
|
||||||
[self updateCellsVisible];
|
[self updateCellsVisible];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4321,134 +4382,8 @@ typedef enum : NSUInteger {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)updateShouldObserveVMUpdates
|
|
||||||
{
|
|
||||||
if (!CurrentAppContext().isAppForegroundAndActive) {
|
|
||||||
self.shouldObserveVMUpdates = NO;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!self.isViewVisible) {
|
|
||||||
self.shouldObserveVMUpdates = NO;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (OWSWindowManager.sharedManager.isPresentingMenuActions) {
|
|
||||||
self.shouldObserveVMUpdates = NO;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.shouldObserveVMUpdates = YES;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)setShouldObserveVMUpdates:(BOOL)shouldObserveVMUpdates
|
|
||||||
{
|
|
||||||
if (_shouldObserveVMUpdates == shouldObserveVMUpdates) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_shouldObserveVMUpdates = shouldObserveVMUpdates;
|
|
||||||
|
|
||||||
if (self.shouldObserveVMUpdates) {
|
|
||||||
OWSLogVerbose(@"resume observation of view model.");
|
|
||||||
|
|
||||||
[self updateConversationSnapshot];
|
|
||||||
[self resetContentAndLayout];
|
|
||||||
[self updateBackButtonUnreadCount];
|
|
||||||
[self updateNavigationBarSubtitleLabel];
|
|
||||||
[self updateDisappearingMessagesConfiguration];
|
|
||||||
|
|
||||||
// Detect changes in the mapping's "window" size.
|
|
||||||
if (self.previousViewTopToContentBottom && self.previousViewItemCount
|
|
||||||
&& self.previousViewItemCount.unsignedIntegerValue != self.viewItems.count) {
|
|
||||||
CGFloat newContentHeight = self.safeContentHeight;
|
|
||||||
CGPoint newContentOffset
|
|
||||||
= CGPointMake(0, MAX(0, newContentHeight - self.previousViewTopToContentBottom.floatValue));
|
|
||||||
[self.collectionView setContentOffset:newContentOffset animated:NO];
|
|
||||||
}
|
|
||||||
|
|
||||||
// When we resume observing database changes, we want to scroll to show the user
|
|
||||||
// any new items inserted while we were not observing. We therefore find the
|
|
||||||
// first item at or after the "view horizon". See the comments below which explain
|
|
||||||
// the "view horizon".
|
|
||||||
id<ConversationViewItem> _Nullable lastViewItem = self.viewItems.lastObject;
|
|
||||||
BOOL hasAddedNewItems = (lastViewItem && self.previousLastTimestamp
|
|
||||||
&& lastViewItem.interaction.timestamp > self.previousLastTimestamp.unsignedLongLongValue);
|
|
||||||
|
|
||||||
OWSLogInfo(@"hasAddedNewItems: %d", hasAddedNewItems);
|
|
||||||
if (hasAddedNewItems) {
|
|
||||||
NSIndexPath *_Nullable indexPathToShow = [self firstIndexPathAtViewHorizonTimestamp];
|
|
||||||
if (indexPathToShow) {
|
|
||||||
// The goal is to show _both_ the last item before the "view horizon" and the
|
|
||||||
// first item after the "view horizon". We can't do "top on first item after"
|
|
||||||
// or "bottom on last item before" or we won't see the other. Unfortunately,
|
|
||||||
// this gets tricky if either is huge. The largest cells are oversize text,
|
|
||||||
// which should be rare. Other cells are considerably smaller than a screenful.
|
|
||||||
[self.collectionView scrollToItemAtIndexPath:indexPathToShow
|
|
||||||
atScrollPosition:UICollectionViewScrollPositionCenteredVertically
|
|
||||||
animated:NO];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.viewHorizonTimestamp = nil;
|
|
||||||
OWSLogVerbose(@"resumed observation of view model.");
|
|
||||||
} else {
|
|
||||||
OWSLogVerbose(@"pausing observation of view model.");
|
|
||||||
// When stopping observation, try to record the timestamp of the "view horizon".
|
|
||||||
// The "view horizon" is where we'll want to focus the users when we resume
|
|
||||||
// observation if any changes have happened while we weren't observing.
|
|
||||||
// Ideally, we'll focus on those changes. But we can't skip over unread
|
|
||||||
// interactions, so we prioritize those, if any.
|
|
||||||
//
|
|
||||||
// We'll use this later to update the view to reflect any changes made while
|
|
||||||
// we were not observing the database. See extendRangeToIncludeUnobservedItems
|
|
||||||
// and the logic above.
|
|
||||||
id<ConversationViewItem> _Nullable lastViewItem = self.viewItems.lastObject;
|
|
||||||
if (lastViewItem) {
|
|
||||||
self.previousLastTimestamp = @(lastViewItem.interaction.timestamp);
|
|
||||||
self.previousViewItemCount = @(self.viewItems.count);
|
|
||||||
} else {
|
|
||||||
self.previousLastTimestamp = nil;
|
|
||||||
self.previousViewItemCount = nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
__block TSInteraction *_Nullable firstUnseenInteraction = nil;
|
|
||||||
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
|
||||||
firstUnseenInteraction =
|
|
||||||
[[TSDatabaseView unseenDatabaseViewExtension:transaction] firstObjectInGroup:self.thread.uniqueId];
|
|
||||||
}];
|
|
||||||
if (firstUnseenInteraction) {
|
|
||||||
// If there are any unread interactions, focus on the first one.
|
|
||||||
self.viewHorizonTimestamp = @(firstUnseenInteraction.timestamp);
|
|
||||||
} else if (lastViewItem) {
|
|
||||||
// Otherwise, focus _just after_ the last interaction.
|
|
||||||
self.viewHorizonTimestamp = @(lastViewItem.interaction.timestamp + 1);
|
|
||||||
} else {
|
|
||||||
self.viewHorizonTimestamp = nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Snapshot the scroll state by measuring the "distance from top of view to
|
|
||||||
// bottom of content"; if the mapping's "window" size grows, it will grow
|
|
||||||
// _upward_.
|
|
||||||
OWSAssertDebug([self.collectionView.collectionViewLayout isKindOfClass:[ConversationViewLayout class]]);
|
|
||||||
ConversationViewLayout *conversationViewLayout
|
|
||||||
= (ConversationViewLayout *)self.collectionView.collectionViewLayout;
|
|
||||||
// To avoid laying out the collection view during initial view
|
|
||||||
// presentation, don't trigger layout here (via safeContentHeight)
|
|
||||||
// until layout has been done at least once.
|
|
||||||
if (conversationViewLayout.hasEverHadLayout) {
|
|
||||||
self.previousViewTopToContentBottom = @(self.safeContentHeight - self.collectionView.contentOffset.y);
|
|
||||||
} else {
|
|
||||||
self.previousViewTopToContentBottom = nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
OWSLogVerbose(@"paused observation of view model.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (nullable NSIndexPath *)firstIndexPathAtViewHorizonTimestamp
|
- (nullable NSIndexPath *)firstIndexPathAtViewHorizonTimestamp
|
||||||
{
|
{
|
||||||
OWSAssertDebug(self.shouldObserveVMUpdates);
|
|
||||||
|
|
||||||
if (!self.viewHorizonTimestamp) {
|
if (!self.viewHorizonTimestamp) {
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
@ -4571,6 +4506,13 @@ typedef enum : NSUInteger {
|
||||||
- (CGPoint)collectionView:(UICollectionView *)collectionView
|
- (CGPoint)collectionView:(UICollectionView *)collectionView
|
||||||
targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset
|
targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset
|
||||||
{
|
{
|
||||||
|
if (self.menuActionsViewController != nil) {
|
||||||
|
NSValue *_Nullable contentOffset = [self contentOffsetForMenuActionInteraction];
|
||||||
|
if (contentOffset != nil) {
|
||||||
|
return contentOffset.CGPointValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (self.scrollContinuity == kScrollContinuityBottom && self.lastKnownDistanceFromBottom) {
|
if (self.scrollContinuity == kScrollContinuityBottom && self.lastKnownDistanceFromBottom) {
|
||||||
NSValue *_Nullable contentOffset =
|
NSValue *_Nullable contentOffset =
|
||||||
[self contentOffsetForLastKnownDistanceFromBottom:self.lastKnownDistanceFromBottom.floatValue];
|
[self contentOffsetForLastKnownDistanceFromBottom:self.lastKnownDistanceFromBottom.floatValue];
|
||||||
|
@ -4795,11 +4737,6 @@ typedef enum : NSUInteger {
|
||||||
|
|
||||||
#pragma mark - ConversationViewModelDelegate
|
#pragma mark - ConversationViewModelDelegate
|
||||||
|
|
||||||
- (BOOL)isObservingVMUpdates
|
|
||||||
{
|
|
||||||
return self.shouldObserveVMUpdates;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)conversationViewModelWillUpdate
|
- (void)conversationViewModelWillUpdate
|
||||||
{
|
{
|
||||||
OWSAssertIsOnMainThread();
|
OWSAssertIsOnMainThread();
|
||||||
|
@ -4823,13 +4760,15 @@ typedef enum : NSUInteger {
|
||||||
OWSAssertDebug(conversationUpdate);
|
OWSAssertDebug(conversationUpdate);
|
||||||
OWSAssertDebug(self.conversationViewModel);
|
OWSAssertDebug(self.conversationViewModel);
|
||||||
|
|
||||||
if (!self.shouldObserveVMUpdates) {
|
if (!self.viewLoaded) {
|
||||||
|
// It's safe to ignore updates before the view loads;
|
||||||
|
// viewWillAppear will call resetContentAndLayout.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
[self updateConversationSnapshot];
|
|
||||||
[self updateBackButtonUnreadCount];
|
[self updateBackButtonUnreadCount];
|
||||||
[self updateNavigationBarSubtitleLabel];
|
[self updateNavigationBarSubtitleLabel];
|
||||||
|
[self dismissMenuActionsIfNecessary];
|
||||||
|
|
||||||
if (self.isGroupConversation) {
|
if (self.isGroupConversation) {
|
||||||
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||||
|
@ -5037,13 +4976,6 @@ typedef enum : NSUInteger {
|
||||||
[self scrollToBottomAnimated:NO];
|
[self scrollToBottomAnimated:NO];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)conversationViewModelDidDeleteMostRecentMenuActionsViewItem
|
|
||||||
{
|
|
||||||
OWSAssertIsOnMainThread();
|
|
||||||
|
|
||||||
[[OWSWindowManager sharedManager] hideMenuActionsWindow];
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - Orientation
|
#pragma mark - Orientation
|
||||||
|
|
||||||
- (void)viewWillTransitionToSize:(CGSize)size
|
- (void)viewWillTransitionToSize:(CGSize)size
|
||||||
|
@ -5057,7 +4989,7 @@ typedef enum : NSUInteger {
|
||||||
// in the content of this view. It's easier to dismiss the
|
// in the content of this view. It's easier to dismiss the
|
||||||
// "message actions" window when the device changes orientation
|
// "message actions" window when the device changes orientation
|
||||||
// than to try to ensure this works in that case.
|
// than to try to ensure this works in that case.
|
||||||
[[OWSWindowManager sharedManager] hideMenuActionsWindow];
|
[self dismissMenuActions];
|
||||||
|
|
||||||
// Snapshot the "last visible row".
|
// Snapshot the "last visible row".
|
||||||
NSIndexPath *_Nullable lastVisibleIndexPath = self.lastVisibleIndexPath;
|
NSIndexPath *_Nullable lastVisibleIndexPath = self.lastVisibleIndexPath;
|
||||||
|
@ -5083,7 +5015,9 @@ typedef enum : NSUInteger {
|
||||||
|
|
||||||
[strongSelf updateInputToolbarLayout];
|
[strongSelf updateInputToolbarLayout];
|
||||||
|
|
||||||
if (lastVisibleIndexPath) {
|
if (self.menuActionsViewController != nil) {
|
||||||
|
[self scrollToMenuActionInteraction:NO];
|
||||||
|
} else if (lastVisibleIndexPath) {
|
||||||
[strongSelf.collectionView scrollToItemAtIndexPath:lastVisibleIndexPath
|
[strongSelf.collectionView scrollToItemAtIndexPath:lastVisibleIndexPath
|
||||||
atScrollPosition:UICollectionViewScrollPositionBottom
|
atScrollPosition:UICollectionViewScrollPositionBottom
|
||||||
animated:NO];
|
animated:NO];
|
||||||
|
@ -5137,26 +5071,6 @@ typedef enum : NSUInteger {
|
||||||
[self updateScrollDownButtonLayout];
|
[self updateScrollDownButtonLayout];
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - Conversation Snapshot
|
|
||||||
|
|
||||||
- (void)tryToUpdateConversationSnapshot
|
|
||||||
{
|
|
||||||
if (!self.isObservingVMUpdates) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
[self updateConversationSnapshot];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)updateConversationSnapshot
|
|
||||||
{
|
|
||||||
ConversationSnapshot *conversationSnapshot = [ConversationSnapshot new];
|
|
||||||
conversationSnapshot.viewItems = self.conversationViewModel.viewItems;
|
|
||||||
conversationSnapshot.dynamicInteractions = self.conversationViewModel.dynamicInteractions;
|
|
||||||
conversationSnapshot.canLoadMoreItems = self.conversationViewModel.canLoadMoreItems;
|
|
||||||
_conversationSnapshot = conversationSnapshot;
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
NS_ASSUME_NONNULL_END
|
||||||
|
|
|
@ -34,6 +34,19 @@ typedef NS_ENUM(NSUInteger, ConversationUpdateItemType) {
|
||||||
|
|
||||||
#pragma mark -
|
#pragma mark -
|
||||||
|
|
||||||
|
@interface ConversationViewState : NSObject
|
||||||
|
|
||||||
|
@property (nonatomic, readonly) NSArray<id<ConversationViewItem>> *viewItems;
|
||||||
|
@property (nonatomic, readonly) NSDictionary<NSString *, NSNumber *> *interactionIndexMap;
|
||||||
|
// We have to track interactionIds separately. We can't just use interactionIndexMap.allKeys,
|
||||||
|
// as that won't preserve ordering.
|
||||||
|
@property (nonatomic, readonly) NSArray<NSString *> *interactionIds;
|
||||||
|
@property (nonatomic, readonly, nullable) NSNumber *unreadIndicatorIndex;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
#pragma mark -
|
||||||
|
|
||||||
@interface ConversationUpdateItem : NSObject
|
@interface ConversationUpdateItem : NSObject
|
||||||
|
|
||||||
@property (nonatomic, readonly) ConversationUpdateItemType updateItemType;
|
@property (nonatomic, readonly) ConversationUpdateItemType updateItemType;
|
||||||
|
@ -74,10 +87,6 @@ typedef NS_ENUM(NSUInteger, ConversationUpdateItemType) {
|
||||||
// to prod the view to reset its scroll state, etc.
|
// to prod the view to reset its scroll state, etc.
|
||||||
- (void)conversationViewModelDidReset;
|
- (void)conversationViewModelDidReset;
|
||||||
|
|
||||||
- (void)conversationViewModelDidDeleteMostRecentMenuActionsViewItem;
|
|
||||||
|
|
||||||
- (BOOL)isObservingVMUpdates;
|
|
||||||
|
|
||||||
- (ConversationStyle *)conversationStyle;
|
- (ConversationStyle *)conversationStyle;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
@ -86,10 +95,9 @@ typedef NS_ENUM(NSUInteger, ConversationUpdateItemType) {
|
||||||
|
|
||||||
@interface ConversationViewModel : NSObject
|
@interface ConversationViewModel : NSObject
|
||||||
|
|
||||||
@property (nonatomic, readonly) NSArray<id<ConversationViewItem>> *viewItems;
|
@property (nonatomic, readonly) ConversationViewState *viewState;
|
||||||
@property (nonatomic, nullable) NSString *focusMessageIdOnOpen;
|
@property (nonatomic, nullable) NSString *focusMessageIdOnOpen;
|
||||||
@property (nonatomic, readonly, nullable) ThreadDynamicInteractions *dynamicInteractions;
|
@property (nonatomic, readonly, nullable) ThreadDynamicInteractions *dynamicInteractions;
|
||||||
@property (nonatomic, nullable) id<ConversationViewItem> mostRecentMenuActionsViewItem;
|
|
||||||
|
|
||||||
- (instancetype)init NS_UNAVAILABLE;
|
- (instancetype)init NS_UNAVAILABLE;
|
||||||
- (instancetype)initWithThread:(TSThread *)thread
|
- (instancetype)initWithThread:(TSThread *)thread
|
||||||
|
|
|
@ -44,6 +44,50 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
#pragma mark -
|
#pragma mark -
|
||||||
|
|
||||||
|
@implementation ConversationViewState
|
||||||
|
|
||||||
|
- (instancetype)initWithViewItems:(NSArray<id<ConversationViewItem>> *)viewItems
|
||||||
|
{
|
||||||
|
self = [super init];
|
||||||
|
if (!self) {
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
_viewItems = viewItems;
|
||||||
|
NSMutableDictionary<NSString *, NSNumber *> *interactionIndexMap = [NSMutableDictionary new];
|
||||||
|
NSMutableArray<NSString *> *interactionIds = [NSMutableArray new];
|
||||||
|
for (NSUInteger i = 0; i < self.viewItems.count; i++) {
|
||||||
|
id<ConversationViewItem> viewItem = self.viewItems[i];
|
||||||
|
interactionIndexMap[viewItem.interaction.uniqueId] = @(i);
|
||||||
|
[interactionIds addObject:viewItem.interaction.uniqueId];
|
||||||
|
|
||||||
|
if (viewItem.unreadIndicator != nil) {
|
||||||
|
_unreadIndicatorIndex = @(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_interactionIndexMap = [interactionIndexMap copy];
|
||||||
|
_interactionIds = [interactionIds copy];
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (nullable id<ConversationViewItem>)unreadIndicatorViewItem
|
||||||
|
{
|
||||||
|
if (self.unreadIndicatorIndex == nil) {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
NSUInteger index = self.unreadIndicatorIndex.unsignedIntegerValue;
|
||||||
|
if (index >= self.viewItems.count) {
|
||||||
|
OWSFailDebug(@"Invalid index.");
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
return self.viewItems[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
#pragma mark -
|
||||||
|
|
||||||
@implementation ConversationUpdateItem
|
@implementation ConversationUpdateItem
|
||||||
|
|
||||||
- (instancetype)initWithUpdateItemType:(ConversationUpdateItemType)updateItemType
|
- (instancetype)initWithUpdateItemType:(ConversationUpdateItemType)updateItemType
|
||||||
|
@ -150,7 +194,7 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
||||||
// * Afterward, we must prod the view controller to update layout & view state.
|
// * Afterward, we must prod the view controller to update layout & view state.
|
||||||
@property (nonatomic) ConversationMessageMapping *messageMapping;
|
@property (nonatomic) ConversationMessageMapping *messageMapping;
|
||||||
|
|
||||||
@property (nonatomic) NSArray<id<ConversationViewItem>> *viewItems;
|
@property (nonatomic) ConversationViewState *viewState;
|
||||||
@property (nonatomic) NSMutableDictionary<NSString *, id<ConversationViewItem>> *viewItemCache;
|
@property (nonatomic) NSMutableDictionary<NSString *, id<ConversationViewItem>> *viewItemCache;
|
||||||
|
|
||||||
@property (nonatomic, nullable) ThreadDynamicInteractions *dynamicInteractions;
|
@property (nonatomic, nullable) ThreadDynamicInteractions *dynamicInteractions;
|
||||||
|
@ -187,6 +231,7 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
||||||
_persistedViewItems = @[];
|
_persistedViewItems = @[];
|
||||||
_unsavedOutgoingMessages = @[];
|
_unsavedOutgoingMessages = @[];
|
||||||
self.focusMessageIdOnOpen = focusMessageIdOnOpen;
|
self.focusMessageIdOnOpen = focusMessageIdOnOpen;
|
||||||
|
_viewState = [[ConversationViewState alloc] initWithViewItems:@[]];
|
||||||
|
|
||||||
[self configure];
|
[self configure];
|
||||||
|
|
||||||
|
@ -492,22 +537,12 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (nullable id<ConversationViewItem>)viewItemForUnreadMessagesIndicator
|
|
||||||
{
|
|
||||||
for (id<ConversationViewItem> viewItem in self.viewItems) {
|
|
||||||
if (viewItem.unreadIndicator) {
|
|
||||||
return viewItem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)clearUnreadMessagesIndicator
|
- (void)clearUnreadMessagesIndicator
|
||||||
{
|
{
|
||||||
OWSAssertIsOnMainThread();
|
OWSAssertIsOnMainThread();
|
||||||
|
|
||||||
// TODO: Remove by making unread indicator a view model concern.
|
// TODO: Remove by making unread indicator a view model concern.
|
||||||
id<ConversationViewItem> _Nullable oldIndicatorItem = [self viewItemForUnreadMessagesIndicator];
|
id<ConversationViewItem> _Nullable oldIndicatorItem = [self.viewState unreadIndicatorViewItem];
|
||||||
if (oldIndicatorItem) {
|
if (oldIndicatorItem) {
|
||||||
// TODO ideally this would be happening within the *same* transaction that caused the unreadMessageIndicator
|
// TODO ideally this would be happening within the *same* transaction that caused the unreadMessageIndicator
|
||||||
// to be cleared.
|
// to be cleared.
|
||||||
|
@ -542,19 +577,12 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
||||||
|
|
||||||
OWSLogVerbose(@"");
|
OWSLogVerbose(@"");
|
||||||
|
|
||||||
if (!self.delegate.isObservingVMUpdates) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// External database modifications (e.g. changes from another process such as the SAE)
|
// External database modifications (e.g. changes from another process such as the SAE)
|
||||||
// are "flushed" using touchDbAsync when the app re-enters the foreground.
|
// are "flushed" using touchDbAsync when the app re-enters the foreground.
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)uiDatabaseWillUpdate:(NSNotification *)notification
|
- (void)uiDatabaseWillUpdate:(NSNotification *)notification
|
||||||
{
|
{
|
||||||
if (!self.delegate.isObservingVMUpdates) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
[self.delegate conversationViewModelWillUpdate];
|
[self.delegate conversationViewModelWillUpdate];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -594,12 +622,6 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString *_Nullable mostRecentMenuActionsInterationId = self.mostRecentMenuActionsViewItem.interaction.uniqueId;
|
|
||||||
if (mostRecentMenuActionsInterationId != nil &&
|
|
||||||
[diff.removedItemIds containsObject:mostRecentMenuActionsInterationId]) {
|
|
||||||
[self.delegate conversationViewModelDidDeleteMostRecentMenuActionsViewItem];
|
|
||||||
}
|
|
||||||
|
|
||||||
NSMutableSet<NSString *> *diffAddedItemIds = [diff.addedItemIds mutableCopy];
|
NSMutableSet<NSString *> *diffAddedItemIds = [diff.addedItemIds mutableCopy];
|
||||||
NSMutableSet<NSString *> *diffRemovedItemIds = [diff.removedItemIds mutableCopy];
|
NSMutableSet<NSString *> *diffRemovedItemIds = [diff.removedItemIds mutableCopy];
|
||||||
NSMutableSet<NSString *> *diffUpdatedItemIds = [diff.updatedItemIds mutableCopy];
|
NSMutableSet<NSString *> *diffUpdatedItemIds = [diff.updatedItemIds mutableCopy];
|
||||||
|
@ -626,10 +648,7 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NSMutableArray<NSString *> *oldItemIdList = [NSMutableArray new];
|
NSArray<NSString *> *oldItemIdList = self.viewState.interactionIds;
|
||||||
for (id<ConversationViewItem> viewItem in self.viewItems) {
|
|
||||||
[oldItemIdList addObject:viewItem.itemId];
|
|
||||||
}
|
|
||||||
|
|
||||||
// We need to reload any modified interactions _before_ we call
|
// We need to reload any modified interactions _before_ we call
|
||||||
// reloadViewItems.
|
// reloadViewItems.
|
||||||
|
@ -668,7 +687,7 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
OWSLogVerbose(@"self.viewItems.count: %zd -> %zd", oldItemIdList.count, self.viewItems.count);
|
OWSLogVerbose(@"self.viewItems.count: %zd -> %zd", oldItemIdList.count, self.viewState.viewItems.count);
|
||||||
|
|
||||||
[self updateViewWithOldItemIdList:oldItemIdList updatedItemSet:updatedItemSet];
|
[self updateViewWithOldItemIdList:oldItemIdList updatedItemSet:updatedItemSet];
|
||||||
}
|
}
|
||||||
|
@ -681,10 +700,7 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
||||||
|
|
||||||
OWSLogVerbose(@"");
|
OWSLogVerbose(@"");
|
||||||
|
|
||||||
NSMutableArray<NSString *> *oldItemIdList = [NSMutableArray new];
|
NSArray<NSString *> *oldItemIdList = self.viewState.interactionIds;
|
||||||
for (id<ConversationViewItem> viewItem in self.viewItems) {
|
|
||||||
[oldItemIdList addObject:viewItem.itemId];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (![self reloadViewItems]) {
|
if (![self reloadViewItems]) {
|
||||||
// These errors are rare.
|
// These errors are rare.
|
||||||
|
@ -695,7 +711,7 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
OWSLogVerbose(@"self.viewItems.count: %zd -> %zd", oldItemIdList.count, self.viewItems.count);
|
OWSLogVerbose(@"self.viewItems.count: %zd -> %zd", oldItemIdList.count, self.viewState.viewItems.count);
|
||||||
|
|
||||||
[self updateViewWithOldItemIdList:oldItemIdList updatedItemSet:[NSSet set]];
|
[self updateViewWithOldItemIdList:oldItemIdList updatedItemSet:[NSSet set]];
|
||||||
}
|
}
|
||||||
|
@ -705,23 +721,15 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
||||||
OWSAssertDebug(oldItemIdList);
|
OWSAssertDebug(oldItemIdList);
|
||||||
OWSAssertDebug(updatedItemSetParam);
|
OWSAssertDebug(updatedItemSetParam);
|
||||||
|
|
||||||
if (!self.delegate.isObservingVMUpdates) {
|
|
||||||
OWSLogVerbose(@"Skipping VM update.");
|
|
||||||
// We fire this event, but it will be ignored.
|
|
||||||
[self.delegate conversationViewModelDidUpdate:ConversationUpdate.minorUpdate];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oldItemIdList.count != [NSSet setWithArray:oldItemIdList].count) {
|
if (oldItemIdList.count != [NSSet setWithArray:oldItemIdList].count) {
|
||||||
OWSFailDebug(@"Old view item list has duplicates.");
|
OWSFailDebug(@"Old view item list has duplicates.");
|
||||||
[self.delegate conversationViewModelDidUpdate:ConversationUpdate.reloadUpdate];
|
[self.delegate conversationViewModelDidUpdate:ConversationUpdate.reloadUpdate];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
NSMutableArray<NSString *> *newItemIdList = [NSMutableArray new];
|
NSArray<NSString *> *newItemIdList = self.viewState.interactionIds;
|
||||||
NSMutableDictionary<NSString *, id<ConversationViewItem>> *newViewItemMap = [NSMutableDictionary new];
|
NSMutableDictionary<NSString *, id<ConversationViewItem>> *newViewItemMap = [NSMutableDictionary new];
|
||||||
for (id<ConversationViewItem> viewItem in self.viewItems) {
|
for (id<ConversationViewItem> viewItem in self.viewState.viewItems) {
|
||||||
[newItemIdList addObject:viewItem.itemId];
|
|
||||||
newViewItemMap[viewItem.itemId] = viewItem;
|
newViewItemMap[viewItem.itemId] = viewItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1530,7 +1538,7 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
||||||
viewItem.senderName = senderName;
|
viewItem.senderName = senderName;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.viewItems = viewItems;
|
self.viewState = [[ConversationViewState alloc] initWithViewItems:viewItems];
|
||||||
self.viewItemCache = viewItemCache;
|
self.viewItemCache = viewItemCache;
|
||||||
|
|
||||||
return !hasError;
|
return !hasError;
|
||||||
|
@ -1681,7 +1689,7 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
||||||
|
|
||||||
// Update the view items if necessary.
|
// Update the view items if necessary.
|
||||||
// We don't have to do this if they haven't been configured yet.
|
// We don't have to do this if they haven't been configured yet.
|
||||||
if (didChange && self.viewItems != nil) {
|
if (didChange && self.viewState.viewItems != nil) {
|
||||||
// When we receive an incoming message, we clear any typing indicators
|
// When we receive an incoming message, we clear any typing indicators
|
||||||
// from that sender. Ideally, we'd like both changes (disappearance of
|
// from that sender. Ideally, we'd like both changes (disappearance of
|
||||||
// the typing indicators, appearance of the incoming message) to show up
|
// the typing indicators, appearance of the incoming message) to show up
|
||||||
|
|
|
@ -21,9 +21,12 @@ public class MenuAction: NSObject {
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
protocol MenuActionsViewControllerDelegate: class {
|
protocol MenuActionsViewControllerDelegate: class {
|
||||||
func menuActionsDidHide(_ menuActionsViewController: MenuActionsViewController)
|
func menuActionsWillPresent(_ menuActionsViewController: MenuActionsViewController)
|
||||||
func menuActions(_ menuActionsViewController: MenuActionsViewController, isPresentingWithVerticalFocusChange: CGFloat)
|
func menuActionsIsPresenting(_ menuActionsViewController: MenuActionsViewController)
|
||||||
func menuActions(_ menuActionsViewController: MenuActionsViewController, isDismissingWithVerticalFocusChange: CGFloat)
|
func menuActionsDidPresent(_ menuActionsViewController: MenuActionsViewController)
|
||||||
|
|
||||||
|
func menuActionsIsDismissing(_ menuActionsViewController: MenuActionsViewController)
|
||||||
|
func menuActionsDidDismiss(_ menuActionsViewController: MenuActionsViewController)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
|
@ -32,18 +35,20 @@ class MenuActionsViewController: UIViewController, MenuActionSheetDelegate {
|
||||||
@objc
|
@objc
|
||||||
weak var delegate: MenuActionsViewControllerDelegate?
|
weak var delegate: MenuActionsViewControllerDelegate?
|
||||||
|
|
||||||
|
@objc
|
||||||
|
public let focusedInteraction: TSInteraction
|
||||||
|
|
||||||
private let focusedView: UIView
|
private let focusedView: UIView
|
||||||
private let actionSheetView: MenuActionSheetView
|
private let actionSheetView: MenuActionSheetView
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
Logger.verbose("")
|
Logger.verbose("")
|
||||||
assert(didInformDelegateOfDismissalAnimation)
|
|
||||||
assert(didInformDelegateThatDisappearenceCompleted)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
required init(focusedView: UIView, actions: [MenuAction]) {
|
required init(focusedInteraction: TSInteraction, focusedView: UIView, actions: [MenuAction]) {
|
||||||
self.focusedView = focusedView
|
self.focusedView = focusedView
|
||||||
|
self.focusedInteraction = focusedInteraction
|
||||||
|
|
||||||
self.actionSheetView = MenuActionSheetView(actions: actions)
|
self.actionSheetView = MenuActionSheetView(actions: actions)
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
@ -86,8 +91,7 @@ class MenuActionsViewController: UIViewController, MenuActionSheetDelegate {
|
||||||
// When the user has manually dismissed the menu, we do a nice animation
|
// When the user has manually dismissed the menu, we do a nice animation
|
||||||
// but if the view otherwise disappears (e.g. due to resigning active),
|
// but if the view otherwise disappears (e.g. due to resigning active),
|
||||||
// we still want to give the delegate the information it needs to restore it's UI.
|
// we still want to give the delegate the information it needs to restore it's UI.
|
||||||
ensureDelegateIsInformedOfDismissalAnimation()
|
delegate?.menuActionsDidDismiss(self)
|
||||||
ensureDelegateIsInformedThatDisappearenceCompleted()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Orientation
|
// MARK: Orientation
|
||||||
|
@ -98,7 +102,6 @@ class MenuActionsViewController: UIViewController, MenuActionSheetDelegate {
|
||||||
|
|
||||||
// MARK: Present / Dismiss animations
|
// MARK: Present / Dismiss animations
|
||||||
|
|
||||||
var presentationFocusOffset: CGFloat?
|
|
||||||
var snapshotView: UIView?
|
var snapshotView: UIView?
|
||||||
|
|
||||||
private func addSnapshotFocusedView() -> UIView? {
|
private func addSnapshotFocusedView() -> UIView? {
|
||||||
|
@ -150,6 +153,7 @@ class MenuActionsViewController: UIViewController, MenuActionSheetDelegate {
|
||||||
let oldFocusFrame = self.view.convert(focusedView.frame, from: focusedViewSuperview)
|
let oldFocusFrame = self.view.convert(focusedView.frame, from: focusedViewSuperview)
|
||||||
NSLayoutConstraint.deactivate([actionSheetViewVerticalConstraint])
|
NSLayoutConstraint.deactivate([actionSheetViewVerticalConstraint])
|
||||||
self.actionSheetViewVerticalConstraint = self.actionSheetView.autoPinEdge(toSuperviewEdge: .bottom)
|
self.actionSheetViewVerticalConstraint = self.actionSheetView.autoPinEdge(toSuperviewEdge: .bottom)
|
||||||
|
self.delegate?.menuActionsWillPresent(self)
|
||||||
UIView.animate(withDuration: 0.2,
|
UIView.animate(withDuration: 0.2,
|
||||||
delay: backgroundDuration,
|
delay: backgroundDuration,
|
||||||
options: .curveEaseOut,
|
options: .curveEaseOut,
|
||||||
|
@ -160,35 +164,36 @@ class MenuActionsViewController: UIViewController, MenuActionSheetDelegate {
|
||||||
var newFocusFrame = oldFocusFrame
|
var newFocusFrame = oldFocusFrame
|
||||||
|
|
||||||
// Position focused item just over the action sheet.
|
// Position focused item just over the action sheet.
|
||||||
let padding: CGFloat = 10
|
let overlap: CGFloat = (oldFocusFrame.maxY + self.vSpacing) - newSheetFrame.minY
|
||||||
let overlap: CGFloat = (oldFocusFrame.maxY + padding) - newSheetFrame.minY
|
|
||||||
newFocusFrame.origin.y = oldFocusFrame.origin.y - overlap
|
newFocusFrame.origin.y = oldFocusFrame.origin.y - overlap
|
||||||
|
|
||||||
snapshotView.frame = newFocusFrame
|
snapshotView.frame = newFocusFrame
|
||||||
|
|
||||||
let offset = -overlap
|
self.delegate?.menuActionsIsPresenting(self)
|
||||||
self.presentationFocusOffset = offset
|
|
||||||
self.delegate?.menuActions(self, isPresentingWithVerticalFocusChange: offset)
|
|
||||||
},
|
},
|
||||||
completion: nil)
|
completion: { (_) in
|
||||||
|
self.delegate?.menuActionsDidPresent(self)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
public let vSpacing: CGFloat = 10
|
||||||
|
|
||||||
|
@objc
|
||||||
|
public var focusUI: UIView {
|
||||||
|
return actionSheetView
|
||||||
}
|
}
|
||||||
|
|
||||||
private func animateDismiss(action: MenuAction?) {
|
private func animateDismiss(action: MenuAction?) {
|
||||||
guard let actionSheetViewVerticalConstraint = self.actionSheetViewVerticalConstraint else {
|
guard let actionSheetViewVerticalConstraint = self.actionSheetViewVerticalConstraint else {
|
||||||
owsFailDebug("actionSheetVerticalConstraint was unexpectedly nil")
|
owsFailDebug("actionSheetVerticalConstraint was unexpectedly nil")
|
||||||
self.delegate?.menuActionsDidHide(self)
|
delegate?.menuActionsDidDismiss(self)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let snapshotView = self.snapshotView else {
|
guard let snapshotView = self.snapshotView else {
|
||||||
owsFailDebug("snapshotView was unexpectedly nil")
|
owsFailDebug("snapshotView was unexpectedly nil")
|
||||||
self.delegate?.menuActionsDidHide(self)
|
delegate?.menuActionsDidDismiss(self)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let presentationFocusOffset = self.presentationFocusOffset else {
|
|
||||||
owsFailDebug("presentationFocusOffset was unexpectedly nil")
|
|
||||||
self.delegate?.menuActionsDidHide(self)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,48 +208,20 @@ class MenuActionsViewController: UIViewController, MenuActionSheetDelegate {
|
||||||
animations: {
|
animations: {
|
||||||
self.view.backgroundColor = UIColor.clear
|
self.view.backgroundColor = UIColor.clear
|
||||||
self.actionSheetView.superview?.layoutIfNeeded()
|
self.actionSheetView.superview?.layoutIfNeeded()
|
||||||
snapshotView.frame.origin.y -= presentationFocusOffset
|
|
||||||
// this helps when focused view is above navbars, etc.
|
// this helps when focused view is above navbars, etc.
|
||||||
snapshotView.alpha = 0
|
snapshotView.alpha = 0
|
||||||
self.ensureDelegateIsInformedOfDismissalAnimation()
|
|
||||||
|
self.delegate?.menuActionsIsDismissing(self)
|
||||||
},
|
},
|
||||||
completion: { _ in
|
completion: { _ in
|
||||||
self.view.isHidden = true
|
self.view.isHidden = true
|
||||||
self.ensureDelegateIsInformedThatDisappearenceCompleted()
|
self.delegate?.menuActionsDidDismiss(self)
|
||||||
if let action = action {
|
if let action = action {
|
||||||
action.block(action)
|
action.block(action)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
var didInformDelegateThatDisappearenceCompleted = false
|
|
||||||
func ensureDelegateIsInformedThatDisappearenceCompleted() {
|
|
||||||
guard !didInformDelegateThatDisappearenceCompleted else {
|
|
||||||
Logger.debug("ignoring redundant 'disappeared' notification")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
didInformDelegateThatDisappearenceCompleted = true
|
|
||||||
|
|
||||||
self.delegate?.menuActionsDidHide(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
var didInformDelegateOfDismissalAnimation = false
|
|
||||||
func ensureDelegateIsInformedOfDismissalAnimation() {
|
|
||||||
guard !didInformDelegateOfDismissalAnimation else {
|
|
||||||
Logger.debug("ignoring redundant 'dismissal' notification")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
didInformDelegateOfDismissalAnimation = true
|
|
||||||
|
|
||||||
guard let presentationFocusOffset = self.presentationFocusOffset else {
|
|
||||||
owsFailDebug("presentationFocusOffset was unexpectedly nil")
|
|
||||||
self.delegate?.menuActionsDidHide(self)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.delegate?.menuActions(self, isDismissingWithVerticalFocusChange: presentationFocusOffset)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Actions
|
// MARK: Actions
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
|
|
Loading…
Reference in a new issue