|
|
|
@ -211,6 +211,8 @@ typedef enum : NSUInteger {
|
|
|
|
|
@property (nonatomic, readonly) ConversationSearchController *searchController;
|
|
|
|
|
@property (nonatomic, nullable) NSString *lastSearchedText;
|
|
|
|
|
@property (nonatomic) BOOL isShowingSearchUI;
|
|
|
|
|
@property (nonatomic, nullable) MenuActionsViewController *menuActionsViewController;
|
|
|
|
|
@property (nonatomic) CGFloat contentInsetPadding;
|
|
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
|
@ -746,6 +748,8 @@ typedef enum : NSUInteger {
|
|
|
|
|
// We want to set the initial scroll state the first time we enter the view.
|
|
|
|
|
if (!self.viewHasEverAppeared) {
|
|
|
|
|
[self scrollToDefaultPosition];
|
|
|
|
|
} else if (self.menuActionsViewController != nil) {
|
|
|
|
|
[self scrollToFocusInteraction:NO];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[self updateLastVisibleSortId];
|
|
|
|
@ -1257,7 +1261,7 @@ typedef enum : NSUInteger {
|
|
|
|
|
|
|
|
|
|
self.isViewCompletelyAppeared = NO;
|
|
|
|
|
|
|
|
|
|
[[OWSWindowManager sharedManager] hideMenuActionsWindow];
|
|
|
|
|
[self dismissMenuActions];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)viewDidDisappear:(BOOL)animated
|
|
|
|
@ -1965,61 +1969,156 @@ typedef enum : NSUInteger {
|
|
|
|
|
|
|
|
|
|
#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.contentInsetPadding = MAX(mainScreenSize.width, mainScreenSize.height);
|
|
|
|
|
|
|
|
|
|
UIEdgeInsets contentInset = self.collectionView.contentInset;
|
|
|
|
|
contentInset.top += self.contentInsetPadding;
|
|
|
|
|
contentInset.bottom += self.contentInsetPadding;
|
|
|
|
|
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 scrollToFocusInteraction:NO];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)menuActionsDidPresent:(MenuActionsViewController *)menuActionsViewController
|
|
|
|
|
{
|
|
|
|
|
OWSLogVerbose(@"");
|
|
|
|
|
|
|
|
|
|
[self scrollToFocusInteraction: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];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)menuActions:(MenuActionsViewController *)menuActionsViewController
|
|
|
|
|
isPresentingWithVerticalFocusChange:(CGFloat)verticalChange
|
|
|
|
|
- (void)clearMenuActionsState
|
|
|
|
|
{
|
|
|
|
|
UIEdgeInsets oldInset = self.collectionView.contentInset;
|
|
|
|
|
CGPoint oldOffset = self.collectionView.contentOffset;
|
|
|
|
|
OWSLogVerbose(@"");
|
|
|
|
|
|
|
|
|
|
UIEdgeInsets newInset = oldInset;
|
|
|
|
|
CGPoint newOffset = oldOffset;
|
|
|
|
|
if (self.menuActionsViewController == nil) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// In case the message is at the very top or bottom edge of the conversation we have to have these additional
|
|
|
|
|
// insets to be sure we can sufficiently scroll the contentOffset.
|
|
|
|
|
newInset.top += verticalChange;
|
|
|
|
|
newInset.bottom -= verticalChange;
|
|
|
|
|
newOffset.y -= verticalChange;
|
|
|
|
|
UIEdgeInsets contentInset = self.collectionView.contentInset;
|
|
|
|
|
contentInset.top -= self.contentInsetPadding;
|
|
|
|
|
contentInset.bottom -= self.contentInsetPadding;
|
|
|
|
|
self.collectionView.contentInset = contentInset;
|
|
|
|
|
|
|
|
|
|
OWSLogDebug(@"verticalChange: %f, insets: %@ -> %@",
|
|
|
|
|
verticalChange,
|
|
|
|
|
NSStringFromUIEdgeInsets(oldInset),
|
|
|
|
|
NSStringFromUIEdgeInsets(newInset));
|
|
|
|
|
self.menuActionsViewController = nil;
|
|
|
|
|
self.contentInsetPadding = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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)scrollToFocusInteractionIfNecessary
|
|
|
|
|
{
|
|
|
|
|
if (self.menuActionsViewController != nil) {
|
|
|
|
|
[self scrollToFocusInteraction:NO];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)menuActions:(MenuActionsViewController *)menuActionsViewController
|
|
|
|
|
isDismissingWithVerticalFocusChange:(CGFloat)verticalChange
|
|
|
|
|
- (void)scrollToFocusInteraction:(BOOL)animated
|
|
|
|
|
{
|
|
|
|
|
UIEdgeInsets oldInset = self.collectionView.contentInset;
|
|
|
|
|
CGPoint oldOffset = self.collectionView.contentOffset;
|
|
|
|
|
NSValue *_Nullable contentOffset = [self contentOffsetForFocusInteraction];
|
|
|
|
|
if (contentOffset == nil) {
|
|
|
|
|
OWSFailDebug(@"Missing contentOffset.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
[self.collectionView setContentOffset:contentOffset.CGPointValue animated:animated];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UIEdgeInsets newInset = oldInset;
|
|
|
|
|
CGPoint newOffset = oldOffset;
|
|
|
|
|
- (nullable NSValue *)contentOffsetForFocusInteraction
|
|
|
|
|
{
|
|
|
|
|
NSString *_Nullable focusedInteractionId = self.menuActionsViewController.focusedInteraction.uniqueId;
|
|
|
|
|
if (focusedInteractionId == nil) {
|
|
|
|
|
// This is expected if there is no focus interaction.
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
// In case the message is at the very top or bottom edge of the conversation we have to have these additional
|
|
|
|
|
// insets to be sure we can sufficiently scroll the contentOffset.
|
|
|
|
|
newInset.top -= verticalChange;
|
|
|
|
|
newInset.bottom += verticalChange;
|
|
|
|
|
newOffset.y += verticalChange;
|
|
|
|
|
NSIndexPath *_Nullable indexPath = nil;
|
|
|
|
|
for (NSUInteger i = 0; i < self.viewItems.count; i++) {
|
|
|
|
|
id<ConversationViewItem> viewItem = self.viewItems[i];
|
|
|
|
|
if ([viewItem.interaction.uniqueId isEqualToString:focusedInteractionId]) {
|
|
|
|
|
indexPath = [NSIndexPath indexPathForRow:(NSInteger)i inSection:0];
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (indexPath == nil) {
|
|
|
|
|
// This is expected if the focus interaction is being deleted.
|
|
|
|
|
return nil;
|
|
|
|
|
}
|
|
|
|
|
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)];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
OWSLogDebug(@"verticalChange: %f, insets: %@ -> %@",
|
|
|
|
|
verticalChange,
|
|
|
|
|
NSStringFromUIEdgeInsets(oldInset),
|
|
|
|
|
NSStringFromUIEdgeInsets(newInset));
|
|
|
|
|
- (void)dismissMenuActionsIfNecessary
|
|
|
|
|
{
|
|
|
|
|
if (self.shouldDismissMenuActions) {
|
|
|
|
|
[self dismissMenuActions];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
- (BOOL)shouldDismissMenuActions
|
|
|
|
|
{
|
|
|
|
|
if (!OWSWindowManager.sharedManager.isPresentingMenuActions) {
|
|
|
|
|
return NO;
|
|
|
|
|
}
|
|
|
|
|
NSString *_Nullable focusedInteractionId = self.menuActionsViewController.focusedInteraction.uniqueId;
|
|
|
|
|
if (focusedInteractionId == nil) {
|
|
|
|
|
return NO;
|
|
|
|
|
}
|
|
|
|
|
for (id<ConversationViewItem> viewItem in self.viewItems) {
|
|
|
|
|
if ([viewItem.interaction.uniqueId isEqualToString:focusedInteractionId]) {
|
|
|
|
|
return NO;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return YES;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#pragma mark - ConversationViewCellDelegate
|
|
|
|
@ -2068,11 +2167,12 @@ typedef enum : NSUInteger {
|
|
|
|
|
- (void)presentMessageActions:(NSArray<MenuAction *> *)messageActions withFocusedCell:(ConversationViewCell *)cell
|
|
|
|
|
{
|
|
|
|
|
MenuActionsViewController *menuActionsViewController =
|
|
|
|
|
[[MenuActionsViewController alloc] initWithFocusedView:cell actions:messageActions];
|
|
|
|
|
[[MenuActionsViewController alloc] initWithFocusedInteraction:cell.viewItem.interaction
|
|
|
|
|
focusedView:cell
|
|
|
|
|
actions:messageActions];
|
|
|
|
|
|
|
|
|
|
menuActionsViewController.delegate = self;
|
|
|
|
|
|
|
|
|
|
self.conversationViewModel.mostRecentMenuActionsViewItem = cell.viewItem;
|
|
|
|
|
[[OWSWindowManager sharedManager] showMenuActionsWindow:menuActionsViewController];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -3754,8 +3854,12 @@ typedef enum : NSUInteger {
|
|
|
|
|
//
|
|
|
|
|
// Always reserve room for the input accessory, which we display even
|
|
|
|
|
// if the keyboard is not active.
|
|
|
|
|
newInsets.top = 0;
|
|
|
|
|
newInsets.bottom = MAX(0, self.view.height - self.bottomLayoutGuide.length - keyboardEndFrameConverted.origin.y);
|
|
|
|
|
|
|
|
|
|
newInsets.top += self.contentInsetPadding;
|
|
|
|
|
newInsets.bottom += self.contentInsetPadding;
|
|
|
|
|
|
|
|
|
|
BOOL wasScrolledToBottom = [self isScrolledToBottom];
|
|
|
|
|
|
|
|
|
|
void (^adjustInsets)(void) = ^(void) {
|
|
|
|
@ -4410,6 +4514,13 @@ typedef enum : NSUInteger {
|
|
|
|
|
- (CGPoint)collectionView:(UICollectionView *)collectionView
|
|
|
|
|
targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset
|
|
|
|
|
{
|
|
|
|
|
if (self.menuActionsViewController != nil) {
|
|
|
|
|
NSValue *_Nullable contentOffset = [self contentOffsetForFocusInteraction];
|
|
|
|
|
if (contentOffset != nil) {
|
|
|
|
|
return contentOffset.CGPointValue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (self.scrollContinuity == kScrollContinuityBottom && self.lastKnownDistanceFromBottom) {
|
|
|
|
|
NSValue *_Nullable contentOffset =
|
|
|
|
|
[self contentOffsetForLastKnownDistanceFromBottom:self.lastKnownDistanceFromBottom.floatValue];
|
|
|
|
@ -4659,6 +4770,7 @@ typedef enum : NSUInteger {
|
|
|
|
|
|
|
|
|
|
[self updateBackButtonUnreadCount];
|
|
|
|
|
[self updateNavigationBarSubtitleLabel];
|
|
|
|
|
[self dismissMenuActionsIfNecessary];
|
|
|
|
|
|
|
|
|
|
if (self.isGroupConversation) {
|
|
|
|
|
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
|
|
|
@ -4866,13 +4978,6 @@ typedef enum : NSUInteger {
|
|
|
|
|
[self scrollToBottomAnimated:NO];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)conversationViewModelDidDeleteMostRecentMenuActionsViewItem
|
|
|
|
|
{
|
|
|
|
|
OWSAssertIsOnMainThread();
|
|
|
|
|
|
|
|
|
|
[[OWSWindowManager sharedManager] hideMenuActionsWindow];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#pragma mark - Orientation
|
|
|
|
|
|
|
|
|
|
- (void)viewWillTransitionToSize:(CGSize)size
|
|
|
|
@ -4886,7 +4991,7 @@ typedef enum : NSUInteger {
|
|
|
|
|
// in the content of this view. It's easier to dismiss the
|
|
|
|
|
// "message actions" window when the device changes orientation
|
|
|
|
|
// than to try to ensure this works in that case.
|
|
|
|
|
[[OWSWindowManager sharedManager] hideMenuActionsWindow];
|
|
|
|
|
[self dismissMenuActions];
|
|
|
|
|
|
|
|
|
|
// Snapshot the "last visible row".
|
|
|
|
|
NSIndexPath *_Nullable lastVisibleIndexPath = self.lastVisibleIndexPath;
|
|
|
|
@ -4912,7 +5017,9 @@ typedef enum : NSUInteger {
|
|
|
|
|
|
|
|
|
|
[strongSelf updateInputToolbarLayout];
|
|
|
|
|
|
|
|
|
|
if (lastVisibleIndexPath) {
|
|
|
|
|
if (self.menuActionsViewController != nil) {
|
|
|
|
|
[self scrollToFocusInteraction:NO];
|
|
|
|
|
} else if (lastVisibleIndexPath) {
|
|
|
|
|
[strongSelf.collectionView scrollToItemAtIndexPath:lastVisibleIndexPath
|
|
|
|
|
atScrollPosition:UICollectionViewScrollPositionBottom
|
|
|
|
|
animated:NO];
|
|
|
|
|