Faster conversation presentation.

There are multiple places in the codebase we present a conversation.

We used to have some very conservative machinery around how this was done, for
fear of failing to present the call view controller, which would have left a
hidden call in the background. We've since addressed that concern more
thoroughly via the separate calling UIWindow.

As such, the remaining presentation machinery is overly complex and inflexible
for what we need.

Sometimes we want to animate-push the conversation. (tap on home, tap on "send message" in contact card/group members)
Sometimes we want to dismiss a modal, to reveal the conversation behind it (contact picker, group creation)
Sometimes we want to present the conversation with no animation (becoming active from a notification)

We also want to ensure that we're never pushing more than one conversation view
controller, which was previously a problem since we were "pushing" a newly
constructed VC in response to these myriad actions. It turned out there were
certain code paths that caused multiple actions to be fired in rapid succession
which pushed multiple ConversationVC's.

The built-in method: `setViewControllers:animated` easily ensures we only have
one ConversationVC on the stack, while being composable enough to faciliate the
various more efficient animations we desire.

The only thing lost with the complex methods is that the naive
`presentViewController:` can fail, e.g. if another view is already presented.
E.g. if an alert appears *just* before the user taps compose, the contact
picker will fail to present.

Since we no longer depend on this for presenting the CallViewController, this
isn't catostrophic, and in fact, arguable preferable, since we want the user to
read and dismiss any alert explicitly.

// FREEBIE
This commit is contained in:
Michael Kirk 2018-08-18 22:54:35 +02:00
parent ae44b316aa
commit 7e8b2e3034
16 changed files with 109 additions and 171 deletions

View File

@ -62,12 +62,12 @@ public class ContactShareViewHelper: NSObject, CNContactViewControllerDelegate {
}
guard phoneNumbers.count > 1 else {
let recipientId = phoneNumbers.first!
SignalApp.shared().presentConversation(forRecipientId: recipientId, action: action)
SignalApp.shared().presentConversation(forRecipientId: recipientId, action: action, animated: true)
return
}
showPhoneNumberPicker(phoneNumbers: phoneNumbers, fromViewController: fromViewController, completion: { (recipientId) in
SignalApp.shared().presentConversation(forRecipientId: recipientId, action: action)
SignalApp.shared().presentConversation(forRecipientId: recipientId, action: action, animated: true)
})
}

View File

@ -544,17 +544,17 @@ class ContactViewController: OWSViewController, ContactShareViewHelperDelegate {
actionSheet.addAction(UIAlertAction(title: NSLocalizedString("ACTION_SEND_MESSAGE",
comment: "Label for 'send message' button in contact view."),
style: .default) { _ in
SignalApp.shared().presentConversation(forRecipientId: e164, action: .compose)
SignalApp.shared().presentConversation(forRecipientId: e164, action: .compose, animated: true)
})
actionSheet.addAction(UIAlertAction(title: NSLocalizedString("ACTION_AUDIO_CALL",
comment: "Label for 'audio call' button in contact view."),
style: .default) { _ in
SignalApp.shared().presentConversation(forRecipientId: e164, action: .audioCall)
SignalApp.shared().presentConversation(forRecipientId: e164, action: .audioCall, animated: true)
})
actionSheet.addAction(UIAlertAction(title: NSLocalizedString("ACTION_VIDEO_CALL",
comment: "Label for 'video call' button in contact view."),
style: .default) { _ in
SignalApp.shared().presentConversation(forRecipientId: e164, action: .videoCall)
SignalApp.shared().presentConversation(forRecipientId: e164, action: .videoCall, animated: true)
})
} else {
// TODO: We could offer callPhoneNumberWithSystemCall.

View File

@ -1133,22 +1133,6 @@ typedef enum : NSUInteger {
[self updateBackButtonUnreadCount];
[self autoLoadMoreIfNecessary];
switch (self.actionOnOpen) {
case ConversationViewActionNone:
break;
case ConversationViewActionCompose:
[self popKeyBoard];
break;
case ConversationViewActionAudioCall:
[self startAudioCall];
break;
case ConversationViewActionVideoCall:
[self startVideoCall];
break;
}
// Clear the "on open" state after the view has been presented.
self.actionOnOpen = ConversationViewActionNone;
self.focusMessageIdOnOpen = nil;
self.isViewCompletelyAppeared = YES;
@ -1173,6 +1157,23 @@ typedef enum : NSUInteger {
[self becomeFirstResponder];
}
}
switch (self.actionOnOpen) {
case ConversationViewActionNone:
break;
case ConversationViewActionCompose:
[self popKeyBoard];
break;
case ConversationViewActionAudioCall:
[self startAudioCall];
break;
case ConversationViewActionVideoCall:
[self startVideoCall];
break;
}
// Clear the "on open" state after the view has been presented.
self.actionOnOpen = ConversationViewActionNone;
}
// `viewWillDisappear` is called whenever the view *starts* to disappear,

