Respond to CR.

pull/2/head
Matthew Chen 6 years ago
parent a8e9b87f03
commit 6ab8ea9b6e

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

@ -212,7 +212,7 @@ typedef enum : NSUInteger {
@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, nullable) MenuActionsViewController *menuActionsViewController;
@property (nonatomic) CGFloat contentInsetPadding; @property (nonatomic) CGFloat extraContentInsetPadding;
@end @end
@ -749,7 +749,7 @@ typedef enum : NSUInteger {
if (!self.viewHasEverAppeared) { if (!self.viewHasEverAppeared) {
[self scrollToDefaultPosition]; [self scrollToDefaultPosition];
} else if (self.menuActionsViewController != nil) { } else if (self.menuActionsViewController != nil) {
[self scrollToFocusInteraction:NO]; [self scrollToMenuActionInteraction:NO];
} }
[self updateLastVisibleSortId]; [self updateLastVisibleSortId];
@ -763,7 +763,7 @@ typedef enum : NSUInteger {
- (NSArray<id<ConversationViewItem>> *)viewItems - (NSArray<id<ConversationViewItem>> *)viewItems
{ {
return self.conversationViewModel.viewItems; return self.conversationViewModel.viewState.viewItems;
} }
- (ThreadDynamicInteractions *)dynamicInteractions - (ThreadDynamicInteractions *)dynamicInteractions
@ -773,15 +773,12 @@ typedef enum : NSUInteger {
- (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 [NSIndexPath indexPathForRow:row inSection:0];
}
row++;
}
return nil; return nil;
} }
return [NSIndexPath indexPathForRow:unreadIndicatorIndex.integerValue inSection:0];
}
- (NSIndexPath *_Nullable)indexPathOfMessageOnOpen - (NSIndexPath *_Nullable)indexPathOfMessageOnOpen
{ {
@ -1981,11 +1978,11 @@ typedef enum : NSUInteger {
// which we might want to scroll to the bottom of the screen to // which we might want to scroll to the bottom of the screen to
// pin above the menu actions popup. // pin above the menu actions popup.
CGSize mainScreenSize = UIScreen.mainScreen.bounds.size; CGSize mainScreenSize = UIScreen.mainScreen.bounds.size;
self.contentInsetPadding = MAX(mainScreenSize.width, mainScreenSize.height); self.extraContentInsetPadding = MAX(mainScreenSize.width, mainScreenSize.height);
UIEdgeInsets contentInset = self.collectionView.contentInset; UIEdgeInsets contentInset = self.collectionView.contentInset;
contentInset.top += self.contentInsetPadding; contentInset.top += self.extraContentInsetPadding;
contentInset.bottom += self.contentInsetPadding; contentInset.bottom += self.extraContentInsetPadding;
self.collectionView.contentInset = contentInset; self.collectionView.contentInset = contentInset;
self.menuActionsViewController = menuActionsViewController; self.menuActionsViewController = menuActionsViewController;
@ -1996,14 +1993,14 @@ typedef enum : NSUInteger {
OWSLogVerbose(@""); OWSLogVerbose(@"");
// Changes made in this "is presenting" callback are animated by the caller. // Changes made in this "is presenting" callback are animated by the caller.
[self scrollToFocusInteraction:NO]; [self scrollToMenuActionInteraction:NO];
} }
- (void)menuActionsDidPresent:(MenuActionsViewController *)menuActionsViewController - (void)menuActionsDidPresent:(MenuActionsViewController *)menuActionsViewController
{ {
OWSLogVerbose(@""); OWSLogVerbose(@"");
[self scrollToFocusInteraction:NO]; [self scrollToMenuActionInteraction:NO];
} }
- (void)menuActionsIsDismissing:(MenuActionsViewController *)menuActionsViewController - (void)menuActionsIsDismissing:(MenuActionsViewController *)menuActionsViewController
@ -2038,24 +2035,26 @@ typedef enum : NSUInteger {
} }
UIEdgeInsets contentInset = self.collectionView.contentInset; UIEdgeInsets contentInset = self.collectionView.contentInset;
contentInset.top -= self.contentInsetPadding; contentInset.top -= self.extraContentInsetPadding;
contentInset.bottom -= self.contentInsetPadding; contentInset.bottom -= self.extraContentInsetPadding;
self.collectionView.contentInset = contentInset; self.collectionView.contentInset = contentInset;
self.menuActionsViewController = nil; self.menuActionsViewController = nil;
self.contentInsetPadding = 0; self.extraContentInsetPadding = 0;
} }
- (void)scrollToFocusInteractionIfNecessary - (void)scrollToMenuActionInteractionIfNecessary
{ {
if (self.menuActionsViewController != nil) { if (self.menuActionsViewController != nil) {
[self scrollToFocusInteraction:NO]; [self scrollToMenuActionInteraction:NO];
} }
} }
- (void)scrollToFocusInteraction:(BOOL)animated - (void)scrollToMenuActionInteraction:(BOOL)animated
{ {
NSValue *_Nullable contentOffset = [self contentOffsetForFocusInteraction]; OWSAssertDebug(self.menuActionsViewController);
NSValue *_Nullable contentOffset = [self contentOffsetForMenuActionInteraction];
if (contentOffset == nil) { if (contentOffset == nil) {
OWSFailDebug(@"Missing contentOffset."); OWSFailDebug(@"Missing contentOffset.");
return; return;
@ -2063,11 +2062,13 @@ typedef enum : NSUInteger {
[self.collectionView setContentOffset:contentOffset.CGPointValue animated:animated]; [self.collectionView setContentOffset:contentOffset.CGPointValue animated:animated];
} }
- (nullable NSValue *)contentOffsetForFocusInteraction - (nullable NSValue *)contentOffsetForMenuActionInteraction
{ {
NSString *_Nullable focusedInteractionId = self.menuActionsViewController.focusedInteraction.uniqueId; OWSAssertDebug(self.menuActionsViewController);
if (focusedInteractionId == nil) {
// This is expected if there is no focus interaction. NSString *_Nullable menuActionInteractionId = self.menuActionsViewController.focusedInteraction.uniqueId;
if (menuActionInteractionId == nil) {
OWSFailDebug(@"Missing menu action interaction.");
return nil; return nil;
} }
CGPoint modalTopWindow = [self.menuActionsViewController.focusUI convertPoint:CGPointZero toView:nil]; CGPoint modalTopWindow = [self.menuActionsViewController.focusUI convertPoint:CGPointZero toView:nil];
@ -2075,18 +2076,13 @@ typedef enum : NSUInteger {
CGPoint offset = modalTopLocal; CGPoint offset = modalTopLocal;
CGFloat focusTop = offset.y - self.menuActionsViewController.vSpacing; CGFloat focusTop = offset.y - self.menuActionsViewController.vSpacing;
NSIndexPath *_Nullable indexPath = nil; NSNumber *_Nullable interactionIndex
for (NSUInteger i = 0; i < self.viewItems.count; i++) { = self.conversationViewModel.viewState.interactionIndexMap[menuActionInteractionId];
id<ConversationViewItem> viewItem = self.viewItems[i]; if (interactionIndex == nil) {
if ([viewItem.interaction.uniqueId isEqualToString:focusedInteractionId]) { // This is expected if the menu action interaction is being deleted.
indexPath = [NSIndexPath indexPathForRow:(NSInteger)i inSection:0];
break;
}
}
if (indexPath == nil) {
// This is expected if the focus interaction is being deleted.
return nil; return nil;
} }
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:interactionIndex.integerValue inSection:0];
UICollectionViewLayoutAttributes *_Nullable layoutAttributes = UICollectionViewLayoutAttributes *_Nullable layoutAttributes =
[self.layout layoutAttributesForItemAtIndexPath:indexPath]; [self.layout layoutAttributesForItemAtIndexPath:indexPath];
if (layoutAttributes == nil) { if (layoutAttributes == nil) {
@ -2109,16 +2105,12 @@ typedef enum : NSUInteger {
if (!OWSWindowManager.sharedManager.isPresentingMenuActions) { if (!OWSWindowManager.sharedManager.isPresentingMenuActions) {
return NO; return NO;
} }
NSString *_Nullable focusedInteractionId = self.menuActionsViewController.focusedInteraction.uniqueId; NSString *_Nullable menuActionInteractionId = self.menuActionsViewController.focusedInteraction.uniqueId;
if (focusedInteractionId == nil) { if (menuActionInteractionId == nil) {
return NO; return NO;
} }
for (id<ConversationViewItem> viewItem in self.viewItems) { // Check whether there is still a view item for this interaction.
if ([viewItem.interaction.uniqueId isEqualToString:focusedInteractionId]) { return (self.conversationViewModel.viewState.interactionIndexMap[menuActionInteractionId] == nil);
return NO;
}
}
return YES;
} }
#pragma mark - ConversationViewCellDelegate #pragma mark - ConversationViewCellDelegate
@ -3857,8 +3849,8 @@ typedef enum : NSUInteger {
newInsets.top = 0; 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.contentInsetPadding; newInsets.top += self.extraContentInsetPadding;
newInsets.bottom += self.contentInsetPadding; newInsets.bottom += self.extraContentInsetPadding;
BOOL wasScrolledToBottom = [self isScrolledToBottom]; BOOL wasScrolledToBottom = [self isScrolledToBottom];
@ -4515,7 +4507,7 @@ typedef enum : NSUInteger {
targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset
{ {
if (self.menuActionsViewController != nil) { if (self.menuActionsViewController != nil) {
NSValue *_Nullable contentOffset = [self contentOffsetForFocusInteraction]; NSValue *_Nullable contentOffset = [self contentOffsetForMenuActionInteraction];
if (contentOffset != nil) { if (contentOffset != nil) {
return contentOffset.CGPointValue; return contentOffset.CGPointValue;
} }
@ -4768,6 +4760,11 @@ typedef enum : NSUInteger {
OWSAssertDebug(conversationUpdate); OWSAssertDebug(conversationUpdate);
OWSAssertDebug(self.conversationViewModel); OWSAssertDebug(self.conversationViewModel);
if (!self.viewLoaded) {
OWSLogVerbose(@"Ignoring update; view has not yet loaded.");
return;
}
[self updateBackButtonUnreadCount]; [self updateBackButtonUnreadCount];
[self updateNavigationBarSubtitleLabel]; [self updateNavigationBarSubtitleLabel];
[self dismissMenuActionsIfNecessary]; [self dismissMenuActionsIfNecessary];
@ -5018,7 +5015,7 @@ typedef enum : NSUInteger {
[strongSelf updateInputToolbarLayout]; [strongSelf updateInputToolbarLayout];
if (self.menuActionsViewController != nil) { if (self.menuActionsViewController != nil) {
[self scrollToFocusInteraction:NO]; [self scrollToMenuActionInteraction:NO];
} else if (lastVisibleIndexPath) { } else if (lastVisibleIndexPath) {
[strongSelf.collectionView scrollToItemAtIndexPath:lastVisibleIndexPath [strongSelf.collectionView scrollToItemAtIndexPath:lastVisibleIndexPath
atScrollPosition:UICollectionViewScrollPositionBottom atScrollPosition:UICollectionViewScrollPositionBottom

@ -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;
@ -82,7 +95,7 @@ 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;

@ -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.
@ -613,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.
@ -655,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];
} }
@ -668,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.
@ -682,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]];
} }
@ -698,10 +727,9 @@ static const int kYapDatabaseRangeMaxLength = 25000;
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;
} }
@ -1510,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;
@ -1661,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

@ -36,7 +36,7 @@ class MenuActionsViewController: UIViewController, MenuActionSheetDelegate {
weak var delegate: MenuActionsViewControllerDelegate? weak var delegate: MenuActionsViewControllerDelegate?
@objc @objc
public let focusedInteraction: TSInteraction? public let focusedInteraction: TSInteraction
private let focusedView: UIView private let focusedView: UIView
private let actionSheetView: MenuActionSheetView private let actionSheetView: MenuActionSheetView
@ -46,7 +46,7 @@ class MenuActionsViewController: UIViewController, MenuActionSheetDelegate {
} }
@objc @objc
required init(focusedInteraction: TSInteraction?, focusedView: UIView, actions: [MenuAction]) { required init(focusedInteraction: TSInteraction, focusedView: UIView, actions: [MenuAction]) {
self.focusedView = focusedView self.focusedView = focusedView
self.focusedInteraction = focusedInteraction self.focusedInteraction = focusedInteraction
@ -180,7 +180,7 @@ class MenuActionsViewController: UIViewController, MenuActionSheetDelegate {
public let vSpacing: CGFloat = 10 public let vSpacing: CGFloat = 10
@objc @objc
public func focusUI() -> UIView { public var focusUI: UIView {
return actionSheetView return actionSheetView
} }

Loading…
Cancel
Save