From b88a99e04713e8bca6baf375ddaa7d167fd8af19 Mon Sep 17 00:00:00 2001 From: Nora Trapp Date: Fri, 18 Oct 2019 17:18:08 -0700 Subject: [PATCH] Keyboard Shortcuts --- Signal/src/Models/ThreadMapping.swift | 32 +++ .../ConversationInputTextView.m | 50 ++-- .../ConversationInputToolbar.h | 2 + .../ConversationInputToolbar.m | 18 ++ .../ConversationViewController.h | 9 + .../ConversationViewController.m | 75 +++++- .../HomeView/ConversationListViewController.h | 9 +- .../HomeView/ConversationListViewController.m | 163 +++++++++++- .../ConversationSplitViewController.swift | 234 +++++++++++++++++- .../ViewControllers/NewGroupViewController.m | 28 +++ .../translations/en.lproj/Localizable.strings | 45 +++- 11 files changed, 620 insertions(+), 45 deletions(-) diff --git a/Signal/src/Models/ThreadMapping.swift b/Signal/src/Models/ThreadMapping.swift index 96901631fc..05401508d4 100644 --- a/Signal/src/Models/ThreadMapping.swift +++ b/Signal/src/Models/ThreadMapping.swift @@ -124,6 +124,38 @@ class ThreadMapping: NSObject { return threads[indexPath.item] } + @objc(indexPathAfterThread:) + func indexPath(after thread: TSThread?) -> IndexPath? { + guard !threads.isEmpty else { return nil } + + let firstIndexPath = IndexPath(item: 0, section: kSection) + + guard let thread = thread else { return firstIndexPath } + guard let index = threads.firstIndex(where: { $0.uniqueId == thread.uniqueId}) else { return firstIndexPath } + + if index < (threads.count - 1) { + return IndexPath(item: index + 1, section: kSection) + } else { + return firstIndexPath + } + } + + @objc(indexPathBeforeThread:) + func indexPath(before thread: TSThread?) -> IndexPath? { + guard !threads.isEmpty else { return nil } + + let lastIndexPath = IndexPath(item: threads.count - 1, section: kSection) + + guard let thread = thread else { return lastIndexPath } + guard let index = threads.firstIndex(where: { $0.uniqueId == thread.uniqueId}) else { return lastIndexPath } + + if index > 0 { + return IndexPath(item: index - 1, section: kSection) + } else { + return lastIndexPath + } + } + let threadFinder = AnyThreadFinder() @objc diff --git a/Signal/src/ViewControllers/ConversationView/ConversationInputTextView.m b/Signal/src/ViewControllers/ConversationView/ConversationInputTextView.m index a471ae48ee..f28cd859e9 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationInputTextView.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationInputTextView.m @@ -226,41 +226,41 @@ NS_ASSUME_NONNULL_BEGIN - (nullable NSArray *)keyCommands { - // We're permissive about what modifier key we accept for the "send message" hotkey. - // We accept command-return, option-return. - // - // We don't support control-return because it doesn't work. - // - // We don't support shift-return because it is often used for "newline" in other - // messaging apps. + // We don't define discoverability title for these key commands as they're + // considered "default" functionality and shouldn't clutter the shortcut + // list that is rendered when you hold down the command key. + return @[ - [self keyCommandWithInput:@"\r" - modifierFlags:UIKeyModifierCommand - action:@selector(modifiedReturnPressed:) - discoverabilityTitle:@"Send Message"], - // "Alternate" is option. - [self keyCommandWithInput:@"\r" - modifierFlags:UIKeyModifierAlternate - action:@selector(modifiedReturnPressed:) - discoverabilityTitle:@"Send Message"], + // An unmodified return can only be sent by a hardware keyboard, + // return on the software keyboard will not trigger this command. + // Return, send message + [UIKeyCommand keyCommandWithInput:@"\r" modifierFlags:0 action:@selector(unmodifedReturnPressed:)], + // Alt + Return, inserts a new line + [UIKeyCommand keyCommandWithInput:@"\r" + modifierFlags:UIKeyModifierAlternate + action:@selector(modifiedReturnPressed:)], + // Shift + Return, inserts a new line + [UIKeyCommand keyCommandWithInput:@"\r" + modifierFlags:UIKeyModifierShift + action:@selector(modifiedReturnPressed:)], ]; } -- (UIKeyCommand *)keyCommandWithInput:(NSString *)input - modifierFlags:(UIKeyModifierFlags)modifierFlags - action:(SEL)action - discoverabilityTitle:(NSString *)discoverabilityTitle +- (void)unmodifedReturnPressed:(UIKeyCommand *)sender { - return [UIKeyCommand keyCommandWithInput:input - modifierFlags:modifierFlags - action:action - discoverabilityTitle:discoverabilityTitle]; + OWSLogInfo(@"unmodifedReturnPressed: %@", sender.input); + + [self.inputTextViewDelegate inputTextViewSendMessagePressed]; } - (void)modifiedReturnPressed:(UIKeyCommand *)sender { OWSLogInfo(@"modifiedReturnPressed: %@", sender.input); - [self.inputTextViewDelegate inputTextViewSendMessagePressed]; + + self.text = [self.text stringByAppendingString:@"\n"]; + + [self.inputTextViewDelegate textViewDidChange:self]; + [self.textViewToolbarDelegate textViewDidChange:self]; } @end diff --git a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h index 5be7a3a4de..0074eccc10 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h +++ b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h @@ -86,6 +86,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)clearTextMessageAnimated:(BOOL)isAnimated; - (void)clearDesiredKeyboard; - (void)toggleDefaultKeyboard; +- (void)showStickerKeyboard; +- (void)showAttachmentKeyboard; - (void)updateFontSizes; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m index 15f0ca8c1f..d9b32b182e 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m @@ -1212,6 +1212,24 @@ const CGFloat kMaxTextViewHeight = 98; } } +- (void)showStickerKeyboard +{ + OWSAssertIsOnMainThread(); + + if (self.desiredKeyboardType != KeyboardType_Sticker) { + [self toggleKeyboardType:KeyboardType_Sticker]; + } +} + +- (void)showAttachmentKeyboard +{ + OWSAssertIsOnMainThread(); + + if (self.desiredKeyboardType != KeyboardType_Attachment) { + [self toggleKeyboardType:KeyboardType_Attachment]; + } +} + #pragma mark - ConversationTextViewToolbarDelegate - (void)setBounds:(CGRect)bounds diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.h b/Signal/src/ViewControllers/ConversationView/ConversationViewController.h index 71f2cdab9e..9a0d6bbf16 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.h +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.h @@ -33,6 +33,15 @@ typedef NS_ENUM(NSUInteger, ConversationViewAction) { - (void)peekSetup; - (void)popped; +#pragma mark - Keyboard Shortcuts + +- (void)showConversationSettings; +- (void)focusInputToolbar; +- (void)openAllMedia; +- (void)openStickerKeyboard; +- (void)openAttachmentKeyboard; +- (void)openGifSearch; + @end #pragma mark - Internal Methods. Used in extensions diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 56e8e2ca91..d8993b33c3 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -1748,7 +1748,42 @@ typedef enum : NSUInteger { settingsVC.conversationSettingsViewDelegate = self; [settingsVC configureWithThread:self.thread]; settingsVC.showVerificationOnAppear = showVerification; - [self.navigationController pushViewController:settingsVC animated:YES]; + + [self.navigationController setViewControllers:[self.viewControllersUpToSelf arrayByAddingObject:settingsVC] + animated:YES]; +} + +- (void)showConversationSettingsAndShowAllMedia +{ + OWSConversationSettingsViewController *settingsVC = [OWSConversationSettingsViewController new]; + settingsVC.conversationSettingsViewDelegate = self; + [settingsVC configureWithThread:self.thread]; + + MediaTileViewController *allMedia = [[MediaTileViewController alloc] initWithThread:self.thread]; + + [self.navigationController + setViewControllers:[self.viewControllersUpToSelf arrayByAddingObjectsFromArray:@[ settingsVC, allMedia ]] + animated:YES]; +} + +- (NSArray *)viewControllersUpToSelf +{ + OWSAssertIsOnMainThread(); + OWSAssertDebug(self.navigationController); + + if (self.navigationController.topViewController == self) { + return self.navigationController.viewControllers; + } + + NSArray *viewControllers = self.navigationController.viewControllers; + NSUInteger index = [viewControllers indexOfObject:self]; + + if (index == NSNotFound) { + OWSFailDebug(@"Unexpectedly missing from view hierarhy"); + return viewControllers; + } + + return [viewControllers subarrayWithRange:NSMakeRange(0, index + 1)]; } #pragma mark - DisappearingTimerConfigurationViewDelegate @@ -5338,6 +5373,44 @@ typedef enum : NSUInteger { } } +#pragma mark - Keyboard Shortcuts + +- (void)focusInputToolbar +{ + OWSAssertIsOnMainThread(); + + [self.inputToolbar clearDesiredKeyboard]; + [self popKeyBoard]; +} + +- (void)openAllMedia +{ + OWSAssertIsOnMainThread(); + + [self showConversationSettingsAndShowAllMedia]; +} + +- (void)openStickerKeyboard +{ + OWSAssertIsOnMainThread(); + + [self.inputToolbar showStickerKeyboard]; +} + +- (void)openAttachmentKeyboard +{ + OWSAssertIsOnMainThread(); + + [self.inputToolbar showAttachmentKeyboard]; +} + +- (void)openGifSearch +{ + OWSAssertIsOnMainThread(); + + [self showGifPicker]; +} + @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/HomeView/ConversationListViewController.h b/Signal/src/ViewControllers/HomeView/ConversationListViewController.h index 816fb9367d..cad4a7d470 100644 --- a/Signal/src/ViewControllers/HomeView/ConversationListViewController.h +++ b/Signal/src/ViewControllers/HomeView/ConversationListViewController.h @@ -25,8 +25,15 @@ typedef NS_ENUM(NSInteger, ConversationListViewControllerSection) { focusMessageId:(nullable NSString *)focusMessageId animated:(BOOL)isAnimated; -// Used by force-touch Springboard icon shortcut +// Used by force-touch Springboard icon shortcut and key commands - (void)showNewConversationView; +- (void)showNewGroupView; +- (void)showAppSettings; +- (void)focusSearch; +- (void)selectPreviousConversation; +- (void)selectNextConversation; +- (void)archiveSelectedConversation; +- (void)unarchiveSelectedConversation; @property (nonatomic) TSThread *lastViewedThread; diff --git a/Signal/src/ViewControllers/HomeView/ConversationListViewController.m b/Signal/src/ViewControllers/HomeView/ConversationListViewController.m index 93e758b97c..00729216df 100644 --- a/Signal/src/ViewControllers/HomeView/ConversationListViewController.m +++ b/Signal/src/ViewControllers/HomeView/ConversationListViewController.m @@ -751,9 +751,7 @@ NSString *const kArchiveButtonPseudoGroup = @"kArchiveButtonPseudoGroup"; OWSAssertDebug(avatarImage); UIButton *avatarButton = [AvatarImageButton buttonWithType:UIButtonTypeCustom]; - [avatarButton addTarget:self - action:@selector(settingsButtonPressed:) - forControlEvents:UIControlEventTouchUpInside]; + [avatarButton addTarget:self action:@selector(showAppSettings) forControlEvents:UIControlEventTouchUpInside]; [avatarButton setImage:avatarImage forState:UIControlStateNormal]; [avatarButton autoSetDimension:ALDimensionWidth toSize:kAvatarSize]; [avatarButton autoSetDimension:ALDimensionHeight toSize:kAvatarSize]; @@ -766,7 +764,7 @@ NSString *const kArchiveButtonPseudoGroup = @"kArchiveButtonPseudoGroup"; settingsButton = [[UIBarButtonItem alloc] initWithImage:image style:UIBarButtonItemStylePlain target:self - action:@selector(settingsButtonPressed:) + action:@selector(showAppSettings) accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"settings")]; } settingsButton.accessibilityLabel = CommonStrings.openSettingsButton; @@ -792,12 +790,6 @@ NSString *const kArchiveButtonPseudoGroup = @"kArchiveButtonPseudoGroup"; } } -- (void)settingsButtonPressed:(id)sender -{ - OWSNavigationController *navigationController = [AppSettingsViewController inModalNavigationController]; - [self presentFormSheetViewController:navigationController animated:YES completion:nil]; -} - - (nullable UIViewController *)previewingContext:(id)previewingContext viewControllerForLocation:(CGPoint)location { @@ -853,6 +845,143 @@ NSString *const kArchiveButtonPseudoGroup = @"kArchiveButtonPseudoGroup"; }]; } +- (void)showNewGroupView +{ + OWSAssertIsOnMainThread(); + + OWSLogInfo(@""); + + NewGroupViewController *viewController = [NewGroupViewController new]; + + [self.contactsManager requestSystemContactsOnceWithCompletion:^(NSError *_Nullable error) { + if (error) { + OWSLogError(@"Error when requesting contacts: %@", error); + } + // Even if there is an error fetching contacts we proceed to the next screen. + // As the compose view will present the proper thing depending on contact access. + // + // We just want to make sure contact access is *complete* before showing the compose + // screen to avoid flicker. + OWSNavigationController *modal = [[OWSNavigationController alloc] initWithRootViewController:viewController]; + [self.navigationController presentFormSheetViewController:modal animated:YES completion:nil]; + }]; +} + +- (void)showAppSettings +{ + OWSAssertIsOnMainThread(); + + OWSLogInfo(@""); + + OWSNavigationController *navigationController = [AppSettingsViewController inModalNavigationController]; + [self presentFormSheetViewController:navigationController animated:YES completion:nil]; +} + +- (void)focusSearch +{ + OWSAssertIsOnMainThread(); + + OWSLogInfo(@""); + + // If we have presented a conversation list (the archive) search there instead. + if (self.presentedConversationListViewController) { + [self.presentedConversationListViewController focusSearch]; + return; + } + + [self.searchBar becomeFirstResponder]; +} + +- (void)selectPreviousConversation +{ + OWSAssertIsOnMainThread(); + + OWSLogInfo(@""); + + // If we have presented a conversation list (the archive) navigate through that instead. + if (self.presentedConversationListViewController) { + [self.presentedConversationListViewController selectPreviousConversation]; + return; + } + + TSThread *_Nullable currentThread = self.conversationSplitViewController.selectedThread; + NSIndexPath *_Nullable previousIndexPath = [self.threadMapping indexPathBeforeThread:currentThread]; + if (previousIndexPath) { + [self presentThread:[self threadForIndexPath:previousIndexPath] action:ConversationViewActionNone animated:YES]; + [self.tableView selectRowAtIndexPath:previousIndexPath + animated:YES + scrollPosition:UITableViewScrollPositionNone]; + } +} + +- (void)selectNextConversation +{ + OWSAssertIsOnMainThread(); + + OWSLogInfo(@""); + + // If we have presented a conversation list (the archive) navigate through that instead. + if (self.presentedConversationListViewController) { + [self.presentedConversationListViewController selectNextConversation]; + return; + } + + TSThread *_Nullable currentThread = self.conversationSplitViewController.selectedThread; + NSIndexPath *_Nullable nextIndexPath = [self.threadMapping indexPathAfterThread:currentThread]; + if (nextIndexPath) { + [self presentThread:[self threadForIndexPath:nextIndexPath] action:ConversationViewActionNone animated:YES]; + [self.tableView selectRowAtIndexPath:nextIndexPath animated:YES scrollPosition:UITableViewScrollPositionNone]; + } +} + +- (void)archiveSelectedConversation +{ + OWSAssertIsOnMainThread(); + + OWSLogInfo(@""); + + TSThread *_Nullable selectedThread = self.conversationSplitViewController.selectedThread; + + if (!selectedThread) { + return; + } + + if (selectedThread.isArchived) { + return; + } + + [self.conversationSplitViewController closeSelectedConversationAnimated:YES]; + + [self.databaseStorage writeWithBlock:^(SDSAnyWriteTransaction *transaction) { + [selectedThread archiveThreadWithTransaction:transaction]; + }]; + [self updateViewState]; +} + +- (void)unarchiveSelectedConversation +{ + OWSAssertIsOnMainThread(); + + OWSLogInfo(@""); + + TSThread *_Nullable selectedThread = self.conversationSplitViewController.selectedThread; + + if (!selectedThread) { + return; + } + + if (!selectedThread.isArchived) { + return; + } + + [self.conversationSplitViewController closeSelectedConversationAnimated:YES]; + + [self.databaseStorage writeWithBlock:^(SDSAnyWriteTransaction *transaction) { + [selectedThread unarchiveThreadWithTransaction:transaction]; + }]; + [self updateViewState]; +} + - (void)showCameraView { [self ows_askForCameraPermissions:^(BOOL cameraGranted) { @@ -1508,6 +1637,20 @@ NSString *const kArchiveButtonPseudoGroup = @"kArchiveButtonPseudoGroup"; [self showViewController:conversationList sender:self]; } +- (nullable ConversationListViewController *)presentedConversationListViewController +{ + UIViewController *_Nullable topViewController = self.navigationController.topViewController; + if (topViewController == self) { + return nil; + } + + if (![topViewController isKindOfClass:[ConversationListViewController class]]) { + return nil; + } + + return (ConversationListViewController *)topViewController; +} + - (NSString *)currentGrouping { switch (self.conversationListMode) { diff --git a/Signal/src/ViewControllers/HomeView/ConversationSplitViewController.swift b/Signal/src/ViewControllers/HomeView/ConversationSplitViewController.swift index 6668fa96c2..994686eb4d 100644 --- a/Signal/src/ViewControllers/HomeView/ConversationSplitViewController.swift +++ b/Signal/src/ViewControllers/HomeView/ConversationSplitViewController.swift @@ -68,11 +68,6 @@ class ConversationSplitViewController: UISplitViewController { return Theme.isDarkThemeEnabled ? .lightContent : .default } - @objc - func showNewConversationView() { - conversationListVC.showNewConversationView() - } - @objc(closeSelectedConversationAnimated:) func closeSelectedConversation(animated: Bool) { guard let selectedConversationViewController = selectedConversationViewController else { return } @@ -153,6 +148,235 @@ class ConversationSplitViewController: UISplitViewController { } currentDetailViewController = vc } + + // MARK: - Keyboard Shortcuts + + override var canBecomeFirstResponder: Bool { + return true + } + + let globalKeyCommands = [ + UIKeyCommand( + input: "n", + modifierFlags: .command, + action: #selector(showNewConversationView), + discoverabilityTitle: NSLocalizedString( + "KEY_COMMAND_NEW_MESSAGE", + comment: "A keyboard command to present the new message dialog." + ) + ), + UIKeyCommand( + input: "g", + modifierFlags: .command, + action: #selector(showNewGroupView), + discoverabilityTitle: NSLocalizedString( + "KEY_COMMAND_NEW_GROUP", + comment: "A keyboard command to present the new group dialog." + ) + ), + UIKeyCommand( + input: ",", + modifierFlags: .command, + action: #selector(showAppSettings), + discoverabilityTitle: NSLocalizedString( + "KEY_COMMAND_SETTINGS", + comment: "A keyboard command to present the application settings dialog." + ) + ), + UIKeyCommand( + input: "f", + modifierFlags: .command, + action: #selector(focusSearch), + discoverabilityTitle: NSLocalizedString( + "KEY_COMMAND_SEARCH", + comment: "A keyboard command to begin a search on the conversation list." + ) + ), + UIKeyCommand( + input: UIKeyCommand.inputUpArrow, + modifierFlags: .alternate, + action: #selector(selectPreviousConversation), + discoverabilityTitle: NSLocalizedString( + "KEY_COMMAND_PREVIOUS_CONVERSATION", + comment: "A keyboard command to jump to the previous conversation in the list." + ) + ), + UIKeyCommand( + input: UIKeyCommand.inputDownArrow, + modifierFlags: .alternate, + action: #selector(selectNextConversation), + discoverabilityTitle: NSLocalizedString( + "KEY_COMMAND_NEXT_CONVERSATION", + comment: "A keyboard command to jump to the next conversation in the list." + ) + ) + ] + + let selectedConversationKeyCommands = [ + UIKeyCommand( + input: "i", + modifierFlags: [.command, .shift], + action: #selector(openConversationSettings), + discoverabilityTitle: NSLocalizedString( + "KEY_COMMAND_CONVERSATION_INFO", + comment: "A keyboard command to open the current conversation's settings." + ) + ), + UIKeyCommand( + input: "m", + modifierFlags: [.command, .shift], + action: #selector(openAllMedia), + discoverabilityTitle: NSLocalizedString( + "KEY_COMMAND_ALL_MEDIA", + comment: "A keyboard command to open the current conversation's all media view." + ) + ), + UIKeyCommand( + input: "g", + modifierFlags: [.command, .shift], + action: #selector(openGifSearch), + discoverabilityTitle: NSLocalizedString( + "KEY_COMMAND_GIF_SEARCH", + comment: "A keyboard command to open the current conversations GIF picker." + ) + ), + UIKeyCommand( + input: "u", + modifierFlags: .command, + action: #selector(openAttachmentKeyboard), + discoverabilityTitle: NSLocalizedString( + "KEY_COMMAND_ATTACHMENTS", + comment: "A keyboard command to open the current conversation's attachment picker." + ) + ), + UIKeyCommand( + input: "s", + modifierFlags: [.command, .shift], + action: #selector(openStickerKeyboard), + discoverabilityTitle: NSLocalizedString( + "KEY_COMMAND_STICKERS", + comment: "A keyboard command to open the current conversation's sticker picker." + ) + ), + UIKeyCommand( + input: "a", + modifierFlags: [.command, .shift], + action: #selector(archiveSelectedConversation), + discoverabilityTitle: NSLocalizedString( + "KEY_COMMAND_ARCHIVE", + comment: "A keyboard command to archive the current coversation." + ) + ), + UIKeyCommand( + input: "u", + modifierFlags: [.command, .shift], + action: #selector(unarchiveSelectedConversation), + discoverabilityTitle: NSLocalizedString( + "KEY_COMMAND_UNARCHIVE", + comment: "A keyboard command to unarchive the current coversation." + ) + ), + UIKeyCommand( + input: "t", + modifierFlags: [.command, .shift], + action: #selector(focusInputToolbar), + discoverabilityTitle: NSLocalizedString( + "KEY_COMMAND_FOCUS_COMPOSER", + comment: "A keyboard command to focus the current conversation's input field." + ) + ) + ] + + override var keyCommands: [UIKeyCommand]? { + // If there is a modal presented over us, or another window above us, don't respond to keyboard commands. + guard presentedViewController == nil || view.window?.isKeyWindow != true else { return nil } + + if selectedThread != nil { + return selectedConversationKeyCommands + globalKeyCommands + } else { + return globalKeyCommands + } + } + + @objc func showNewConversationView() { + conversationListVC.showNewConversationView() + } + + @objc func showNewGroupView() { + conversationListVC.showNewGroupView() + } + + @objc func showAppSettings() { + conversationListVC.showAppSettings() + } + + @objc func focusSearch() { + conversationListVC.focusSearch() + } + + @objc func selectPreviousConversation() { + conversationListVC.selectPreviousConversation() + } + + @objc func selectNextConversation(_ sender: UIKeyCommand) { + conversationListVC.selectNextConversation() + } + + @objc func archiveSelectedConversation() { + conversationListVC.archiveSelectedConversation() + } + + @objc func unarchiveSelectedConversation() { + conversationListVC.unarchiveSelectedConversation() + } + + @objc func openConversationSettings() { + guard let selectedConversationViewController = selectedConversationViewController else { + return owsFailDebug("unexpectedly missing selected conversation") + } + + selectedConversationViewController.showConversationSettings() + } + + @objc func focusInputToolbar() { + guard let selectedConversationViewController = selectedConversationViewController else { + return owsFailDebug("unexpectedly missing selected conversation") + } + + selectedConversationViewController.focusInputToolbar() + } + + @objc func openAllMedia() { + guard let selectedConversationViewController = selectedConversationViewController else { + return owsFailDebug("unexpectedly missing selected conversation") + } + + selectedConversationViewController.openAllMedia() + } + + @objc func openStickerKeyboard() { + guard let selectedConversationViewController = selectedConversationViewController else { + return owsFailDebug("unexpectedly missing selected conversation") + } + + selectedConversationViewController.openStickerKeyboard() + } + + @objc func openAttachmentKeyboard() { + guard let selectedConversationViewController = selectedConversationViewController else { + return owsFailDebug("unexpectedly missing selected conversation") + } + + selectedConversationViewController.openAttachmentKeyboard() + } + + @objc func openGifSearch() { + guard let selectedConversationViewController = selectedConversationViewController else { + return owsFailDebug("unexpectedly missing selected conversation") + } + + selectedConversationViewController.openGifSearch() + } } extension ConversationSplitViewController: UISplitViewControllerDelegate { diff --git a/Signal/src/ViewControllers/NewGroupViewController.m b/Signal/src/ViewControllers/NewGroupViewController.m index 294e745c85..0bc8bfd808 100644 --- a/Signal/src/ViewControllers/NewGroupViewController.m +++ b/Signal/src/ViewControllers/NewGroupViewController.m @@ -241,6 +241,34 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - Methods +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + + if (self.navigationController.viewControllers.count == 1) { + self.navigationItem.leftBarButtonItem = + [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemStop + target:self + action:@selector(dismissPressed)]; + } +} + +- (void)dismissPressed +{ + [self.groupNameTextField resignFirstResponder]; + + if (!self.hasUnsavedChanges) { + // If user made no changes, dismiss. + [self dismissViewControllerAnimated:YES completion:nil]; + return; + } + + __weak NewGroupViewController *weakSelf = self; + [OWSAlerts showPendingChangesAlertWithDiscardAction:^{ + [weakSelf dismissViewControllerAnimated:YES completion:nil]; + }]; +} + - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 2565638a0a..9c6faec541 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -1269,6 +1269,48 @@ /* Slider label when disappearing messages is off */ "KEEP_MESSAGES_FOREVER" = "Messages do not disappear."; +/* A keyboard command to open the current conversation's all media view. */ +"KEY_COMMAND_ALL_MEDIA" = "Go to All Media"; + +/* A keyboard command to archive the current coversation. */ +"KEY_COMMAND_ARCHIVE" = "Archive Conversation"; + +/* A keyboard command to open the current conversation's attachment picker. */ +"KEY_COMMAND_ATTACHMENTS" = "Show Attachments"; + +/* A keyboard command to open the current conversation's settings. */ +"KEY_COMMAND_CONVERSATION_INFO" = "Go to Conversation Info"; + +/* A keyboard command to focus the current conversation's input field. */ +"KEY_COMMAND_FOCUS_COMPOSER" = "Focus Input Bar"; + +/* A keyboard command to open the current conversations GIF picker. */ +"KEY_COMMAND_GIF_SEARCH" = "Go to GIF Search"; + +/* A keyboard command to present the new group dialog. */ +"KEY_COMMAND_NEW_GROUP" = "New Group"; + +/* A keyboard command to present the new message dialog. */ +"KEY_COMMAND_NEW_MESSAGE" = "New Message"; + +/* A keyboard command to jump to the next conversation in the list. */ +"KEY_COMMAND_NEXT_CONVERSATION" = "Go to Next Conversation"; + +/* A keyboard command to jump to the previous conversation in the list. */ +"KEY_COMMAND_PREVIOUS_CONVERSATION" = "Go to Previous Conversation"; + +/* A keyboard command to begin a search on the conversation list. */ +"KEY_COMMAND_SEARCH" = "Search"; + +/* A keyboard command to present the application settings dialog. */ +"KEY_COMMAND_SETTINGS" = "Settings"; + +/* A keyboard command to open the current conversation's sticker picker. */ +"KEY_COMMAND_STICKERS" = "Show Stickers"; + +/* A keyboard command to unarchive the current coversation. */ +"KEY_COMMAND_UNARCHIVE" = "Unarchive Conversation"; + /* Confirmation button within contextual alert */ "LEAVE_BUTTON_TITLE" = "Leave"; @@ -1338,9 +1380,6 @@ /* Confirmation button text to delete selected media message from the gallery */ "MEDIA_GALLERY_DELETE_SINGLE_MESSAGE" = "Delete Message"; -/* embeds {{sender name}} and {{sent datetime}}, e.g. 'Sarah on 10/30/18, 3:29' */ -"MEDIA_GALLERY_LANDSCAPE_TITLE_FORMAT" = "%@ on %@"; - /* Format for the 'more items' indicator for media galleries. Embeds {{the number of additional items}}. */ "MEDIA_GALLERY_MORE_ITEMS_FORMAT" = "+%@";