View File

@ -1338,7 +1338,7 @@ NS_ASSUME_NONNULL_BEGIN
{
NSString *recipientId = [self unregisteredRecipientId];
TSContactThread *thread = [TSContactThread getOrCreateThreadWithContactId:recipientId];
[SignalApp.sharedApp presentConversationForThread:thread];
[SignalApp.sharedApp presentConversationForThread:thread animated:YES];
}
+ (void)createUnregisteredGroupThread
@ -1356,7 +1356,8 @@ NS_ASSUME_NONNULL_BEGIN
TSGroupModel *model =
[[TSGroupModel alloc] initWithTitle:groupName memberIds:recipientIds image:nil groupId:groupId];
TSGroupThread *thread = [TSGroupThread getOrCreateThreadWithGroupModel:model];
[SignalApp.sharedApp presentConversationForThread:thread];
[SignalApp.sharedApp presentConversationForThread:thread animated:YES];
}
@end

View File

@ -512,7 +512,7 @@ NS_ASSUME_NONNULL_BEGIN
}];
OWSAssert(thread);
[SignalApp.sharedApp presentConversationForThread:thread];
[SignalApp.sharedApp presentConversationForThread:thread animated:YES];
}
@end

View File

@ -137,7 +137,7 @@ class ConversationSearchViewController: UITableViewController {
}
let thread = searchResult.thread
SignalApp.shared().presentConversation(for: thread.threadRecord, action: .compose)
SignalApp.shared().presentConversation(for: thread.threadRecord, action: .compose, animated: true)
case .contacts:
let sectionResults = searchResultSet.contacts
@ -146,7 +146,7 @@ class ConversationSearchViewController: UITableViewController {
return
}
SignalApp.shared().presentConversation(forRecipientId: searchResult.recipientId, action: .compose)
SignalApp.shared().presentConversation(forRecipientId: searchResult.recipientId, action: .compose, animated: true)
case .messages:
let sectionResults = searchResultSet.messages
@ -157,8 +157,9 @@ class ConversationSearchViewController: UITableViewController {
let thread = searchResult.thread
SignalApp.shared().presentConversation(for: thread.threadRecord,
action: .compose,
focusMessageId: searchResult.messageId)
action: .none,
focusMessageId: searchResult.messageId,
animated: true)
}
}

View File

@ -12,20 +12,16 @@ NS_ASSUME_NONNULL_BEGIN
@interface HomeViewController : OWSViewController
- (void)presentThread:(TSThread *)thread action:(ConversationViewAction)action;
- (void)presentThread:(TSThread *)thread action:(ConversationViewAction)action animated:(BOOL)isAnimated;
- (void)presentThread:(TSThread *)thread
action:(ConversationViewAction)action
focusMessageId:(nullable NSString *)focusMessageId;
focusMessageId:(nullable NSString *)focusMessageId
animated:(BOOL)isAnimated;
// Used by force-touch Springboard icon shortcut
- (void)showNewConversationView;
- (void)presentTopLevelModalViewController:(UIViewController *)viewController
animateDismissal:(BOOL)animateDismissal
animatePresentation:(BOOL)animatePresentation;
- (void)pushTopLevelViewController:(UIViewController *)viewController
animateDismissal:(BOOL)animateDismissal
animatePresentation:(BOOL)animatePresentation;
@end
NS_ASSUME_NONNULL_END

View File

