From c3087cf3df73da40137e44c7e2b96f361268a605 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 13 Nov 2017 10:41:41 -0500 Subject: [PATCH 1/4] Don't dismiss keyboard when tapping in the conversation view. --- .../ConversationView/Cells/OWSMessageCell.m | 72 ++++++++++++++++++- 1 file changed, 70 insertions(+), 2 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m index 9952dc815..7f665a6e1 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m @@ -24,6 +24,8 @@ NS_ASSUME_NONNULL_BEGIN @end +#pragma mark - + @implementation BubbleMaskingView - (void)setFrame:(CGRect)frame @@ -65,6 +67,26 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - +@interface OWSMessageTextView : UITextView + +@end + +#pragma mark - + +@implementation OWSMessageTextView + +// Our message text views are never used for editing; +// suppress their ability to become first responder +// so that tapping on them doesn't hide keyboard. +- (BOOL)canBecomeFirstResponder +{ + return NO; +} + +@end + +#pragma mark - + @interface OWSMessageCell () // The nullable properties are created as needed. @@ -90,6 +112,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, nullable) NSArray *dateHeaderConstraints; @property (nonatomic, nullable) NSArray *contentConstraints; @property (nonatomic, nullable) NSArray *footerConstraints; +@property (nonatomic) BOOL isPresentingMenuController; @end @@ -132,7 +155,7 @@ NS_ASSUME_NONNULL_BEGIN [self.payloadView addSubview:self.bubbleImageView]; [self.bubbleImageView autoPinToSuperviewEdges]; - self.textView = [UITextView new]; + self.textView = [OWSMessageTextView new]; self.textView.backgroundColor = [UIColor clearColor]; self.textView.opaque = NO; self.textView.editable = NO; @@ -1038,6 +1061,8 @@ NS_ASSUME_NONNULL_BEGIN [self.expirationTimerView clearAnimations]; [self.expirationTimerView removeFromSuperview]; self.expirationTimerView = nil; + + self.isPresentingMenuController = NO; } #pragma mark - Notifications @@ -1062,6 +1087,16 @@ NS_ASSUME_NONNULL_BEGIN } else { [self.expirationTimerView clearAnimations]; } + + [self debugFirstResponder:self depth:0]; +} + +- (void)debugFirstResponder:(UIView *)view depth:(int)depth +{ + DDLogError(@"debugFirstResponder[%@ / %d]: %d", view.class, depth, [view canBecomeFirstResponder]); + for (UIView *subview in view.subviews) { + [self debugFirstResponder:subview depth:depth + 1]; + } } // case TSInfoMessageAdapter: { @@ -1159,6 +1194,11 @@ NS_ASSUME_NONNULL_BEGIN - (void)showMenuController:(CGPoint)fromLocation { + // We don't want taps on messages to hide the keyboard, + // so we only let messages become first responder + // while they are trying to present the menu controller. + self.isPresentingMenuController = YES; + [self becomeFirstResponder]; if ([UIMenuController sharedMenuController].isMenuVisible) { @@ -1208,7 +1248,35 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)canBecomeFirstResponder { - return YES; + return self.isPresentingMenuController; +} + +- (void)didHideMenuController:(NSNotification *)notification +{ + self.isPresentingMenuController = NO; +} + +- (void)setIsPresentingMenuController:(BOOL)isPresentingMenuController +{ + if (_isPresentingMenuController == isPresentingMenuController) { + return; + } + + if (isPresentingMenuController) { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(didHideMenuController:) + name:UIMenuControllerDidHideMenuNotification + object:nil]; + } else { + [[NSNotificationCenter defaultCenter] removeObserver:self + name:UIMenuControllerDidHideMenuNotification + object:nil]; + } +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; } #pragma mark - Logging From c91dda43e0008a882ae6a283ccdaea219630e0b2 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 13 Nov 2017 11:15:58 -0500 Subject: [PATCH 2/4] Disable partial text selection; ignore taps outside links; ignore taps on non-sent messages, link-icy all links. --- .../ConversationView/Cells/OWSMessageCell.m | 71 +++++++++++++++---- 1 file changed, 58 insertions(+), 13 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m index 7f665a6e1..492bf6166 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m @@ -69,6 +69,8 @@ NS_ASSUME_NONNULL_BEGIN @interface OWSMessageTextView : UITextView +@property (nonatomic) BOOL shouldIgnoreEvents; + @end #pragma mark - @@ -83,6 +85,50 @@ NS_ASSUME_NONNULL_BEGIN return NO; } +// Ignore interactions with the text view _except_ taps on links. +// +// We want to disable "partial" selection of text in the message +// and we want to enable "tap to resend" by tapping on a message. +- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *_Nullable)event +{ + if (self.shouldIgnoreEvents) { + // We ignore all events for failed messages so that users + // can tap-to-resend even "all link" messages. + return NO; + } + + // Find the nearest text position to the event. + UITextPosition *_Nullable position = [self closestPositionToPoint:point]; + if (!position) { + return NO; + } + // Find the range of the character in the text which contains the event. + // + // Try every layout direction (this might not be necessary). + UITextRange *_Nullable range = nil; + for (NSNumber *textLayoutDirection in @[ + @(UITextLayoutDirectionLeft), + @(UITextLayoutDirectionRight), + @(UITextLayoutDirectionUp), + @(UITextLayoutDirectionDown), + ]) { + range = [self.tokenizer rangeEnclosingPosition:position + withGranularity:UITextGranularityCharacter + inDirection:(UITextDirection)textLayoutDirection.intValue]; + if (range) { + break; + } + } + if (!range) { + return NO; + } + // Ignore the event unless it occurred inside a link. + NSInteger startIndex = [self offsetFromPosition:self.beginningOfDocument toPosition:range.start]; + BOOL result = + [self.attributedText attribute:NSLinkAttributeName atIndex:(NSUInteger)startIndex effectiveRange:nil] != nil; + return result; +} + @end #pragma mark - @@ -94,7 +140,7 @@ NS_ASSUME_NONNULL_BEGIN // to always keep one around. @property (nonatomic) BubbleMaskingView *payloadView; @property (nonatomic) UILabel *dateHeaderLabel; -@property (nonatomic) UITextView *textView; +@property (nonatomic) OWSMessageTextView *textView; @property (nonatomic, nullable) UIImageView *failedSendBadgeView; @property (nonatomic, nullable) UILabel *tapForMoreLabel; @property (nonatomic, nullable) UIImageView *bubbleImageView; @@ -669,21 +715,20 @@ NS_ASSUME_NONNULL_BEGIN self.textView.textColor = textColor; // Honor dynamic type in the message bodies. self.textView.font = [self textMessageFont]; + self.textView.linkTextAttributes = @{ + NSForegroundColorAttributeName : textColor, + NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle | NSUnderlinePatternSolid) + }; + self.textView.dataDetectorTypes + = (UIDataDetectorTypeLink | UIDataDetectorTypeAddress | UIDataDetectorTypeCalendarEvent); - // Don't link outgoing messages that haven't been sent yet, as - // this interferes with "tap to retry". - BOOL canLinkify = YES; if (self.viewItem.interaction.interactionType == OWSInteractionType_OutgoingMessage) { + // Ignore taps on links in outgoing messages that haven't been sent yet, as + // this interferes with "tap to retry". TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.viewItem.interaction; - canLinkify = outgoingMessage.messageState == TSOutgoingMessageStateSentToService; - } - if (canLinkify) { - self.textView.linkTextAttributes = @{ - NSForegroundColorAttributeName : textColor, - NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle | NSUnderlinePatternSolid) - }; - self.textView.dataDetectorTypes - = (UIDataDetectorTypeLink | UIDataDetectorTypeAddress | UIDataDetectorTypeCalendarEvent); + self.textView.shouldIgnoreEvents = outgoingMessage.messageState != TSOutgoingMessageStateSentToService; + } else { + self.textView.shouldIgnoreEvents = NO; } if (self.displayableText.isTextTruncated) { From 3da1d8c63fa2a12c86f3ead8ac69eed8b7562f31 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 13 Nov 2017 11:18:38 -0500 Subject: [PATCH 3/4] Disable partial text selection; ignore taps outside links; ignore taps on non-sent messages, link-icy all links. --- .../ConversationView/Cells/OWSMessageCell.m | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m index 492bf6166..32644dd3f 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m @@ -1132,16 +1132,6 @@ NS_ASSUME_NONNULL_BEGIN } else { [self.expirationTimerView clearAnimations]; } - - [self debugFirstResponder:self depth:0]; -} - -- (void)debugFirstResponder:(UIView *)view depth:(int)depth -{ - DDLogError(@"debugFirstResponder[%@ / %d]: %d", view.class, depth, [view canBecomeFirstResponder]); - for (UIView *subview in view.subviews) { - [self debugFirstResponder:subview depth:depth + 1]; - } } // case TSInfoMessageAdapter: { From c3b6c9055e286142e0d13d5ecde860bf9ede5419 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 13 Nov 2017 18:46:41 -0500 Subject: [PATCH 4/4] Disable partial text selection; ignore taps outside links; ignore taps on non-sent messages, link-icy all links. --- .../src/ViewControllers/ConversationView/Cells/OWSMessageCell.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m index 32644dd3f..61db6eecc 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m @@ -1297,6 +1297,8 @@ NS_ASSUME_NONNULL_BEGIN return; } + _isPresentingMenuController = isPresentingMenuController; + if (isPresentingMenuController) { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didHideMenuController:)