// // Copyright (c) 2019 Open Whisper Systems. All rights reserved. // #import "ConversationInputTextView.h" #import "Session-Swift.h" #import #import NS_ASSUME_NONNULL_BEGIN @interface ConversationInputTextView () @property (nonatomic) UILabel *placeholderView; @property (nonatomic) NSArray *placeholderConstraints; @end #pragma mark - @implementation ConversationInputTextView - (instancetype)init { self = [super init]; if (self) { [self setTranslatesAutoresizingMaskIntoConstraints:NO]; self.delegate = self; self.backgroundColor = nil; self.showsHorizontalScrollIndicator = NO; self.showsVerticalScrollIndicator = NO; self.scrollEnabled = YES; self.scrollsToTop = NO; self.userInteractionEnabled = YES; self.font = [UIFont systemFontOfSize:LKValues.mediumFontSize]; self.textColor = LKColors.text; self.textAlignment = NSTextAlignmentNatural; self.tintColor = LKColors.accent; self.contentMode = UIViewContentModeRedraw; self.dataDetectorTypes = UIDataDetectorTypeNone; self.text = nil; self.placeholderView = [UILabel new]; self.placeholderView.text = NSLocalizedString(@"Message", @""); self.placeholderView.textColor = [LKColors.text colorWithAlphaComponent:LKValues.lowOpacity]; self.placeholderView.userInteractionEnabled = NO; [self addSubview:self.placeholderView]; // We need to do these steps _after_ placeholderView is configured. self.font = [UIFont systemFontOfSize:LKValues.mediumFontSize]; CGFloat hMarginLeading = 16.f; CGFloat hMarginTrailing = 16.f; self.textContainerInset = UIEdgeInsetsMake(11.f, CurrentAppContext().isRTL ? hMarginTrailing : hMarginLeading, 11.f, CurrentAppContext().isRTL ? hMarginLeading : hMarginTrailing); self.textContainer.lineFragmentPadding = 0; self.contentInset = UIEdgeInsetsZero; [self ensurePlaceholderConstraints]; [self updatePlaceholderVisibility]; } return self; } #pragma mark - - (void)setFont:(UIFont *_Nullable)font { [super setFont:font]; self.placeholderView.font = font; } - (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)isAnimated { // When creating new lines, contentOffset is animated, but because because // we are simultaneously resizing the text view, this can cause the // text in the textview to be "too high" in the text view. // Solution is to disable animation for setting content offset. [super setContentOffset:contentOffset animated:NO]; } - (void)setContentInset:(UIEdgeInsets)contentInset { [super setContentInset:contentInset]; [self ensurePlaceholderConstraints]; } - (void)setTextContainerInset:(UIEdgeInsets)textContainerInset { [super setTextContainerInset:textContainerInset]; [self ensurePlaceholderConstraints]; } - (void)ensurePlaceholderConstraints { OWSAssertDebug(self.placeholderView); if (self.placeholderConstraints) { [NSLayoutConstraint deactivateConstraints:self.placeholderConstraints]; } // We align the location of our placeholder with the text content of // this view. The only safe way to do that is by measuring the // beginning position. UITextRange *beginningTextRange = [self textRangeFromPosition:self.beginningOfDocument toPosition:self.beginningOfDocument]; CGRect beginningTextRect = [self firstRectForRange:beginningTextRange]; CGFloat topInset = beginningTextRect.origin.y; CGFloat leftInset = beginningTextRect.origin.x; // we use Left instead of Leading, since it's based on the prior CGRect offset self.placeholderConstraints = @[ [self.placeholderView autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:leftInset], [self.placeholderView autoPinEdgeToSuperviewEdge:ALEdgeRight], [self.placeholderView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:topInset], ]; } - (void)updatePlaceholderVisibility { self.placeholderView.hidden = self.text.length > 0; } - (void)setText:(NSString *_Nullable)text { [super setText:text]; [self updatePlaceholderVisibility]; } - (BOOL)canBecomeFirstResponder { return YES; } - (BOOL)pasteboardHasPossibleAttachment { // We don't want to load/convert images more than once so we // only do a cursory validation pass at this time. return ([SignalAttachment pasteboardHasPossibleAttachment] && ![SignalAttachment pasteboardHasText]); } - (BOOL)canPerformAction:(SEL)action withSender:(nullable id)sender { if (action == @selector(paste:)) { if ([self pasteboardHasPossibleAttachment]) { return YES; } } return [super canPerformAction:action withSender:sender]; } - (void)paste:(nullable id)sender { if ([self pasteboardHasPossibleAttachment]) { SignalAttachment *attachment = [SignalAttachment attachmentFromPasteboard]; // Note: attachment might be nil or have an error at this point; that's fine. [self.inputTextViewDelegate didPasteAttachment:attachment]; return; } [super paste:sender]; } - (NSString *)trimmedText { return [self.text ows_stripped]; } - (void)setPlaceholderText:(NSString *)placeholderText { [self.placeholderView setText:placeholderText]; } #pragma mark - UITextViewDelegate - (void)textViewDidChange:(UITextView *)textView { OWSAssertDebug(self.inputTextViewDelegate); OWSAssertDebug(self.textViewToolbarDelegate); [self updatePlaceholderVisibility]; [self.inputTextViewDelegate textViewDidChange:self]; [self.textViewToolbarDelegate textViewDidChange:self]; } - (void)textViewDidChangeSelection:(UITextView *)textView { [self.textViewToolbarDelegate textViewDidChangeSelection:self]; } #pragma mark - Key Commands - (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. 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"], ]; } - (UIKeyCommand *)keyCommandWithInput:(NSString *)input modifierFlags:(UIKeyModifierFlags)modifierFlags action:(SEL)action discoverabilityTitle:(NSString *)discoverabilityTitle { return [UIKeyCommand keyCommandWithInput:input modifierFlags:modifierFlags action:action discoverabilityTitle:discoverabilityTitle]; } - (void)modifiedReturnPressed:(UIKeyCommand *)sender { OWSLogInfo(@"modifiedReturnPressed: %@", sender.input); [self.inputTextViewDelegate inputTextViewSendMessagePressed]; } @end NS_ASSUME_NONNULL_END