@ -554,9 +554,8 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations
//
// We just want to make sure contact access is *complete* before showing the compose
// screen to avoid flicker.
OWSNavigationController *navigationController =
[[OWSNavigationController alloc] initWithRootViewController:viewController];
[self presentTopLevelModalViewController:navigationController animateDismissal:YES animatePresentation:YES];
OWSNavigationController *modal = [[OWSNavigationController alloc] initWithRootViewController:viewController];
[self.navigationController presentViewController:modal animated:YES completion:nil];
}];
}
@ -744,8 +743,6 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations
ExperienceUpgradesPageViewController *experienceUpgradeViewController =
[[ExperienceUpgradesPageViewController alloc] initWithExperienceUpgrades:unseenUpgrades];
[self presentViewController:experienceUpgradeViewController animated:YES completion:nil];
} else if (!self.hasEverAppeared && [ProfileViewController shouldDisplayProfileViewOnLaunch]) {
[ProfileViewController presentForUpgradeOrNag:self];
} else {
[OWSAlerts showIOSUpgradeNagIfNecessary];
}
@ -1156,7 +1153,7 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations
}
case HomeViewControllerSectionConversations: {
TSThread *thread = [self threadForIndexPath:indexPath];
[self presentThread:thread action:ConversationViewActionNone];
[self presentThread:thread action:ConversationViewActionNone animated:YES];
[tableView deselectRowAtIndexPath:indexPath animated:YES];
break;
}
@ -1167,14 +1164,15 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations
}
}
- (void)presentThread:(TSThread *)thread action:(ConversationViewAction)action
- (void)presentThread:(TSThread *)thread action:(ConversationViewAction)action animated:(BOOL)isAnimated
{
[self presentThread:thread action:action focusMessageId:nil];
[self presentThread:thread action:action focusMessageId:nil animated:isAnimated];
}
- (void)presentThread:(TSThread *)thread
action:(ConversationViewAction)action
focusMessageId:(nullable NSString *)focusMessageId
animated:(BOOL)isAnimated
{
if (thread == nil) {
OWSFail(@"Thread unexpectedly nil");
@ -1183,79 +1181,14 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations
// We do this synchronously if we're already on the main thread.
DispatchMainThreadSafe(^{
ConversationViewController *viewController = [ConversationViewController new];
[viewController configureForThread:thread action:action focusMessageId:focusMessageId];
ConversationViewController *conversationVC = [ConversationViewController new];
[conversationVC configureForThread:thread action:action focusMessageId:focusMessageId];
self.lastThread = thread;
[self pushTopLevelViewController:viewController animateDismissal:YES animatePresentation:YES];
[self.navigationController setViewControllers:@[ self, conversationVC ] animated:isAnimated];
});
}
- (void)presentTopLevelModalViewController:(UIViewController *)viewController
animateDismissal:(BOOL)animateDismissal
animatePresentation:(BOOL)animatePresentation
{
OWSAssertIsOnMainThread();
OWSAssert(viewController);
[self presentViewControllerWithBlock:^{
[self presentViewController:viewController animated:animatePresentation completion:nil];
}
animateDismissal:animateDismissal];
}
- (void)pushTopLevelViewController:(UIViewController *)viewController
animateDismissal:(BOOL)animateDismissal
animatePresentation:(BOOL)animatePresentation
{
OWSAssertIsOnMainThread();
OWSAssert(viewController);
[self presentViewControllerWithBlock:^{
[self.navigationController pushViewController:viewController animated:animatePresentation];
}
animateDismissal:animateDismissal];
}
- (void)presentViewControllerWithBlock:(void (^)(void))presentationBlock animateDismissal:(BOOL)animateDismissal
{
OWSAssertIsOnMainThread();
OWSAssert(presentationBlock);
// Presenting a "top level" view controller has three steps:
//
// First, dismiss any presented modal.
// Second, pop to the root view controller if necessary.
// Third present the new view controller using presentationBlock.
// Define a block to perform the second step.
void (^dismissNavigationBlock)(void) = ^{
if (self.navigationController.viewControllers.lastObject != self) {
[CATransaction begin];
[CATransaction setCompletionBlock:^{
presentationBlock();
}];
[self.navigationController popToViewController:self animated:animateDismissal];
[CATransaction commit];
} else {
presentationBlock();
}
};
// Perform the first step.
if (self.presentedViewController) {
if ([self.presentedViewController isKindOfClass:[CallViewController class]]) {
OWSProdInfo([OWSAnalyticsEvents errorCouldNotPresentViewDueToCall]);
return;
}
[self.presentedViewController dismissViewControllerAnimated:animateDismissal completion:dismissNavigationBlock];
} else {
dismissNavigationBlock();
}
}
#pragma mark - Groupings
- (YapDatabaseViewMappings *)threadMappings

View File

@ -823,11 +823,8 @@ NS_ASSUME_NONNULL_BEGIN
- (void)newConversationWithThread:(TSThread *)thread
{
OWSAssert(thread != nil);
[self dismissViewControllerAnimated:YES
completion:^() {
[SignalApp.sharedApp presentConversationForThread:thread
action:ConversationViewActionCompose];
}];
[SignalApp.sharedApp presentConversationForThread:thread action:ConversationViewActionCompose animated:NO];
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}
- (void)showNewGroupView:(id)sender

View File

@ -452,30 +452,24 @@ const NSUInteger kNewGroupViewControllerAvatarWidth = 68;
DDLogError(@"Group creation successful.");
dispatch_async(dispatch_get_main_queue(), ^{
[self dismissViewControllerAnimated:YES
completion:^{
// Pop to new group thread.
[SignalApp.sharedApp presentConversationForThread:thread];
}];
[SignalApp.sharedApp presentConversationForThread:thread action:ConversationViewActionCompose animated:NO];
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
});
};
void (^failureHandler)(NSError *error) = ^(NSError *error) {
DDLogError(@"Group creation failed: %@", error);
dispatch_async(dispatch_get_main_queue(), ^{
[self dismissViewControllerAnimated:YES
completion:^{
// Add an error message to the new group indicating
// that group creation didn't succeed.
[[[TSErrorMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp]
inThread:thread
failedMessageType:TSErrorMessageGroupCreationFailed]
save];
// Add an error message to the new group indicating
// that group creation didn't succeed.
TSErrorMessage *errorMessage = [[TSErrorMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp]
inThread:thread
failedMessageType:TSErrorMessageGroupCreationFailed];
[errorMessage save];
[SignalApp.sharedApp presentConversationForThread:thread];
}];
dispatch_async(dispatch_get_main_queue(), ^{
[SignalApp.sharedApp presentConversationForThread:thread action:ConversationViewActionCompose animated:NO];
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
});
};

View File

@ -16,7 +16,7 @@ NS_ASSUME_NONNULL_BEGIN
+ (void)presentForAppSettings:(UINavigationController *)navigationController;
+ (void)presentForRegistration:(UINavigationController *)navigationController;
+ (void)presentForUpgradeOrNag:(HomeViewController *)presentingController NS_SWIFT_NAME(presentForUpgradeOrNag(from:));
+ (void)presentForUpgradeOrNag:(HomeViewController *)fromViewController NS_SWIFT_NAME(presentForUpgradeOrNag(from:));
@end

View File

@ -558,15 +558,13 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat
[navigationController pushViewController:vc animated:YES];
}
+ (void)presentForUpgradeOrNag:(HomeViewController *)presentingController
+ (void)presentForUpgradeOrNag:(HomeViewController *)fromViewController
{
OWSAssert(presentingController);
OWSAssert(fromViewController);
ProfileViewController *vc = [[ProfileViewController alloc] initWithMode:ProfileViewMode_UpgradeOrNag];
OWSNavigationController *navigationController = [[OWSNavigationController alloc] initWithRootViewController:vc];
[presentingController presentTopLevelModalViewController:navigationController
animateDismissal:YES
animatePresentation:YES];
[fromViewController presentViewController:navigationController animated:YES completion:nil];
}
#pragma mark - AvatarViewHelperDelegate

View File

@ -403,12 +403,16 @@ NS_ASSUME_NONNULL_BEGIN
{
OWSAssert(recipientId.length > 0);
[SignalApp.sharedApp presentConversationForRecipientId:recipientId action:ConversationViewActionCompose];
[SignalApp.sharedApp presentConversationForRecipientId:recipientId
action:ConversationViewActionCompose
animated:YES];
}
- (void)callMember:(NSString *)recipientId
{
[SignalApp.sharedApp presentConversationForRecipientId:recipientId action:ConversationViewActionAudioCall];
[SignalApp.sharedApp presentConversationForRecipientId:recipientId
action:ConversationViewActionAudioCall
animated:YES];
}
- (void)showSafetyNumberView:(NSString *)recipientId

View File

@ -35,16 +35,24 @@ NS_ASSUME_NONNULL_BEGIN
+ (instancetype)sharedApp;
#pragma mark - View Convenience Methods
#pragma mark - Conversation Presentation
- (void)presentConversationForRecipientId:(NSString *)recipientId animated:(BOOL)isAnimated;
- (void)presentConversationForRecipientId:(NSString *)recipientId
action:(ConversationViewAction)action
animated:(BOOL)isAnimated;
- (void)presentConversationForThreadId:(NSString *)threadId animated:(BOOL)isAnimated;
- (void)presentConversationForThread:(TSThread *)thread animated:(BOOL)isAnimated;
- (void)presentConversationForThread:(TSThread *)thread action:(ConversationViewAction)action animated:(BOOL)isAnimated;
- (void)presentConversationForRecipientId:(NSString *)recipientId;
- (void)presentConversationForRecipientId:(NSString *)recipientId action:(ConversationViewAction)action;
- (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;
focusMessageId:(nullable NSString *)focusMessageId
animated:(BOOL)isAnimated;
#pragma mark - Methods

View File

@ -152,24 +152,24 @@ NS_ASSUME_NONNULL_BEGIN
#pragma mark - View Convenience Methods
- (void)presentConversationForRecipientId:(NSString *)recipientId animated:(BOOL)isAnimated
{
[self presentConversationForRecipientId:recipientId action:ConversationViewActionNone animated:(BOOL)isAnimated];
}
- (void)presentConversationForRecipientId:(NSString *)recipientId
action:(ConversationViewAction)action
animated:(BOOL)isAnimated
{
[self presentConversationForRecipientId:recipientId action:ConversationViewActionNone];
__block TSThread *thread = nil;
[OWSPrimaryStorage.dbReadWriteConnection
readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
thread = [TSContactThread getOrCreateThreadWithContactId:recipientId transaction:transaction];
}];
[self presentConversationForThread:thread action:action animated:(BOOL)isAnimated];
}
- (void)presentConversationForRecipientId:(NSString *)recipientId action:(ConversationViewAction)action
{
DispatchMainThreadSafe(^{
__block TSThread *thread = nil;
[OWSPrimaryStorage.dbReadWriteConnection
readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
thread = [TSContactThread getOrCreateThreadWithContactId:recipientId transaction:transaction];
}];
[self presentConversationForThread:thread action:action];
});
}
- (void)presentConversationForThreadId:(NSString *)threadId
- (void)presentConversationForThreadId:(NSString *)threadId animated:(BOOL)isAnimated
{
OWSAssert(threadId.length > 0);
@ -179,22 +179,23 @@ NS_ASSUME_NONNULL_BEGIN
return;
}
[self presentConversationForThread:thread];
[self presentConversationForThread:thread animated:isAnimated];
}
- (void)presentConversationForThread:(TSThread *)thread
- (void)presentConversationForThread:(TSThread *)thread animated:(BOOL)isAnimated
{
[self presentConversationForThread:thread action:ConversationViewActionNone];
[self presentConversationForThread:thread action:ConversationViewActionNone animated:isAnimated];
}
- (void)presentConversationForThread:(TSThread *)thread action:(ConversationViewAction)action
- (void)presentConversationForThread:(TSThread *)thread action:(ConversationViewAction)action animated:(BOOL)isAnimated
{
[self presentConversationForThread:thread action:action focusMessageId:nil];
[self presentConversationForThread:thread action:action focusMessageId:nil animated:isAnimated];
}
- (void)presentConversationForThread:(TSThread *)thread
action:(ConversationViewAction)action
focusMessageId:(nullable NSString *)focusMessageId
animated:(BOOL)isAnimated
{
OWSAssertIsOnMainThread();
@ -216,7 +217,7 @@ NS_ASSUME_NONNULL_BEGIN
}
}
[self.homeViewController presentThread:thread action:action focusMessageId:focusMessageId];
[self.homeViewController presentThread:thread action:action focusMessageId:focusMessageId animated:isAnimated];
});
}

View File

@ -154,7 +154,11 @@ NSString *const Signal_Message_MarkAsRead_Identifier = @"Signal_Message_MarkAsRe
}
self.hasPresentedConversationSinceLastDeactivation = YES;
[SignalApp.sharedApp presentConversationForThreadId:threadId];
// This will happen before the app is visible. By making this animated:NO, the conversation screen
// will be visible to the user immediately upon opening the app, rather than having to watch it animate
// in from the homescreen.
[SignalApp.sharedApp presentConversationForThreadId:threadId animated:NO];
}
- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification