parent
7d3df0bf0a
commit
4a94d039e8
|
@ -18,10 +18,22 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
#pragma mark -
|
||||
|
||||
@protocol ConversationTextViewToolbarDelegate <NSObject>
|
||||
|
||||
- (void)textViewDidChange;
|
||||
|
||||
- (void)textViewReturnPressed;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface ConversationInputTextView : UITextView
|
||||
|
||||
@property (weak, nonatomic) id<ConversationInputTextViewDelegate> inputTextViewDelegate;
|
||||
|
||||
@property (weak, nonatomic) id<ConversationTextViewToolbarDelegate> textViewToolbarDelegate;
|
||||
|
||||
- (NSString *)trimmedText;
|
||||
|
||||
@end
|
||||
|
|
|
@ -8,6 +8,16 @@
|
|||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface ConversationInputTextView () <UITextViewDelegate>
|
||||
|
||||
@property (nonatomic) UILabel *placeholderView;
|
||||
@property (nonatomic) NSArray<NSLayoutConstraint *> *placeholderConstraints;
|
||||
@property (nonatomic) BOOL isEditing;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation ConversationInputTextView
|
||||
|
||||
- (instancetype)init
|
||||
|
@ -16,9 +26,10 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
if (self) {
|
||||
[self setTranslatesAutoresizingMaskIntoConstraints:NO];
|
||||
|
||||
self.delegate = self;
|
||||
|
||||
CGFloat cornerRadius = 6.0f;
|
||||
|
||||
self.font = [UIFont ows_dynamicTypeBodyFont];
|
||||
self.backgroundColor = [UIColor whiteColor];
|
||||
self.layer.borderColor = [UIColor lightGrayColor].CGColor;
|
||||
self.layer.borderWidth = 0.5f;
|
||||
|
@ -26,9 +37,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
self.scrollIndicatorInsets = UIEdgeInsetsMake(cornerRadius, 0.0f, cornerRadius, 0.0f);
|
||||
|
||||
self.textContainerInset = UIEdgeInsetsMake(4.0f, 2.0f, 4.0f, 2.0f);
|
||||
self.contentInset = UIEdgeInsetsMake(1.0f, 0.0f, 1.0f, 0.0f);
|
||||
|
||||
self.scrollEnabled = YES;
|
||||
self.scrollsToTop = NO;
|
||||
self.userInteractionEnabled = YES;
|
||||
|
@ -45,13 +53,89 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
self.text = nil;
|
||||
|
||||
// _placeHolder = nil;
|
||||
// _placeHolderTextColor = [UIColor lightGrayColor];
|
||||
self.placeholderView = [UILabel new];
|
||||
self.placeholderView.text = NSLocalizedString(@"new_message", @"");
|
||||
self.placeholderView.textColor = [UIColor lightGrayColor];
|
||||
self.placeholderView.textAlignment = NSTextAlignmentLeft;
|
||||
[self addSubview:self.placeholderView];
|
||||
|
||||
// We need to do these steps _after_ placeholderView is configured.
|
||||
self.font = [UIFont ows_dynamicTypeBodyFont];
|
||||
self.textContainerInset = UIEdgeInsetsMake(4.0f, 2.0f, 4.0f, 2.0f);
|
||||
self.contentInset = UIEdgeInsetsMake(1.0f, 0.0f, 1.0f, 0.0f);
|
||||
|
||||
[self ensurePlaceholderConstraints];
|
||||
[self updatePlaceholderVisibility];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setFont:(UIFont *_Nullable)font
|
||||
{
|
||||
[super setFont:font];
|
||||
|
||||
self.placeholderView.font = font;
|
||||
}
|
||||
|
||||
- (void)setContentInset:(UIEdgeInsets)contentInset
|
||||
{
|
||||
[super setContentInset:contentInset];
|
||||
|
||||
[self ensurePlaceholderConstraints];
|
||||
}
|
||||
|
||||
- (void)setTextContainerInset:(UIEdgeInsets)textContainerInset
|
||||
{
|
||||
[super setTextContainerInset:textContainerInset];
|
||||
|
||||
[self ensurePlaceholderConstraints];
|
||||
}
|
||||
|
||||
- (void)ensurePlaceholderConstraints
|
||||
{
|
||||
OWSAssert(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 hInset = beginningTextRect.origin.x;
|
||||
CGFloat topInset = beginningTextRect.origin.y;
|
||||
|
||||
self.placeholderConstraints = @[
|
||||
[self.placeholderView autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:hInset],
|
||||
[self.placeholderView autoPinEdgeToSuperviewEdge:ALEdgeRight withInset:hInset],
|
||||
[self.placeholderView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:topInset],
|
||||
];
|
||||
}
|
||||
|
||||
- (void)updatePlaceholderVisibility
|
||||
{
|
||||
self.placeholderView.hidden = self.text.length > 0 || self.isEditing;
|
||||
}
|
||||
|
||||
- (void)setText:(NSString *_Nullable)text
|
||||
{
|
||||
[super setText:text];
|
||||
|
||||
[self updatePlaceholderVisibility];
|
||||
}
|
||||
|
||||
- (void)setIsEditing:(BOOL)isEditing
|
||||
{
|
||||
_isEditing = isEditing;
|
||||
|
||||
[self updatePlaceholderVisibility];
|
||||
}
|
||||
|
||||
- (BOOL)canBecomeFirstResponder
|
||||
{
|
||||
return YES;
|
||||
|
@ -246,6 +330,47 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
//}
|
||||
//@end
|
||||
|
||||
#pragma mark - UITextViewDelegate
|
||||
|
||||
- (void)textViewDidBeginEditing:(UITextView *)textView
|
||||
{
|
||||
// TODO: Is this necessary?
|
||||
|
||||
[textView becomeFirstResponder];
|
||||
|
||||
self.isEditing = YES;
|
||||
}
|
||||
|
||||
- (void)textViewDidChange:(UITextView *)textView
|
||||
{
|
||||
OWSAssert(self.textViewToolbarDelegate);
|
||||
|
||||
[self updatePlaceholderVisibility];
|
||||
|
||||
[self.textViewToolbarDelegate textViewDidChange];
|
||||
}
|
||||
|
||||
- (void)textViewDidEndEditing:(UITextView *)textView
|
||||
{
|
||||
[textView resignFirstResponder];
|
||||
|
||||
self.isEditing = NO;
|
||||
}
|
||||
|
||||
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
|
||||
{
|
||||
OWSAssert(self.textViewToolbarDelegate);
|
||||
|
||||
if (range.length > 0) {
|
||||
return YES;
|
||||
}
|
||||
if ([text isEqualToString:@"\n"]) {
|
||||
[self.textViewToolbarDelegate textViewReturnPressed];
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -20,9 +20,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
- (void)textViewDidChange;
|
||||
|
||||
// TODO: Is this necessary.
|
||||
//- (void)textViewDidBeginEditing;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
|
|
@ -14,25 +14,26 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
static void *kConversationInputTextViewObservingContext = &kConversationInputTextViewObservingContext;
|
||||
|
||||
@interface ConversationInputToolbar () <UIGestureRecognizerDelegate, UITextViewDelegate>
|
||||
@interface ConversationInputToolbar () <UIGestureRecognizerDelegate, ConversationTextViewToolbarDelegate>
|
||||
|
||||
@property (nonatomic, readonly) ConversationInputTextView *inputTextView;
|
||||
@property (nonatomic, readonly) UIButton *attachmentButton;
|
||||
@property (nonatomic, readonly) UIButton *sendButton;
|
||||
@property (nonatomic, readonly) UIButton *voiceMemoButton;
|
||||
@property (nonatomic, readonly) UIView *leftButtonWrapper;
|
||||
@property (nonatomic, readonly) UIView *rightButtonWrapper;
|
||||
|
||||
@property (nonatomic) ConversationInputTextView *inputTextView;
|
||||
@property (nonatomic) UIButton *attachmentButton;
|
||||
@property (nonatomic) UIButton *sendButton;
|
||||
@property (nonatomic) BOOL shouldShowVoiceMemoButton;
|
||||
@property (nonatomic) UIButton *voiceMemoButton;
|
||||
@property (nonatomic) UIView *leftButtonWrapper;
|
||||
@property (nonatomic) UIView *rightButtonWrapper;
|
||||
|
||||
@property (nonatomic) NSArray<NSLayoutConstraint *> *contentContraints;
|
||||
|
||||
#pragma mark - Voice Memo Recording UI
|
||||
|
||||
@property (nonatomic, nullable) UIView *voiceMemoUI;
|
||||
@property (nonatomic) UIView *voiceMemoContentView;
|
||||
@property (nonatomic, nullable) UIView *voiceMemoContentView;
|
||||
@property (nonatomic) NSDate *voiceMemoStartTime;
|
||||
@property (nonatomic, nullable) NSTimer *voiceMemoUpdateTimer;
|
||||
@property (nonatomic) UILabel *recordingLabel;
|
||||
@property (nonatomic, nullable) UILabel *recordingLabel;
|
||||
@property (nonatomic) BOOL isRecordingVoiceMemo;
|
||||
@property (nonatomic) CGPoint voiceMemoGestureStartLocation;
|
||||
|
||||
|
@ -69,7 +70,7 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex
|
|||
[backgroundView autoPinEdgesToSuperviewEdges];
|
||||
|
||||
_inputTextView = [ConversationInputTextView new];
|
||||
self.inputTextView.delegate = self;
|
||||
self.inputTextView.textViewToolbarDelegate = self;
|
||||
[self addSubview:self.inputTextView];
|
||||
|
||||
// We want to be permissive about taps on the send and attachment buttons,
|
||||
|
@ -111,7 +112,7 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex
|
|||
|
||||
UIImage *voiceMemoIcon = [UIImage imageNamed:@"voice-memo-button"];
|
||||
OWSAssert(voiceMemoIcon);
|
||||
self.voiceMemoButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
_voiceMemoButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
[self.voiceMemoButton setImage:[voiceMemoIcon imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]
|
||||
forState:UIControlStateNormal];
|
||||
self.voiceMemoButton.imageView.tintColor = [UIColor ows_materialBlueColor];
|
||||
|
@ -157,7 +158,7 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex
|
|||
|
||||
[self ensureShouldShowVoiceMemoButton];
|
||||
// TODO: Remove this when we remove the delegate method.
|
||||
[self textViewDidChange:self.inputTextView];
|
||||
[self textViewDidChange];
|
||||
}
|
||||
|
||||
- (void)clearTextMessage
|
||||
|
@ -495,6 +496,8 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex
|
|||
|
||||
UIView *oldVoiceMemoUI = self.voiceMemoUI;
|
||||
self.voiceMemoUI = nil;
|
||||
self.voiceMemoContentView = nil;
|
||||
self.recordingLabel = nil;
|
||||
NSTimer *voiceMemoUpdateTimer = self.voiceMemoUpdateTimer;
|
||||
self.voiceMemoUpdateTimer = nil;
|
||||
|
||||
|
@ -572,41 +575,19 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex
|
|||
[self.inputToolbarDelegate attachmentButtonPressed];
|
||||
}
|
||||
|
||||
#pragma mark - UITextViewDelegate
|
||||
#pragma mark - ConversationTextViewToolbarDelegate
|
||||
|
||||
- (void)textViewDidBeginEditing:(UITextView *)textView
|
||||
{
|
||||
OWSAssert(textView == self.inputTextView);
|
||||
|
||||
[textView becomeFirstResponder];
|
||||
}
|
||||
|
||||
- (void)textViewDidChange:(UITextView *)textView
|
||||
- (void)textViewDidChange
|
||||
{
|
||||
OWSAssert(self.inputToolbarDelegate);
|
||||
OWSAssert(textView == self.inputTextView);
|
||||
|
||||
[self ensureShouldShowVoiceMemoButton];
|
||||
[self.inputToolbarDelegate textViewDidChange];
|
||||
}
|
||||
|
||||
- (void)textViewDidEndEditing:(UITextView *)textView
|
||||
- (void)textViewReturnPressed
|
||||
{
|
||||
OWSAssert(textView == self.inputTextView);
|
||||
|
||||
[textView resignFirstResponder];
|
||||
}
|
||||
|
||||
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
|
||||
{
|
||||
if (range.length > 0) {
|
||||
return YES;
|
||||
}
|
||||
if ([text isEqualToString:@"\n"]) {
|
||||
[self sendButtonPressed];
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
[self sendButtonPressed];
|
||||
}
|
||||
|
||||
#pragma mark - Text Input Sizing
|
||||
|
|
|
@ -3491,6 +3491,8 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
|
|||
message:errorMessage];
|
||||
}
|
||||
|
||||
// TODO: Is this necessary? It seems redundant with observing changes to
|
||||
// the collection view's layout.
|
||||
- (void)textViewDidChangeLayout
|
||||
{
|
||||
OWSAssert([NSThread isMainThread]);
|
||||
|
|
Loading…
Reference in New Issue