|
|
|
@ -19,31 +19,25 @@
|
|
|
|
|
NS_ASSUME_NONNULL_BEGIN
|
|
|
|
|
|
|
|
|
|
static void *kConversationInputTextViewObservingContext = &kConversationInputTextViewObservingContext;
|
|
|
|
|
static const CGFloat ConversationInputToolbarBorderViewHeight = 0.5;
|
|
|
|
|
|
|
|
|
|
const CGFloat kMinTextViewHeight = 36;
|
|
|
|
|
const CGFloat kMaxTextViewHeight = 98;
|
|
|
|
|
|
|
|
|
|
#pragma mark -
|
|
|
|
|
|
|
|
|
|
@interface ConversationInputToolbar () <UIGestureRecognizerDelegate,
|
|
|
|
|
ConversationTextViewToolbarDelegate,
|
|
|
|
|
QuotedReplyPreviewDelegate>
|
|
|
|
|
@interface ConversationInputToolbar () <ConversationTextViewToolbarDelegate, QuotedReplyPreviewDelegate>
|
|
|
|
|
|
|
|
|
|
@property (nonatomic, readonly) ConversationStyle *conversationStyle;
|
|
|
|
|
|
|
|
|
|
@property (nonatomic, readonly) UIView *composeContainer;
|
|
|
|
|
@property (nonatomic, readonly) ConversationInputTextView *inputTextView;
|
|
|
|
|
@property (nonatomic, readonly) UIStackView *contentStackView;
|
|
|
|
|
@property (nonatomic, readonly) UIStackView *contentRows;
|
|
|
|
|
@property (nonatomic, readonly) UIStackView *composeRow;
|
|
|
|
|
@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) BOOL shouldShowVoiceMemoButton;
|
|
|
|
|
|
|
|
|
|
@property (nonatomic) NSArray<NSLayoutConstraint *> *contentContraints;
|
|
|
|
|
@property (nonatomic) NSValue *lastTextContentSize;
|
|
|
|
|
@property (nonatomic) CGFloat toolbarHeight;
|
|
|
|
|
@property (nonatomic) CGFloat textViewHeight;
|
|
|
|
|
@property (nonatomic, readonly) NSLayoutConstraint *textViewHeightConstraint;
|
|
|
|
|
|
|
|
|
|
#pragma mark -
|
|
|
|
|
|
|
|
|
@ -81,23 +75,11 @@ static const CGFloat ConversationInputToolbarBorderViewHeight = 0.5;
|
|
|
|
|
return self;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)dealloc
|
|
|
|
|
{
|
|
|
|
|
[self removeKVOObservers];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (CGSize)intrinsicContentSize
|
|
|
|
|
{
|
|
|
|
|
// Since we have `self.autoresizingMask = UIViewAutoresizingFlexibleHeight`, the intrinsicContentSize is used
|
|
|
|
|
// to determine the height of the rendered inputAccessoryView.
|
|
|
|
|
CGFloat height = self.toolbarHeight + ConversationInputToolbarBorderViewHeight;
|
|
|
|
|
if (self.quotedMessagePreview) {
|
|
|
|
|
height += self.quotedMessageTopMargin;
|
|
|
|
|
height += self.quotedMessagePreview.intrinsicContentSize.height;
|
|
|
|
|
}
|
|
|
|
|
CGSize newSize = CGSizeMake(self.bounds.size.width, height);
|
|
|
|
|
|
|
|
|
|
return newSize;
|
|
|
|
|
// Since we have `self.autoresizingMask = UIViewAutoresizingFlexibleHeight`, we must specify
|
|
|
|
|
// an intrinsicContentSize. Specifying CGSize.zero causes the height to be determined by autolayout.
|
|
|
|
|
return CGSizeZero;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)createContents
|
|
|
|
@ -107,10 +89,13 @@ static const CGFloat ConversationInputToolbarBorderViewHeight = 0.5;
|
|
|
|
|
if (UIAccessibilityIsReduceTransparencyEnabled()) {
|
|
|
|
|
self.backgroundColor = [UIColor ows_toolbarBackgroundColor];
|
|
|
|
|
} else {
|
|
|
|
|
// We can mute the blur by making our background color more opaque.
|
|
|
|
|
self.backgroundColor = [[UIColor ows_toolbarBackgroundColor] colorWithAlphaComponent:0.6];
|
|
|
|
|
UIBlurEffect *blurEffect;
|
|
|
|
|
// More muted blur effects look gray when overlayed on white.
|
|
|
|
|
blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
|
|
|
|
|
|
|
|
|
|
// We can make our blur effect more muted by increasing this background alpha.
|
|
|
|
|
self.backgroundColor = [[UIColor ows_toolbarBackgroundColor] colorWithAlphaComponent:0.4f];
|
|
|
|
|
|
|
|
|
|
UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleExtraLight];
|
|
|
|
|
UIVisualEffectView *blurEffectView = [[UIVisualEffectView alloc] initWithEffect:blurEffect];
|
|
|
|
|
[self addSubview:blurEffectView];
|
|
|
|
|
[blurEffectView autoPinEdgesToSuperviewEdges];
|
|
|
|
@ -118,38 +103,13 @@ static const CGFloat ConversationInputToolbarBorderViewHeight = 0.5;
|
|
|
|
|
|
|
|
|
|
self.autoresizingMask = UIViewAutoresizingFlexibleHeight;
|
|
|
|
|
|
|
|
|
|
UIView *borderView = [UIView new];
|
|
|
|
|
borderView.backgroundColor = [UIColor colorWithWhite:238 / 255.f alpha:1.f];
|
|
|
|
|
[self addSubview:borderView];
|
|
|
|
|
[borderView autoPinWidthToSuperview];
|
|
|
|
|
[borderView autoPinEdgeToSuperviewEdge:ALEdgeTop];
|
|
|
|
|
[borderView autoSetDimension:ALDimensionHeight toSize:ConversationInputToolbarBorderViewHeight];
|
|
|
|
|
|
|
|
|
|
_composeContainer = [UIView containerView];
|
|
|
|
|
_contentStackView = [[UIStackView alloc] initWithArrangedSubviews:@[ _composeContainer ]];
|
|
|
|
|
_contentStackView.axis = UILayoutConstraintAxisVertical;
|
|
|
|
|
|
|
|
|
|
[self addSubview:_contentStackView];
|
|
|
|
|
[_contentStackView autoPinEdgesToSuperviewEdges];
|
|
|
|
|
|
|
|
|
|
_inputTextView = [ConversationInputTextView new];
|
|
|
|
|
self.inputTextView.layer.cornerRadius = kMinTextViewHeight / 2.0f;
|
|
|
|
|
self.inputTextView.textViewToolbarDelegate = self;
|
|
|
|
|
self.inputTextView.font = [UIFont ows_dynamicTypeBodyFont];
|
|
|
|
|
[self.composeContainer addSubview:self.inputTextView];
|
|
|
|
|
|
|
|
|
|
// We want to be permissive about taps on the send and attachment buttons,
|
|
|
|
|
// so we use wrapper views that capture nearby taps. This is a lot easier
|
|
|
|
|
// than trying to manipulate the size of the buttons themselves, as you
|
|
|
|
|
// can't coordinate the layout of the button content (e.g. image or text)
|
|
|
|
|
// using iOS auto layout.
|
|
|
|
|
_leftButtonWrapper = [UIView containerView];
|
|
|
|
|
[self.leftButtonWrapper
|
|
|
|
|
addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(leftButtonTapped:)]];
|
|
|
|
|
[self.composeContainer addSubview:self.leftButtonWrapper];
|
|
|
|
|
_rightButtonWrapper = [UIView containerView];
|
|
|
|
|
[self.rightButtonWrapper
|
|
|
|
|
addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(rightButtonTapped:)]];
|
|
|
|
|
[self.composeContainer addSubview:self.rightButtonWrapper];
|
|
|
|
|
[self.inputTextView setContentHuggingHorizontalLow];
|
|
|
|
|
|
|
|
|
|
_textViewHeightConstraint = [self.inputTextView autoSetDimension:ALDimensionHeight toSize:kMinTextViewHeight];
|
|
|
|
|
|
|
|
|
|
_attachmentButton = [[UIButton alloc] init];
|
|
|
|
|
self.attachmentButton.accessibilityLabel
|
|
|
|
@ -159,51 +119,60 @@ static const CGFloat ConversationInputToolbarBorderViewHeight = 0.5;
|
|
|
|
|
[self.attachmentButton addTarget:self
|
|
|
|
|
action:@selector(attachmentButtonPressed)
|
|
|
|
|
forControlEvents:UIControlEventTouchUpInside];
|
|
|
|
|
[self.attachmentButton setImage:[UIImage imageNamed:@"btnAttachments--blue"] forState:UIControlStateNormal];
|
|
|
|
|
self.attachmentButton.contentEdgeInsets = UIEdgeInsetsMake(0, 3, 0, 3);
|
|
|
|
|
[self.leftButtonWrapper addSubview:self.attachmentButton];
|
|
|
|
|
UIImage *attachmentImage = [UIImage imageNamed:@"btnAttachments--blue"];
|
|
|
|
|
[self.attachmentButton setImage:[attachmentImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]
|
|
|
|
|
forState:UIControlStateNormal];
|
|
|
|
|
self.attachmentButton.tintColor = UIColor.ows_navbarIconColor;
|
|
|
|
|
[self.attachmentButton autoSetDimensionsToSize:CGSizeMake(40, kMinTextViewHeight)];
|
|
|
|
|
|
|
|
|
|
// TODO: Fix layout in this class.
|
|
|
|
|
_sendButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
|
|
|
|
[self.sendButton
|
|
|
|
|
setTitle:NSLocalizedString(@"SEND_BUTTON_TITLE", @"Label for the send button in the conversation view.")
|
|
|
|
|
forState:UIControlStateNormal];
|
|
|
|
|
[self.sendButton setTitleColor:[UIColor ows_materialBlueColor] forState:UIControlStateNormal];
|
|
|
|
|
[self.sendButton setTitleColor:UIColor.ows_signalBlueColor forState:UIControlStateNormal];
|
|
|
|
|
self.sendButton.titleLabel.textAlignment = NSTextAlignmentCenter;
|
|
|
|
|
self.sendButton.titleLabel.font = [UIFont ows_mediumFontWithSize:16.f];
|
|
|
|
|
self.sendButton.titleLabel.font = [UIFont ows_mediumFontWithSize:17.f];
|
|
|
|
|
self.sendButton.contentEdgeInsets = UIEdgeInsetsMake(0, 4, 0, 4);
|
|
|
|
|
[self.sendButton autoSetDimension:ALDimensionHeight toSize:kMinTextViewHeight];
|
|
|
|
|
[self.sendButton addTarget:self action:@selector(sendButtonPressed) forControlEvents:UIControlEventTouchUpInside];
|
|
|
|
|
[self.rightButtonWrapper addSubview:self.sendButton];
|
|
|
|
|
|
|
|
|
|
UIImage *voiceMemoIcon = [UIImage imageNamed:@"voice-memo-button"];
|
|
|
|
|
OWSAssert(voiceMemoIcon);
|
|
|
|
|
_voiceMemoButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
|
|
|
|
[self.voiceMemoButton setImage:[voiceMemoIcon imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]
|
|
|
|
|
forState:UIControlStateNormal];
|
|
|
|
|
self.voiceMemoButton.imageView.tintColor = [UIColor ows_materialBlueColor];
|
|
|
|
|
[self.rightButtonWrapper addSubview:self.voiceMemoButton];
|
|
|
|
|
self.voiceMemoButton.imageView.tintColor = UIColor.ows_navbarIconColor;
|
|
|
|
|
[self.voiceMemoButton autoSetDimensionsToSize:CGSizeMake(40, kMinTextViewHeight)];
|
|
|
|
|
|
|
|
|
|
// We want to be permissive about the voice message gesture, so we hang
|
|
|
|
|
// the long press GR on the button's wrapper, not the button itself.
|
|
|
|
|
UILongPressGestureRecognizer *longPressGestureRecognizer =
|
|
|
|
|
[[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
|
|
|
|
|
longPressGestureRecognizer.minimumPressDuration = 0;
|
|
|
|
|
longPressGestureRecognizer.delegate = self;
|
|
|
|
|
[self.rightButtonWrapper addGestureRecognizer:longPressGestureRecognizer];
|
|
|
|
|
[self.voiceMemoButton addGestureRecognizer:longPressGestureRecognizer];
|
|
|
|
|
|
|
|
|
|
self.userInteractionEnabled = YES;
|
|
|
|
|
|
|
|
|
|
[self addKVOObservers];
|
|
|
|
|
_composeRow = [[UIStackView alloc]
|
|
|
|
|
initWithArrangedSubviews:@[ self.attachmentButton, self.inputTextView, self.voiceMemoButton, self.sendButton ]];
|
|
|
|
|
self.composeRow.axis = UILayoutConstraintAxisHorizontal;
|
|
|
|
|
self.composeRow.layoutMarginsRelativeArrangement = YES;
|
|
|
|
|
self.composeRow.layoutMargins = UIEdgeInsetsMake(6, 6, 6, 6);
|
|
|
|
|
self.composeRow.alignment = UIStackViewAlignmentBottom;
|
|
|
|
|
self.composeRow.spacing = 8;
|
|
|
|
|
|
|
|
|
|
_contentRows = [[UIStackView alloc] initWithArrangedSubviews:@[ self.composeRow ]];
|
|
|
|
|
self.contentRows.axis = UILayoutConstraintAxisVertical;
|
|
|
|
|
|
|
|
|
|
[self ensureShouldShowVoiceMemoButton];
|
|
|
|
|
[self addSubview:self.contentRows];
|
|
|
|
|
[self.contentRows autoPinEdgesToSuperviewEdges];
|
|
|
|
|
|
|
|
|
|
[self ensureContentConstraints];
|
|
|
|
|
[self ensureShouldShowVoiceMemoButtonAnimated:NO];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)updateFontSizes
|
|
|
|
|
{
|
|
|
|
|
self.inputTextView.font = [UIFont ows_dynamicTypeBodyFont];
|
|
|
|
|
|
|
|
|
|
[self ensureContentConstraints];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)setInputTextViewDelegate:(id<ConversationInputTextViewDelegate>)value
|
|
|
|
@ -221,18 +190,19 @@ static const CGFloat ConversationInputToolbarBorderViewHeight = 0.5;
|
|
|
|
|
return self.inputTextView.trimmedText;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)setMessageText:(NSString *_Nullable)value
|
|
|
|
|
- (void)setMessageText:(NSString *_Nullable)value animated:(BOOL)isAnimated
|
|
|
|
|
{
|
|
|
|
|
OWSAssert(self.inputTextView);
|
|
|
|
|
|
|
|
|
|
self.inputTextView.text = value;
|
|
|
|
|
|
|
|
|
|
[self ensureShouldShowVoiceMemoButton];
|
|
|
|
|
[self ensureShouldShowVoiceMemoButtonAnimated:isAnimated];
|
|
|
|
|
[self updateHeightWithTextView:self.inputTextView];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)clearTextMessage
|
|
|
|
|
- (void)clearTextMessageAnimated:(BOOL)isAnimated
|
|
|
|
|
{
|
|
|
|
|
[self setMessageText:nil];
|
|
|
|
|
[self setMessageText:nil animated:isAnimated];
|
|
|
|
|
[self.inputTextView.undoManager removeAllActions];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -256,17 +226,6 @@ static const CGFloat ConversationInputToolbarBorderViewHeight = 0.5;
|
|
|
|
|
[self.inputTextView reloadInputViews];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)setShouldShowVoiceMemoButton:(BOOL)shouldShowVoiceMemoButton
|
|
|
|
|
{
|
|
|
|
|
if (_shouldShowVoiceMemoButton == shouldShowVoiceMemoButton) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_shouldShowVoiceMemoButton = shouldShowVoiceMemoButton;
|
|
|
|
|
|
|
|
|
|
[self ensureContentConstraints];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)setQuotedReply:(nullable OWSQuotedReplyModel *)quotedReply
|
|
|
|
|
{
|
|
|
|
|
if (quotedReply == _quotedReply) {
|
|
|
|
@ -294,8 +253,8 @@ static const CGFloat ConversationInputToolbarBorderViewHeight = 0.5;
|
|
|
|
|
[wrapper addSubview:quotedMessagePreview];
|
|
|
|
|
[quotedMessagePreview autoPinToSuperviewMargins];
|
|
|
|
|
|
|
|
|
|
// TODO animate
|
|
|
|
|
[self.contentStackView insertArrangedSubview:wrapper atIndex:0];
|
|
|
|
|
[self.contentRows insertArrangedSubview:wrapper atIndex:0];
|
|
|
|
|
|
|
|
|
|
self.quotedMessagePreview = wrapper;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -306,9 +265,8 @@ static const CGFloat ConversationInputToolbarBorderViewHeight = 0.5;
|
|
|
|
|
|
|
|
|
|
- (void)clearQuotedMessagePreview
|
|
|
|
|
{
|
|
|
|
|
// TODO animate
|
|
|
|
|
if (self.quotedMessagePreview) {
|
|
|
|
|
[self.contentStackView removeArrangedSubview:self.quotedMessagePreview];
|
|
|
|
|
[self.contentRows removeArrangedSubview:self.quotedMessagePreview];
|
|
|
|
|
[self.quotedMessagePreview removeFromSuperview];
|
|
|
|
|
self.quotedMessagePreview = nil;
|
|
|
|
|
}
|
|
|
|
@ -329,106 +287,38 @@ static const CGFloat ConversationInputToolbarBorderViewHeight = 0.5;
|
|
|
|
|
return self.inputTextView.isFirstResponder;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)ensureContentConstraints
|
|
|
|
|
- (void)ensureShouldShowVoiceMemoButtonAnimated:(BOOL)isAnimated
|
|
|
|
|
{
|
|
|
|
|
[NSLayoutConstraint deactivateConstraints:self.contentContraints];
|
|
|
|
|
|
|
|
|
|
const int textViewVInset = 5;
|
|
|
|
|
const int contentHInset = 6;
|
|
|
|
|
const int contentHSpacing = 6;
|
|
|
|
|
|
|
|
|
|
// We want to grow the text input area to fit its content within reason.
|
|
|
|
|
const CGFloat kMinTextViewHeight = ceil(self.inputTextView.font.lineHeight
|
|
|
|
|
+ self.inputTextView.textContainerInset.top + self.inputTextView.textContainerInset.bottom
|
|
|
|
|
+ self.inputTextView.contentInset.top + self.inputTextView.contentInset.bottom);
|
|
|
|
|
// Exactly 4 lines of text with default sizing.
|
|
|
|
|
const CGFloat kMaxTextViewHeight = 98.f;
|
|
|
|
|
const CGFloat textViewDesiredHeight = (self.inputTextView.contentSize.height + self.inputTextView.contentInset.top
|
|
|
|
|
+ self.inputTextView.contentInset.bottom);
|
|
|
|
|
const CGFloat textViewHeight = ceil(CGFloatClamp(textViewDesiredHeight, kMinTextViewHeight, kMaxTextViewHeight));
|
|
|
|
|
const CGFloat kMinContentHeight = kMinTextViewHeight + textViewVInset * 2;
|
|
|
|
|
|
|
|
|
|
self.textViewHeight = textViewHeight;
|
|
|
|
|
self.toolbarHeight = textViewHeight + textViewVInset * 2;
|
|
|
|
|
|
|
|
|
|
self.leftButtonWrapper.hidden = NO;
|
|
|
|
|
self.inputTextView.hidden = NO;
|
|
|
|
|
self.voiceMemoButton.hidden = NO;
|
|
|
|
|
|
|
|
|
|
UIButton *leftButton = self.attachmentButton;
|
|
|
|
|
UIButton *rightButton = (self.shouldShowVoiceMemoButton ? self.voiceMemoButton : self.sendButton);
|
|
|
|
|
UIButton *inactiveRightButton = (self.shouldShowVoiceMemoButton ? self.sendButton : self.voiceMemoButton);
|
|
|
|
|
leftButton.enabled = YES;
|
|
|
|
|
rightButton.enabled = YES;
|
|
|
|
|
inactiveRightButton.enabled = NO;
|
|
|
|
|
leftButton.hidden = NO;
|
|
|
|
|
rightButton.hidden = NO;
|
|
|
|
|
inactiveRightButton.hidden = YES;
|
|
|
|
|
|
|
|
|
|
[leftButton setContentHuggingHigh];
|
|
|
|
|
[rightButton setContentHuggingHigh];
|
|
|
|
|
[leftButton setCompressionResistanceHigh];
|
|
|
|
|
[rightButton setCompressionResistanceHigh];
|
|
|
|
|
[self.inputTextView setCompressionResistanceLow];
|
|
|
|
|
[self.inputTextView setContentHuggingLow];
|
|
|
|
|
|
|
|
|
|
OWSAssert(leftButton.superview == self.leftButtonWrapper);
|
|
|
|
|
OWSAssert(rightButton.superview == self.rightButtonWrapper);
|
|
|
|
|
|
|
|
|
|
// The leading and trailing buttons should be center-aligned with the
|
|
|
|
|
// inputTextView when the inputTextView is at its minimum size.
|
|
|
|
|
//
|
|
|
|
|
// We want the leading and trailing buttons to hug the bottom of the input
|
|
|
|
|
// toolbar as the inputTextView expands.
|
|
|
|
|
//
|
|
|
|
|
// Therefore we fix the button heights to the size of the toolbar when
|
|
|
|
|
// inputTextView is at its minimum size.
|
|
|
|
|
//
|
|
|
|
|
// Additionally, we use "wrapper" views around the leading and trailing
|
|
|
|
|
// buttons to expand their hot area.
|
|
|
|
|
self.contentContraints = @[
|
|
|
|
|
[self.leftButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeLeft],
|
|
|
|
|
[self.leftButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeTop],
|
|
|
|
|
[self.leftButtonWrapper autoPinBottomToSuperviewMarginWithInset:0],
|
|
|
|
|
|
|
|
|
|
[leftButton autoSetDimension:ALDimensionHeight toSize:kMinContentHeight],
|
|
|
|
|
[leftButton autoPinLeadingToSuperviewMarginWithInset:contentHInset],
|
|
|
|
|
[leftButton autoPinTrailingToSuperviewMarginWithInset:contentHSpacing],
|
|
|
|
|
[leftButton autoPinEdgeToSuperviewEdge:ALEdgeBottom],
|
|
|
|
|
|
|
|
|
|
[self.inputTextView autoPinEdge:ALEdgeLeft toEdge:ALEdgeRight ofView:self.leftButtonWrapper],
|
|
|
|
|
[self.inputTextView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:textViewVInset],
|
|
|
|
|
[self.inputTextView autoPinBottomToSuperviewMarginWithInset:textViewVInset],
|
|
|
|
|
[self.inputTextView autoSetDimension:ALDimensionHeight toSize:textViewHeight],
|
|
|
|
|
|
|
|
|
|
[self.rightButtonWrapper autoPinEdge:ALEdgeLeft toEdge:ALEdgeRight ofView:self.inputTextView],
|
|
|
|
|
[self.rightButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeRight],
|
|
|
|
|
[self.rightButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeTop],
|
|
|
|
|
[self.rightButtonWrapper autoPinBottomToSuperviewMarginWithInset:0],
|
|
|
|
|
|
|
|
|
|
[rightButton autoSetDimension:ALDimensionHeight toSize:kMinContentHeight],
|
|
|
|
|
[rightButton autoPinLeadingToSuperviewMarginWithInset:contentHSpacing],
|
|
|
|
|
[rightButton autoPinTrailingToSuperviewMarginWithInset:contentHInset],
|
|
|
|
|
[rightButton autoPinEdgeToSuperviewEdge:ALEdgeBottom]
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// Layout immediately, unless the input toolbar hasn't even been laid out yet.
|
|
|
|
|
if (self.bounds.size.width > 0 && self.bounds.size.height > 0) {
|
|
|
|
|
void (^updateBlock)(void) = ^{
|
|
|
|
|
if (self.inputTextView.trimmedText.length > 0) {
|
|
|
|
|
if (!self.voiceMemoButton.isHidden) {
|
|
|
|
|
self.voiceMemoButton.hidden = YES;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (self.sendButton.isHidden) {
|
|
|
|
|
self.sendButton.hidden = NO;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if (self.voiceMemoButton.isHidden) {
|
|
|
|
|
self.voiceMemoButton.hidden = NO;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!self.sendButton.isHidden) {
|
|
|
|
|
self.sendButton.hidden = YES;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
[self layoutIfNeeded];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
- (void)ensureShouldShowVoiceMemoButton
|
|
|
|
|
{
|
|
|
|
|
self.shouldShowVoiceMemoButton = self.inputTextView.trimmedText.length < 1;
|
|
|
|
|
if (isAnimated) {
|
|
|
|
|
[UIView animateWithDuration:0.1 animations:updateBlock];
|
|
|
|
|
} else {
|
|
|
|
|
updateBlock();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)handleLongPress:(UIGestureRecognizer *)sender
|
|
|
|
|
{
|
|
|
|
|
if (!self.shouldShowVoiceMemoButton) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (sender.state) {
|
|
|
|
|
case UIGestureRecognizerStatePossible:
|
|
|
|
|
case UIGestureRecognizerStateCancelled:
|
|
|
|
@ -481,17 +371,6 @@ static const CGFloat ConversationInputToolbarBorderViewHeight = 0.5;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#pragma mark - UIGestureRecognizerDelegate
|
|
|
|
|
|
|
|
|
|
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
|
|
|
|
|
{
|
|
|
|
|
if ([gestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]]) {
|
|
|
|
|
return self.shouldShowVoiceMemoButton;
|
|
|
|
|
} else {
|
|
|
|
|
return YES;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#pragma mark - Voice Memo
|
|
|
|
|
|
|
|
|
|
- (void)showVoiceMemoUI
|
|
|
|
@ -698,22 +577,6 @@ static const CGFloat ConversationInputToolbarBorderViewHeight = 0.5;
|
|
|
|
|
|
|
|
|
|
#pragma mark - Event Handlers
|
|
|
|
|
|
|
|
|
|
- (void)leftButtonTapped:(UIGestureRecognizer *)sender
|
|
|
|
|
{
|
|
|
|
|
if (sender.state == UIGestureRecognizerStateRecognized) {
|
|
|
|
|
[self attachmentButtonPressed];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)rightButtonTapped:(UIGestureRecognizer *)sender
|
|
|
|
|
{
|
|
|
|
|
if (sender.state == UIGestureRecognizerStateRecognized) {
|
|
|
|
|
if (!self.shouldShowVoiceMemoButton) {
|
|
|
|
|
[self sendButtonPressed];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)sendButtonPressed
|
|
|
|
|
{
|
|
|
|
|
OWSAssert(self.inputToolbarDelegate);
|
|
|
|
@ -730,58 +593,33 @@ static const CGFloat ConversationInputToolbarBorderViewHeight = 0.5;
|
|
|
|
|
|
|
|
|
|
#pragma mark - ConversationTextViewToolbarDelegate
|
|
|
|
|
|
|
|
|
|
- (void)textViewDidChange
|
|
|
|
|
- (void)textViewDidChange:(UITextView *)textView
|
|
|
|
|
{
|
|
|
|
|
OWSAssert(self.inputToolbarDelegate);
|
|
|
|
|
|
|
|
|
|
[self ensureShouldShowVoiceMemoButton];
|
|
|
|
|
[self ensureShouldShowVoiceMemoButtonAnimated:YES];
|
|
|
|
|
[self updateHeightWithTextView:textView];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#pragma mark - Text Input Sizing
|
|
|
|
|
|
|
|
|
|
- (void)addKVOObservers
|
|
|
|
|
- (void)updateHeightWithTextView:(UITextView *)textView
|
|
|
|
|
{
|
|
|
|
|
[self.inputTextView addObserver:self
|
|
|
|
|
forKeyPath:NSStringFromSelector(@selector(contentSize))
|
|
|
|
|
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
|
|
|
|
|
context:kConversationInputTextViewObservingContext];
|
|
|
|
|
}
|
|
|
|
|
// compute new height assuming width is unchanged
|
|
|
|
|
CGSize currentSize = textView.frame.size;
|
|
|
|
|
CGFloat newHeight = [self clampedHeightWithTextView:textView fixedWidth:currentSize.width];
|
|
|
|
|
|
|
|
|
|
- (void)removeKVOObservers
|
|
|
|
|
{
|
|
|
|
|
@try {
|
|
|
|
|
[self.inputTextView removeObserver:self
|
|
|
|
|
forKeyPath:NSStringFromSelector(@selector(contentSize))
|
|
|
|
|
context:kConversationInputTextViewObservingContext];
|
|
|
|
|
} @catch (NSException *__unused exception) {
|
|
|
|
|
// TODO: This try/catch can probably be safely removed.
|
|
|
|
|
OWSFail(@"%@ removeKVOObservers failed.", self.logTag);
|
|
|
|
|
if (newHeight != self.textViewHeight) {
|
|
|
|
|
self.textViewHeight = newHeight;
|
|
|
|
|
OWSAssert(self.textViewHeightConstraint);
|
|
|
|
|
self.textViewHeightConstraint.constant = newHeight;
|
|
|
|
|
[self invalidateIntrinsicContentSize];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)observeValueForKeyPath:(nullable NSString *)keyPath
|
|
|
|
|
ofObject:(nullable id)object
|
|
|
|
|
change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change
|
|
|
|
|
context:(nullable void *)context
|
|
|
|
|
- (CGFloat)clampedHeightWithTextView:(UITextView *)textView fixedWidth:(CGFloat)fixedWidth
|
|
|
|
|
{
|
|
|
|
|
if (context == kConversationInputTextViewObservingContext) {
|
|
|
|
|
|
|
|
|
|
if (object == self.inputTextView && [keyPath isEqualToString:NSStringFromSelector(@selector(contentSize))]) {
|
|
|
|
|
CGSize textContentSize = self.inputTextView.contentSize;
|
|
|
|
|
NSValue *_Nullable lastTextContentSize = self.lastTextContentSize;
|
|
|
|
|
self.lastTextContentSize = [NSValue valueWithCGSize:textContentSize];
|
|
|
|
|
|
|
|
|
|
// Update view constraints, but only when text content size changes.
|
|
|
|
|
//
|
|
|
|
|
// NOTE: We use a "fuzzy equals" comparison to avoid infinite recursion,
|
|
|
|
|
// since ensureContentConstraints can affect the text content size.
|
|
|
|
|
if (!lastTextContentSize || fabs(lastTextContentSize.CGSizeValue.width - textContentSize.width) > 0.1f
|
|
|
|
|
|| fabs(lastTextContentSize.CGSizeValue.height - textContentSize.height) > 0.1f) {
|
|
|
|
|
[self ensureContentConstraints];
|
|
|
|
|
[self invalidateIntrinsicContentSize];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
CGSize fixedWidthSize = CGSizeMake(fixedWidth, CGFLOAT_MAX);
|
|
|
|
|
CGSize contentSize = [textView sizeThatFits:fixedWidthSize];
|
|
|
|
|
|
|
|
|
|
return CGFloatClamp(contentSize.height, kMinTextViewHeight, kMaxTextViewHeight);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#pragma mark QuotedReplyPreviewViewDelegate
|
|
|
|
|