diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.h b/Signal/src/ViewControllers/ConversationView/ConversationViewController.h index 002a95218..31d7841c1 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.h +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.h @@ -19,7 +19,9 @@ typedef NS_ENUM(NSUInteger, ConversationViewAction) { @property (nonatomic, readonly) TSThread *thread; -- (void)configureForThread:(TSThread *)thread action:(ConversationViewAction)action; +- (void)configureForThread:(TSThread *)thread + action:(ConversationViewAction)action + focusMessageId:(nullable NSString *)focusMessageId; - (void)popKeyBoard; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 861314ec2..55f436262 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -195,6 +195,8 @@ typedef enum : NSUInteger { @property (nonatomic) NSUInteger lastRangeLength; @property (nonatomic) ConversationViewAction actionOnOpen; +@property (nonatomic, nullable) NSString *focusMessageIdOnOpen; + @property (nonatomic) BOOL peek; @property (nonatomic, readonly) OWSContactsManager *contactsManager; @@ -426,11 +428,16 @@ typedef enum : NSUInteger { [self hideInputIfNeeded]; } -- (void)configureForThread:(TSThread *)thread action:(ConversationViewAction)action +- (void)configureForThread:(TSThread *)thread + action:(ConversationViewAction)action + focusMessageId:(nullable NSString *)focusMessageId { + OWSAssert(thread); + _thread = thread; _isGroupConversation = [self.thread isKindOfClass:[TSGroupThread class]]; self.actionOnOpen = action; + self.focusMessageIdOnOpen = focusMessageId; _cellMediaCache = [NSCache new]; // Cache the cell media for ~24 cells. self.cellMediaCache.countLimit = 24; @@ -698,13 +705,43 @@ typedef enum : NSUInteger { return nil; } +- (NSIndexPath *_Nullable)indexPathOfMessageOnOpen +{ + OWSAssert(self.focusMessageIdOnOpen); + OWSAssert(self.dynamicInteractions.focusMessagePosition); + + if (!self.dynamicInteractions.focusMessagePosition) { + // This might happen if the focus message has disappeared + // before this view could appear. + OWSFail(@"%@ focus message has unknown position.", self.logTag); + return nil; + } + NSUInteger focusMessagePosition = self.dynamicInteractions.focusMessagePosition.unsignedIntegerValue; + if (focusMessagePosition >= self.viewItems.count) { + // This might happen if the focus message is outside the maximum + // valid load window size for this view. + OWSFail(@"%@ focus message has invalid position.", self.logTag); + return nil; + } + NSInteger row = (NSInteger)((self.viewItems.count - 1) - focusMessagePosition); + return [NSIndexPath indexPathForRow:row inSection:0]; +} + - (void)scrollToDefaultPosition { if (self.isUserScrolling) { return; } - NSIndexPath *_Nullable indexPath = [self indexPathOfUnreadMessagesIndicator]; + NSIndexPath *_Nullable indexPath = nil; + if (self.focusMessageIdOnOpen) { + indexPath = [self indexPathOfMessageOnOpen]; + } + + if (!indexPath) { + indexPath = [self indexPathOfUnreadMessagesIndicator]; + } + if (indexPath) { if (indexPath.section == 0 && indexPath.row == 0) { [self.collectionView setContentOffset:CGPointZero animated:NO]; @@ -1066,6 +1103,7 @@ typedef enum : NSUInteger { [self startReadTimer]; [self updateNavigationBarSubtitleLabel]; [self updateBackButtonUnreadCount]; + [self autoLoadMoreIfNecessary]; switch (self.actionOnOpen) { case ConversationViewActionNone: @@ -1081,8 +1119,9 @@ typedef enum : NSUInteger { break; } + // Clear the "on open" state after the view has been presented. self.actionOnOpen = ConversationViewActionNone; - + self.focusMessageIdOnOpen = nil; self.isViewCompletelyAppeared = YES; self.viewHasEverAppeared = YES; @@ -1557,7 +1596,7 @@ typedef enum : NSUInteger { // Don’t auto-scroll after “loading more messages” unless we have “more unseen messages”. // // Otherwise, tapping on "load more messages" autoscrolls you downward which is completely wrong. - if (hasEarlierUnseenMessages) { + if (hasEarlierUnseenMessages && !self.focusMessageIdOnOpen) { [self scrollToUnreadIndicatorAnimated]; } } @@ -1634,9 +1673,20 @@ typedef enum : NSUInteger { if (self.lastRangeLength == 0) { // If this is the first time we're configuring the range length, - // try to take into account the position of the unread indicator. + // try to take into account the position of the unread indicator + // and the "focus message". OWSAssert(self.dynamicInteractions); + if (self.focusMessageIdOnOpen) { + OWSAssert(self.dynamicInteractions.focusMessagePosition); + if (self.dynamicInteractions.focusMessagePosition) { + DDLogVerbose(@"%@ ensuring load of focus message: %@", + self.logTag, + self.dynamicInteractions.focusMessagePosition); + rangeLength = MAX(rangeLength, 1 + self.dynamicInteractions.focusMessagePosition.unsignedIntegerValue); + } + } + if (self.dynamicInteractions.unreadIndicatorPosition) { NSUInteger unreadIndicatorPosition = (NSUInteger)[self.dynamicInteractions.unreadIndicatorPosition longValue]; @@ -1649,7 +1699,7 @@ typedef enum : NSUInteger { // We'd like to include at least N seen messages, // to give the user the context of where they left off the conversation. const NSUInteger kPreferredSeenMessageCount = 1; - rangeLength = unreadIndicatorPosition + kPreferredSeenMessageCount; + rangeLength = MAX(rangeLength, unreadIndicatorPosition + kPreferredSeenMessageCount); } } @@ -2473,6 +2523,7 @@ typedef enum : NSUInteger { dbConnection:self.editingDatabaseConnection hideUnreadMessagesIndicator:self.hasClearedUnreadMessagesIndicator firstUnseenInteractionTimestamp:self.dynamicInteractions.firstUnseenInteractionTimestamp + focusMessageId:self.focusMessageIdOnOpen maxRangeSize:maxRangeSize]; } diff --git a/Signal/src/ViewControllers/HomeView/ConversationSearchViewController.swift b/Signal/src/ViewControllers/HomeView/ConversationSearchViewController.swift index 46f5b99b0..a68001c76 100644 --- a/Signal/src/ViewControllers/HomeView/ConversationSearchViewController.swift +++ b/Signal/src/ViewControllers/HomeView/ConversationSearchViewController.swift @@ -79,7 +79,9 @@ class ConversationSearchViewController: UITableViewController { } let thread = searchResult.thread - SignalApp.shared().presentConversation(for: thread.threadRecord, action: .compose) + SignalApp.shared().presentConversation(for: thread.threadRecord, + action: .compose, + focusMessageId: searchResult.messageId) } } diff --git a/Signal/src/ViewControllers/HomeView/HomeViewController.h b/Signal/src/ViewControllers/HomeView/HomeViewController.h index a19c94488..d86824b5b 100644 --- a/Signal/src/ViewControllers/HomeView/HomeViewController.h +++ b/Signal/src/ViewControllers/HomeView/HomeViewController.h @@ -11,6 +11,9 @@ @interface HomeViewController : OWSViewController - (void)presentThread:(TSThread *)thread action:(ConversationViewAction)action; +- (void)presentThread:(TSThread *)thread + action:(ConversationViewAction)action + focusMessageId:(nullable NSString *)focusMessageId; - (void)showNewConversationView; diff --git a/Signal/src/ViewControllers/HomeView/HomeViewController.m b/Signal/src/ViewControllers/HomeView/HomeViewController.m index 5a858829e..fb4d3c048 100644 --- a/Signal/src/ViewControllers/HomeView/HomeViewController.m +++ b/Signal/src/ViewControllers/HomeView/HomeViewController.m @@ -424,7 +424,7 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations ConversationViewController *vc = [ConversationViewController new]; TSThread *thread = [self threadForIndexPath:indexPath]; self.lastThread = thread; - [vc configureForThread:thread action:ConversationViewActionNone]; + [vc configureForThread:thread action:ConversationViewActionNone focusMessageId:nil]; [vc peekSetup]; return vc; @@ -1000,6 +1000,13 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations } - (void)presentThread:(TSThread *)thread action:(ConversationViewAction)action +{ + [self presentThread:thread action:action focusMessageId:nil]; +} + +- (void)presentThread:(TSThread *)thread + action:(ConversationViewAction)action + focusMessageId:(nullable NSString *)focusMessageId { if (thread == nil) { OWSFail(@"Thread unexpectedly nil"); @@ -1008,11 +1015,11 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations // We do this synchronously if we're already on the main thread. DispatchMainThreadSafe(^{ - ConversationViewController *mvc = [ConversationViewController new]; - [mvc configureForThread:thread action:action]; + ConversationViewController *viewController = [ConversationViewController new]; + [viewController configureForThread:thread action:action focusMessageId:focusMessageId]; self.lastThread = thread; - [self pushTopLevelViewController:mvc animateDismissal:YES animatePresentation:YES]; + [self pushTopLevelViewController:viewController animateDismissal:YES animatePresentation:YES]; }); } diff --git a/Signal/src/call/OutboundCallInitiator.swift b/Signal/src/call/OutboundCallInitiator.swift index b3900960f..cc2f49626 100644 --- a/Signal/src/call/OutboundCallInitiator.swift +++ b/Signal/src/call/OutboundCallInitiator.swift @@ -45,10 +45,7 @@ import SignalMessaging isVideo: Bool) -> Bool { // Rather than an init-assigned dependency property, we access `callUIAdapter` via Environment // because it can change after app launch due to user settings - guard let callUIAdapter = SignalApp.shared().callUIAdapter else { - owsFail("\(TAG) can't initiate call because callUIAdapter is nil") - return false - } + let callUIAdapter = SignalApp.shared().callUIAdapter guard let frontmostViewController = UIApplication.shared.frontmostViewController else { owsFail("\(TAG) could not identify frontmostViewController in \(#function)") return false diff --git a/Signal/src/environment/SignalApp.h b/Signal/src/environment/SignalApp.h index 05231e32a..f9277ec49 100644 --- a/Signal/src/environment/SignalApp.h +++ b/Signal/src/environment/SignalApp.h @@ -4,6 +4,8 @@ #import "ConversationViewController.h" +NS_ASSUME_NONNULL_BEGIN + @class AccountManager; @class CallService; @class CallUIAdapter; @@ -17,8 +19,8 @@ @interface SignalApp : NSObject -@property (nonatomic, weak) HomeViewController *homeViewController; -@property (nonatomic, weak) OWSNavigationController *signUpFlowNavigationController; +@property (nonatomic, nullable, weak) HomeViewController *homeViewController; +@property (nonatomic, nullable, weak) OWSNavigationController *signUpFlowNavigationController; // TODO: Convert to singletons? @property (nonatomic, readonly) OWSWebRTCCallMessageHandler *callMessageHandler; @@ -40,6 +42,9 @@ - (void)presentConversationForThreadId:(NSString *)threadId; - (void)presentConversationForThread:(TSThread *)thread; - (void)presentConversationForThread:(TSThread *)thread action:(ConversationViewAction)action; +- (void)presentConversationForThread:(TSThread *)thread + action:(ConversationViewAction)action + focusMessageId:(nullable NSString *)focusMessageId; #pragma mark - Methods @@ -48,3 +53,5 @@ + (void)clearAllNotifications; @end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/environment/SignalApp.m b/Signal/src/environment/SignalApp.m index de9e7a952..4d95b7d8d 100644 --- a/Signal/src/environment/SignalApp.m +++ b/Signal/src/environment/SignalApp.m @@ -13,6 +13,8 @@ #import #import +NS_ASSUME_NONNULL_BEGIN + @interface SignalApp () @property (nonatomic) OWSWebRTCCallMessageHandler *callMessageHandler; @@ -186,6 +188,13 @@ } - (void)presentConversationForThread:(TSThread *)thread action:(ConversationViewAction)action +{ + [self presentConversationForThread:thread action:action focusMessageId:nil]; +} + +- (void)presentConversationForThread:(TSThread *)thread + action:(ConversationViewAction)action + focusMessageId:(nullable NSString *)focusMessageId { OWSAssertIsOnMainThread(); @@ -207,7 +216,7 @@ } } - [self.homeViewController presentThread:thread action:action]; + [self.homeViewController presentThread:thread action:action focusMessageId:focusMessageId]; }); } @@ -248,3 +257,5 @@ } @end + +NS_ASSUME_NONNULL_END diff --git a/SignalMessaging/utils/ConversationSearcher.swift b/SignalMessaging/utils/ConversationSearcher.swift index 32f4bab1d..2c68cc250 100644 --- a/SignalMessaging/utils/ConversationSearcher.swift +++ b/SignalMessaging/utils/ConversationSearcher.swift @@ -7,10 +7,14 @@ import SignalServiceKit public class ConversationSearchResult { public let thread: ThreadViewModel + + public let messageId: String? + public let snippet: String? - init(thread: ThreadViewModel, snippet: String?) { + init(thread: ThreadViewModel, messageId: String?, snippet: String?) { self.thread = thread + self.messageId = messageId self.snippet = snippet } } @@ -71,7 +75,7 @@ public class ConversationSearcher: NSObject { if let thread = match as? TSThread { let threadViewModel = ThreadViewModel(thread: thread, transaction: transaction) let snippet: String? = thread.lastMessageText(transaction: transaction) - let searchResult = ConversationSearchResult(thread: threadViewModel, snippet: snippet) + let searchResult = ConversationSearchResult(thread: threadViewModel, messageId: nil, snippet: snippet) if let contactThread = thread as? TSContactThread { let recipientId = contactThread.contactIdentifier() @@ -82,14 +86,14 @@ public class ConversationSearcher: NSObject { let thread = message.thread(with: transaction) let threadViewModel = ThreadViewModel(thread: thread, transaction: transaction) - let searchResult = ConversationSearchResult(thread: threadViewModel, snippet: snippet) + let searchResult = ConversationSearchResult(thread: threadViewModel, messageId: message.uniqueId, snippet: snippet) messages.append(searchResult) } else if let signalAccount = match as? SignalAccount { let searchResult = ContactSearchResult(signalAccount: signalAccount) contacts.append(searchResult) } else { - Logger.debug("\(self.logTag) in \(#function) unhandled item: \(match)") + owsFail("\(self.logTag) in \(#function) unhandled item: \(match)") } } diff --git a/SignalMessaging/utils/ThreadUtil.h b/SignalMessaging/utils/ThreadUtil.h index 197cc1c67..dde4de03b 100644 --- a/SignalMessaging/utils/ThreadUtil.h +++ b/SignalMessaging/utils/ThreadUtil.h @@ -25,6 +25,15 @@ NS_ASSUME_NONNULL_BEGIN // to include the unread indicator. @property (nonatomic, nullable, readonly) NSNumber *unreadIndicatorPosition; +// Represents the "reverse index" of the focus message, if any. +// The "reverse index" is the distance of this interaction from +// the last interaction in the thread. Therefore the last interaction +// will have a "reverse index" of zero. +// +// We use "reverse indices" because (among other uses) we use this to +// determine the initial load window size. +@property (nonatomic, nullable, readonly) NSNumber *focusMessagePosition; + // If there are unseen messages in the thread, this is the timestamp // of the oldest unseen message. // @@ -105,6 +114,7 @@ NS_ASSUME_NONNULL_BEGIN dbConnection:(YapDatabaseConnection *)dbConnection hideUnreadMessagesIndicator:(BOOL)hideUnreadMessagesIndicator firstUnseenInteractionTimestamp:(nullable NSNumber *)firstUnseenInteractionTimestamp + focusMessageId:(nullable NSString *)focusMessageId maxRangeSize:(int)maxRangeSize; + (BOOL)shouldShowGroupProfileBannerInThread:(TSThread *)thread blockingManager:(OWSBlockingManager *)blockingManager; diff --git a/SignalMessaging/utils/ThreadUtil.m b/SignalMessaging/utils/ThreadUtil.m index c5df4a72a..ad516915a 100644 --- a/SignalMessaging/utils/ThreadUtil.m +++ b/SignalMessaging/utils/ThreadUtil.m @@ -31,6 +31,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, nullable) NSNumber *unreadIndicatorPosition; +@property (nonatomic, nullable) NSNumber *focusMessagePosition; + @property (nonatomic, nullable) NSNumber *firstUnseenInteractionTimestamp; @property (nonatomic) BOOL hasMoreUnseenMessages; @@ -221,6 +223,7 @@ NS_ASSUME_NONNULL_BEGIN hideUnreadMessagesIndicator:(BOOL)hideUnreadMessagesIndicator firstUnseenInteractionTimestamp: (nullable NSNumber *)firstUnseenInteractionTimestampParameter + focusMessageId:(nullable NSString *)focusMessageId maxRangeSize:(int)maxRangeSize { OWSAssert(thread); @@ -615,11 +618,52 @@ NS_ASSUME_NONNULL_BEGIN indicator.timestampForSorting); } } + + // Determine the position of the focus message _after_ performing any mutations + // around dynamic interactions. + if (focusMessageId != nil) { + result.focusMessagePosition = + [self focusMessagePositionForThread:thread transaction:transaction focusMessageId:focusMessageId]; + } }]; return result; } + ++ (nullable NSNumber *)focusMessagePositionForThread:(TSThread *)thread + transaction:(YapDatabaseReadWriteTransaction *)transaction + focusMessageId:(NSString *)focusMessageId +{ + OWSAssert(thread); + OWSAssert(transaction); + OWSAssert(focusMessageId); + + YapDatabaseViewTransaction *databaseView = [transaction ext:TSMessageDatabaseViewExtensionName]; + + NSString *_Nullable group = nil; + NSUInteger index; + BOOL success = + [databaseView getGroup:&group index:&index forKey:focusMessageId inCollection:TSInteraction.collection]; + if (!success) { + // This might happen if the focus message has disappeared + // before this view could appear. + OWSFail(@"%@ failed to find focus message index.", self.logTag); + return nil; + } + if (![group isEqualToString:thread.uniqueId]) { + OWSFail(@"%@ focus message has invalid group.", self.logTag); + return nil; + } + NSUInteger count = [databaseView numberOfItemsInGroup:thread.uniqueId]; + if (index >= count) { + OWSFail(@"%@ focus message has invalid index.", self.logTag); + return nil; + } + NSUInteger position = (count - index) - 1; + return @(position); +} + + (BOOL)shouldShowGroupProfileBannerInThread:(TSThread *)thread blockingManager:(OWSBlockingManager *)blockingManager { OWSAssert(thread);