mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			249 lines
		
	
	
		
			7.4 KiB
		
	
	
	
		
			Objective-C
		
	
			
		
		
	
	
			249 lines
		
	
	
		
			7.4 KiB
		
	
	
	
		
			Objective-C
		
	
| //
 | |
| //  Copyright (c) 2019 Open Whisper Systems. All rights reserved.
 | |
| //
 | |
| 
 | |
| #import "ConversationInputTextView.h"
 | |
| #import "Session-Swift.h"
 | |
| #import <SignalUtilitiesKit/NSString+SSK.h>
 | |
| #import <SignalCoreKit/NSString+OWS.h>
 | |
| 
 | |
| NS_ASSUME_NONNULL_BEGIN
 | |
| 
 | |
| @interface ConversationInputTextView () <UITextViewDelegate>
 | |
| 
 | |
| @property (nonatomic) UILabel *placeholderView;
 | |
| @property (nonatomic) NSArray<NSLayoutConstraint *> *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.composeViewTextFieldPlaceholderOpacity];
 | |
|         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<UIKeyCommand *> *)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
 |