2017-10-10 22:13:54 +02:00
//
2019-01-11 15:24:24 +01:00
// Copyright ( c ) 2019 Open Whisper Systems . All rights reserved .
2017-10-10 22:13:54 +02:00
//
# import "ConversationInputToolbar.h"
# import "ConversationInputTextView.h"
2018-02-07 18:44:09 +01:00
# import "Environment.h"
2017-10-24 15:41:03 +02:00
# import "OWSMath.h"
2019-05-02 23:58:48 +02:00
# import "Session-Swift.h"
2017-10-10 22:13:54 +02:00
# import "UIColor+OWS.h"
# import "UIFont+OWS.h"
2019-01-24 19:06:22 +01:00
# import < PromiseKit / AnyPromise . h >
2020-11-11 07:45:50 +01:00
# import < SignalUtilitiesKit / OWSFormat . h >
# import < SignalUtilitiesKit / SignalUtilitiesKit - Swift . h >
2020-11-26 00:37:56 +01:00
# import < SessionUtilitiesKit / UIView + OWS . h >
# import < SessionMessagingKit / TSQuotedMessage . h >
2017-10-10 22:13:54 +02:00
NS_ASSUME _NONNULL _BEGIN
2019-02-06 06:36:03 +01:00
typedef NS_CLOSED _ENUM ( NSUInteger , VoiceMemoRecordingState ) {
VoiceMemoRecordingState_Idle ,
VoiceMemoRecordingState_RecordingHeld ,
VoiceMemoRecordingState_RecordingLocked
} ;
2017-10-13 17:17:55 +02:00
static void * kConversationInputTextViewObservingContext = & kConversationInputTextViewObservingContext ;
2018-02-07 18:44:09 +01:00
2019-12-09 06:47:13 +01:00
const CGFloat kMinTextViewHeight = 40 ;
const CGFloat kMaxTextViewHeight = 120 ;
2018-07-03 04:04:15 +02:00
2018-02-07 18:44:09 +01:00
# pragma mark -
2019-01-17 17:56:52 +01:00
@ interface InputLinkPreview : NSObject
@ property ( nonatomic ) NSString * previewUrl ;
@ property ( nonatomic , nullable ) OWSLinkPreviewDraft * linkPreviewDraft ;
@ end
# pragma mark -
@ implementation InputLinkPreview
@ end
# pragma mark -
@ interface ConversationInputToolbar ( ) < ConversationTextViewToolbarDelegate ,
QuotedReplyPreviewDelegate ,
2019-10-09 05:46:21 +02:00
LinkPreviewViewDraftDelegate ,
2019-10-11 06:52:56 +02:00
LKMentionCandidateSelectionViewDelegate >
2017-10-17 16:07:57 +02:00
2018-06-28 19:28:14 +02:00
@ property ( nonatomic , readonly ) ConversationStyle * conversationStyle ;
2017-10-17 16:07:57 +02:00
@ property ( nonatomic , readonly ) ConversationInputTextView * inputTextView ;
2019-01-22 19:13:54 +01:00
@ property ( nonatomic , readonly ) UIStackView * hStack ;
2017-10-17 16:07:57 +02:00
@ property ( nonatomic , readonly ) UIButton * attachmentButton ;
@ property ( nonatomic , readonly ) UIButton * sendButton ;
@ property ( nonatomic , readonly ) UIButton * voiceMemoButton ;
2019-01-22 19:13:54 +01:00
@ property ( nonatomic , readonly ) UIView * quotedReplyWrapper ;
@ property ( nonatomic , readonly ) UIView * linkPreviewWrapper ;
2019-09-06 08:30:40 +02:00
@ property ( nonatomic , readonly ) UIView * borderView ;
2017-10-10 22:13:54 +02:00
2018-01-02 22:55:40 +01:00
@ property ( nonatomic ) CGFloat textViewHeight ;
2018-07-03 04:04:15 +02:00
@ property ( nonatomic , readonly ) NSLayoutConstraint * textViewHeightConstraint ;
2017-10-10 22:13:54 +02:00
# pragma mark - Voice Memo Recording UI
@ property ( nonatomic , nullable ) UIView * voiceMemoUI ;
2019-02-06 06:36:03 +01:00
@ property ( nonatomic , nullable ) VoiceMemoLockView * voiceMemoLockView ;
2017-10-17 16:07:57 +02:00
@ property ( nonatomic , nullable ) UIView * voiceMemoContentView ;
2017-10-10 22:13:54 +02:00
@ property ( nonatomic ) NSDate * voiceMemoStartTime ;
@ property ( nonatomic , nullable ) NSTimer * voiceMemoUpdateTimer ;
2019-02-06 06:36:03 +01:00
@ property ( nonatomic ) UIGestureRecognizer * voiceMemoGestureRecognizer ;
@ property ( nonatomic , nullable ) UILabel * voiceMemoCancelLabel ;
@ property ( nonatomic , nullable ) UIView * voiceMemoRedRecordingCircle ;
2017-10-17 16:07:57 +02:00
@ property ( nonatomic , nullable ) UILabel * recordingLabel ;
2019-02-06 06:36:03 +01:00
@ property ( nonatomic , readonly ) BOOL isRecordingVoiceMemo ;
@ property ( nonatomic ) VoiceMemoRecordingState voiceMemoRecordingState ;
2017-10-10 22:13:54 +02:00
@ property ( nonatomic ) CGPoint voiceMemoGestureStartLocation ;
2019-01-11 15:24:24 +01:00
@ property ( nonatomic , nullable ) NSArray < NSLayoutConstraint * > * layoutContraints ;
@ property ( nonatomic ) UIEdgeInsets receivedSafeAreaInsets ;
2019-01-17 17:56:52 +01:00
@ property ( nonatomic , nullable ) InputLinkPreview * inputLinkPreview ;
@ property ( nonatomic ) BOOL wasLinkPreviewCancelled ;
2019-01-25 22:43:16 +01:00
@ property ( nonatomic , nullable , weak ) LinkPreviewView * linkPreviewView ;
2019-10-11 06:52:56 +02:00
@ property ( nonatomic ) LKMentionCandidateSelectionView * mentionCandidateSelectionView ;
@ property ( nonatomic ) NSLayoutConstraint * mentionCandidateSelectionViewSizeConstraint ;
2017-10-10 22:13:54 +02:00
@ end
# pragma mark -
@ implementation ConversationInputToolbar
2018-06-28 19:28:14 +02:00
- ( instancetype ) initWithConversationStyle : ( ConversationStyle * ) conversationStyle
2017-10-10 22:13:54 +02:00
{
2018-06-30 00:49:35 +02:00
self = [ super initWithFrame : CGRectZero ] ;
2018-06-28 19:28:14 +02:00
_conversationStyle = conversationStyle ;
2019-01-11 15:24:24 +01:00
_receivedSafeAreaInsets = UIEdgeInsetsZero ;
2017-10-10 22:13:54 +02:00
if ( self ) {
[ self createContents ] ;
}
2018-06-28 19:28:14 +02:00
2017-10-10 22:13:54 +02:00
return self ;
}
2018-01-02 22:55:40 +01:00
- ( CGSize ) intrinsicContentSize
{
2018-07-03 04:04:15 +02:00
// Since we have ` self . autoresizingMask = UIViewAutoresizingFlexibleHeight` , we must specify
// an intrinsicContentSize . Specifying CGSize . zero causes the height to be determined by autolayout .
return CGSizeZero ;
2018-01-02 22:55:40 +01:00
}
2017-10-10 22:13:54 +02:00
- ( void ) createContents
{
2017-10-13 17:17:55 +02:00
self . layoutMargins = UIEdgeInsetsZero ;
2018-01-02 22:55:40 +01:00
self . autoresizingMask = UIViewAutoresizingFlexibleHeight ;
2019-12-09 06:47:13 +01:00
self . backgroundColor = LKColors . composeViewBackground ;
2017-11-03 21:17:21 +01:00
2017-10-10 22:13:54 +02:00
_inputTextView = [ ConversationInputTextView new ] ;
2017-10-17 16:07:57 +02:00
self . inputTextView . textViewToolbarDelegate = self ;
2019-12-09 06:47:13 +01:00
self . inputTextView . textColor = LKColors . text ;
self . inputTextView . font = [ UIFont systemFontOfSize : LKValues . mediumFontSize ] ;
self . inputTextView . backgroundColor = LKColors . composeViewTextFieldBackground ;
2019-01-28 20:44:50 +01:00
[ self . inputTextView setContentHuggingLow ] ;
[ self . inputTextView setCompressionResistanceLow ] ;
2020-12-17 01:37:53 +01:00
self . inputTextView . accessibilityLabel = @ "Input text view" ;
self . inputTextView . isAccessibilityElement = YES ;
2019-03-22 18:38:02 +01:00
SET_SUBVIEW _ACCESSIBILITY _IDENTIFIER ( self , _inputTextView ) ;
2018-07-03 04:04:15 +02:00
_textViewHeightConstraint = [ self . inputTextView autoSetDimension : ALDimensionHeight toSize : kMinTextViewHeight ] ;
2017-10-13 17:17:55 +02:00
2017-10-10 22:13:54 +02:00
_attachmentButton = [ [ UIButton alloc ] init ] ;
2019-12-09 06:47:13 +01:00
self . attachmentButton . accessibilityLabel = NSLocalizedString ( @ "ATTACHMENT_LABEL" , @ "Accessibility label for attaching photos" ) ;
self . attachmentButton . accessibilityHint = NSLocalizedString ( @ "ATTACHMENT_HINT" , @ "Accessibility hint describing what you can do with the attachment button" ) ;
[ self . attachmentButton addTarget : self action : @ selector ( attachmentButtonPressed ) forControlEvents : UIControlEventTouchUpInside ] ;
2020-03-17 06:18:53 +01:00
UIImage * attachmentImage = [ [ UIImage imageNamed : @ "CirclePlus" ] asTintedImageWithColor : LKColors . text ] ;
2019-12-09 06:47:13 +01:00
[ self . attachmentButton setImage : attachmentImage forState : UIControlStateNormal ] ;
2018-07-03 06:28:05 +02:00
[ self . attachmentButton autoSetDimensionsToSize : CGSizeMake ( 40 , kMinTextViewHeight ) ] ;
2019-03-22 18:38:02 +01:00
SET_SUBVIEW _ACCESSIBILITY _IDENTIFIER ( self , _attachmentButton ) ;
2017-10-10 22:13:54 +02:00
_sendButton = [ UIButton buttonWithType : UIButtonTypeCustom ] ;
2020-03-17 06:18:53 +01:00
NSString * iconName = LKAppModeUtilities . isLightMode ? @ "ArrowUpLightMode" : @ "ArrowUpDarkMode" ;
UIImage * sendImage = [ UIImage imageNamed : iconName ] ;
2019-12-10 00:26:09 +01:00
[ self . sendButton setImage : sendImage forState : UIControlStateNormal ] ;
[ self . sendButton autoSetDimensionsToSize : CGSizeMake ( 40 , kMinTextViewHeight ) ] ;
2019-03-22 18:38:02 +01:00
SET_SUBVIEW _ACCESSIBILITY _IDENTIFIER ( self , _sendButton ) ;
2019-12-10 00:26:09 +01:00
[ self . sendButton addTarget : self action : @ selector ( sendButtonPressed ) forControlEvents : UIControlEventTouchUpInside ] ;
2020-12-17 01:37:53 +01:00
self . sendButton . accessibilityLabel = @ "Send button" ;
self . sendButton . isAccessibilityElement = YES ;
2017-10-10 22:13:54 +02:00
2017-10-17 16:07:57 +02:00
_voiceMemoButton = [ UIButton buttonWithType : UIButtonTypeCustom ] ;
2020-03-17 06:18:53 +01:00
UIImage * voiceMemoIcon = [ [ UIImage imageNamed : @ "Microphone" ] asTintedImageWithColor : LKColors . text ] ;
2019-12-09 06:47:13 +01:00
[ self . voiceMemoButton setImage : voiceMemoIcon forState : UIControlStateNormal ] ;
2018-07-03 06:28:05 +02:00
[ self . voiceMemoButton autoSetDimensionsToSize : CGSizeMake ( 40 , kMinTextViewHeight ) ] ;
2020-12-17 01:37:53 +01:00
self . voiceMemoButton . accessibilityLabel = @ "Voice message button" ;
self . voiceMemoButton . isAccessibilityElement = YES ;
2019-03-22 18:38:02 +01:00
SET_SUBVIEW _ACCESSIBILITY _IDENTIFIER ( self , _voiceMemoButton ) ;
2017-10-10 22:13:54 +02:00
2017-10-13 17:17:55 +02:00
// 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 .
2019-12-09 06:47:13 +01:00
UILongPressGestureRecognizer * longPressGestureRecognizer = [ [ UILongPressGestureRecognizer alloc ] initWithTarget : self action : @ selector ( handleLongPress : ) ] ;
2017-10-10 22:13:54 +02:00
longPressGestureRecognizer . minimumPressDuration = 0 ;
2019-02-06 06:36:03 +01:00
self . voiceMemoGestureRecognizer = longPressGestureRecognizer ;
2018-07-03 06:28:05 +02:00
[ self . voiceMemoButton addGestureRecognizer : longPressGestureRecognizer ] ;
2017-10-10 22:13:54 +02:00
self . userInteractionEnabled = YES ;
2019-01-22 19:13:54 +01:00
_quotedReplyWrapper = [ UIView containerView ] ;
2019-12-11 00:25:53 +01:00
self . quotedReplyWrapper . backgroundColor = LKColors . composeViewTextFieldBackground ;
2019-01-22 19:13:54 +01:00
self . quotedReplyWrapper . hidden = YES ;
2019-01-25 17:33:09 +01:00
[ self . quotedReplyWrapper setContentHuggingHorizontalLow ] ;
[ self . quotedReplyWrapper setCompressionResistanceHorizontalLow ] ;
2019-03-22 18:38:02 +01:00
SET_SUBVIEW _ACCESSIBILITY _IDENTIFIER ( self , _quotedReplyWrapper ) ;
2018-07-03 04:04:15 +02:00
2019-01-22 19:13:54 +01:00
_linkPreviewWrapper = [ UIView containerView ] ;
self . linkPreviewWrapper . hidden = YES ;
2019-01-25 17:33:09 +01:00
[ self . linkPreviewWrapper setContentHuggingHorizontalLow ] ;
[ self . linkPreviewWrapper setCompressionResistanceHorizontalLow ] ;
2019-03-22 18:38:02 +01:00
SET_SUBVIEW _ACCESSIBILITY _IDENTIFIER ( self , _linkPreviewWrapper ) ;
2017-10-10 22:13:54 +02:00
2019-01-31 17:50:23 +01:00
// V Stack
2019-12-09 06:47:13 +01:00
UIStackView * vStack = [ [ UIStackView alloc ] initWithArrangedSubviews : @ [ self . quotedReplyWrapper , self . linkPreviewWrapper , self . inputTextView ] ] ;
2019-01-31 17:50:23 +01:00
vStack . axis = UILayoutConstraintAxisVertical ;
[ vStack setContentHuggingHorizontalLow ] ;
[ vStack setCompressionResistanceHorizontalLow ] ;
2019-01-25 17:33:09 +01:00
for ( UIView * button in @ [ self . attachmentButton , self . voiceMemoButton , self . sendButton ] ) {
[ button setContentHuggingHorizontalHigh ] ;
[ button setCompressionResistanceHorizontalHigh ] ;
}
2019-01-22 19:13:54 +01:00
2019-01-31 17:50:23 +01:00
// V Stack Wrapper
2019-12-09 06:47:13 +01:00
const CGFloat vStackRounding = kMinTextViewHeight / 2 ;
2019-01-31 17:50:23 +01:00
UIView * vStackWrapper = [ UIView containerView ] ;
vStackWrapper . layer . cornerRadius = vStackRounding ;
vStackWrapper . clipsToBounds = YES ;
[ vStackWrapper addSubview : vStack ] ;
[ vStack ows_autoPinToSuperviewEdges ] ;
[ vStackWrapper setContentHuggingHorizontalLow ] ;
[ vStackWrapper setCompressionResistanceHorizontalLow ] ;
2019-10-09 05:46:21 +02:00
// User Selection View
2019-10-11 06:52:56 +02:00
_mentionCandidateSelectionView = [ LKMentionCandidateSelectionView new ] ;
[ self addSubview : self . mentionCandidateSelectionView ] ;
[ self . mentionCandidateSelectionView autoPinEdgeToSuperviewEdge : ALEdgeTop ] ;
[ self . mentionCandidateSelectionView autoPinWidthToSuperview ] ;
self . mentionCandidateSelectionViewSizeConstraint = [ self . mentionCandidateSelectionView autoSetDimension : ALDimensionHeight toSize : 0 ] ;
2019-12-11 04:07:27 +01:00
self . mentionCandidateSelectionView . alpha = 0 ;
2019-10-11 06:52:56 +02:00
self . mentionCandidateSelectionView . delegate = self ;
2019-10-09 05:46:21 +02:00
2019-12-10 00:26:09 +01:00
// Button Container
UIView * buttonContainer = [ UIView new ] ;
[ buttonContainer addSubview : self . voiceMemoButton ] ;
[ self . voiceMemoButton ows_autoPinToSuperviewEdges ] ;
[ buttonContainer addSubview : self . sendButton ] ;
[ self . sendButton ows_autoPinToSuperviewEdges ] ;
2019-01-31 17:50:23 +01:00
// H Stack
2019-01-22 19:13:54 +01:00
_hStack = [ [ UIStackView alloc ]
2019-12-10 00:26:09 +01:00
initWithArrangedSubviews : @ [ self . attachmentButton , vStackWrapper , buttonContainer ] ] ;
2019-01-22 19:13:54 +01:00
self . hStack . axis = UILayoutConstraintAxisHorizontal ;
self . hStack . layoutMarginsRelativeArrangement = YES ;
2019-12-09 06:47:13 +01:00
self . hStack . layoutMargins = UIEdgeInsetsMake ( LKValues . smallSpacing , LKValues . smallSpacing , LKValues . smallSpacing , LKValues . smallSpacing ) ;
2019-01-22 19:13:54 +01:00
self . hStack . alignment = UIStackViewAlignmentBottom ;
2019-12-09 06:47:13 +01:00
self . hStack . spacing = LKValues . smallSpacing ;
2019-01-22 19:13:54 +01:00
[ self addSubview : self . hStack ] ;
2019-10-11 06:52:56 +02:00
[ self . hStack autoPinEdge : ALEdgeTop toEdge : ALEdgeBottom ofView : self . mentionCandidateSelectionView ] ;
2019-01-22 19:13:54 +01:00
[ self . hStack autoPinEdgeToSuperviewSafeArea : ALEdgeBottom ] ;
2019-01-25 17:33:09 +01:00
[ self . hStack setContentHuggingHorizontalLow ] ;
[ self . hStack setCompressionResistanceHorizontalLow ] ;
2019-01-11 15:24:24 +01:00
// See comments on updateContentLayout : .
if ( @ available ( iOS 11 , * ) ) {
2019-01-31 17:50:23 +01:00
vStack . insetsLayoutMarginsFromSafeArea = NO ;
vStackWrapper . insetsLayoutMarginsFromSafeArea = NO ;
2019-01-22 19:13:54 +01:00
self . hStack . insetsLayoutMarginsFromSafeArea = NO ;
2019-01-11 15:24:24 +01:00
self . insetsLayoutMarginsFromSafeArea = NO ;
}
2019-01-31 17:50:23 +01:00
vStack . preservesSuperviewLayoutMargins = NO ;
vStackWrapper . preservesSuperviewLayoutMargins = NO ;
2019-01-22 19:13:54 +01:00
self . hStack . preservesSuperviewLayoutMargins = NO ;
2019-01-11 15:24:24 +01:00
self . preservesSuperviewLayoutMargins = NO ;
2017-10-10 22:13:54 +02:00
2019-02-01 23:00:37 +01:00
// Border
//
// The border must reside _outside _ of vStackWrapper so
// that it doesn ' t run afoul of its clipping , so we can ' t
// use addBorderViewWithColor .
2019-09-06 08:30:40 +02:00
_borderView = [ UIView new ] ;
self . borderView . userInteractionEnabled = NO ;
self . borderView . backgroundColor = UIColor . clearColor ;
self . borderView . opaque = NO ;
2019-12-09 06:47:13 +01:00
self . borderView . layer . borderColor = LKColors . text . CGColor ;
self . borderView . layer . opacity = LKValues . composeViewTextFieldBorderOpacity ;
self . borderView . layer . borderWidth = LKValues . composeViewTextFieldBorderThickness ;
2019-09-06 08:30:40 +02:00
self . borderView . layer . cornerRadius = vStackRounding ;
[ self addSubview : self . borderView ] ;
[ self . borderView autoPinToEdgesOfView : vStackWrapper ] ;
[ self . borderView setCompressionResistanceLow ] ;
[ self . borderView setContentHuggingLow ] ;
2019-02-01 23:00:37 +01:00
2018-12-13 18:49:59 +01:00
[ self ensureShouldShowVoiceMemoButtonAnimated : NO doLayout : NO ] ;
2017-10-10 22:13:54 +02:00
}
2017-10-16 18:29:22 +02:00
- ( void ) updateFontSizes
{
self . inputTextView . font = [ UIFont ows_dynamicTypeBodyFont ] ;
}
2017-10-10 22:13:54 +02:00
- ( void ) setInputTextViewDelegate : ( id < ConversationInputTextViewDelegate > ) value
{
2018-09-06 19:01:24 +02:00
OWSAssertDebug ( self . inputTextView ) ;
OWSAssertDebug ( value ) ;
2017-10-10 22:13:54 +02:00
self . inputTextView . inputTextViewDelegate = value ;
}
- ( NSString * ) messageText
{
2018-09-06 19:01:24 +02:00
OWSAssertDebug ( self . inputTextView ) ;
2017-10-10 22:13:54 +02:00
return self . inputTextView . trimmedText ;
}
2018-07-03 06:42:32 +02:00
- ( void ) setMessageText : ( NSString * _Nullable ) value animated : ( BOOL ) isAnimated
2017-10-10 22:13:54 +02:00
{
2018-09-06 19:01:24 +02:00
OWSAssertDebug ( self . inputTextView ) ;
2017-10-10 22:13:54 +02:00
self . inputTextView . text = value ;
2019-04-12 17:04:38 +02:00
// It ' s important that we set the textViewHeight before
// doing any animation in ` ensureShouldShowVoiceMemoButtonAnimated`
// Otherwise , the resultant keyboard frame posted in ` keyboardWillChangeFrame`
// could reflect the inputTextView height * before * the new text was set .
//
// This bug was surfaced to the user as :
// - have a quoted reply draft in the input toolbar
// - type a multiline message
// - hit send
// - quoted reply preview and message text is cleared
// - input toolbar is shrunk to it ' s expected empty - text height
// - * but * the conversation ' s bottom content inset was too large . Specifically , it was
// still sized as if the input textview was multiple lines .
// Presumably this bug only surfaced when an animation coincides with more complicated layout
// changes ( in this case while simultaneous with removing quoted reply subviews , hiding the
// wrapper view * and * changing the height of the input textView
2018-08-30 21:18:03 +02:00
[ self ensureTextViewHeight ] ;
2019-01-17 17:56:52 +01:00
[ self updateInputLinkPreview ] ;
2019-04-12 17:04:38 +02:00
[ self ensureShouldShowVoiceMemoButtonAnimated : isAnimated doLayout : YES ] ;
2018-08-30 21:18:03 +02:00
}
2019-05-15 03:41:07 +02:00
- ( void ) setPlaceholderText : ( NSString * ) placeholderText
{
[ self . inputTextView setPlaceholderText : placeholderText ] ;
}
2018-08-30 21:18:03 +02:00
- ( void ) ensureTextViewHeight
{
2018-07-03 06:42:32 +02:00
[ self updateHeightWithTextView : self . inputTextView ] ;
2017-10-10 22:13:54 +02:00
}
2018-07-03 06:42:32 +02:00
- ( void ) clearTextMessageAnimated : ( BOOL ) isAnimated
2017-10-10 22:13:54 +02:00
{
2018-07-03 06:42:32 +02:00
[ self setMessageText : nil animated : isAnimated ] ;
2017-10-10 22:13:54 +02:00
[ self . inputTextView . undoManager removeAllActions ] ;
2019-01-17 17:56:52 +01:00
self . wasLinkPreviewCancelled = NO ;
2017-10-10 22:13:54 +02:00
}
2018-01-08 23:48:26 +01:00
- ( void ) toggleDefaultKeyboard
{
// Primary language is nil for the emoji keyboard .
if ( ! self . inputTextView . textInputMode . primaryLanguage ) {
// Stay on emoji keyboard after sending
return ;
}
// Otherwise , we want to toggle back to default keyboard if the user had the numeric keyboard present .
// Momentarily switch to a non - default keyboard , else reloadInputViews
// will not affect the displayed keyboard . In practice this isn ' t perceptable to the user .
// The alternative would be to dismiss - and - pop the keyboard , but that can cause a more pronounced animation .
2018-12-15 00:56:34 +01:00
self . inputTextView . keyboardType = UIKeyboardTypeNumbersAndPunctuation ;
[ self . inputTextView reloadInputViews ] ;
self . inputTextView . keyboardType = UIKeyboardTypeDefault ;
[ self . inputTextView reloadInputViews ] ;
2018-01-08 23:48:26 +01:00
}
2019-10-21 07:09:46 +02:00
- ( void ) setAttachmentButtonHidden : ( BOOL ) isHidden
{
[ self . attachmentButton setHidden : isHidden ] ;
}
2018-04-09 15:11:56 +02:00
- ( void ) setQuotedReply : ( nullable OWSQuotedReplyModel * ) quotedReply
2018-02-07 18:44:09 +01:00
{
2018-04-09 15:11:56 +02:00
if ( quotedReply = = _quotedReply ) {
2018-04-04 04:56:45 +02:00
return ;
}
2019-01-22 19:13:54 +01:00
[ self clearQuotedMessagePreview ] ;
2018-02-07 18:44:09 +01:00
2018-04-09 15:11:56 +02:00
_quotedReply = quotedReply ;
2018-04-04 04:56:45 +02:00
2018-04-09 15:11:56 +02:00
if ( ! quotedReply ) {
2018-04-04 04:56:45 +02:00
return ;
}
2018-07-02 22:35:30 +02:00
QuotedReplyPreview * quotedMessagePreview =
[ [ QuotedReplyPreview alloc ] initWithQuotedReply : quotedReply conversationStyle : self . conversationStyle ] ;
quotedMessagePreview . delegate = self ;
2019-01-25 17:33:09 +01:00
[ quotedMessagePreview setContentHuggingHorizontalLow ] ;
[ quotedMessagePreview setCompressionResistanceHorizontalLow ] ;
2018-07-02 22:35:30 +02:00
2019-01-22 19:13:54 +01:00
self . quotedReplyWrapper . hidden = NO ;
2019-01-25 22:43:16 +01:00
self . quotedReplyWrapper . layoutMargins = UIEdgeInsetsZero ;
2019-01-22 19:13:54 +01:00
[ self . quotedReplyWrapper addSubview : quotedMessagePreview ] ;
2018-07-14 00:26:58 +02:00
[ quotedMessagePreview ows_autoPinToSuperviewMargins ] ;
2019-03-22 18:38:02 +01:00
SET_SUBVIEW _ACCESSIBILITY _IDENTIFIER ( self , quotedMessagePreview ) ;
2019-01-25 22:43:16 +01:00
self . linkPreviewView . hasAsymmetricalRounding = ! self . quotedReply ;
2018-07-02 21:58:28 +02:00
}
- ( CGFloat ) quotedMessageTopMargin
{
return 5. f ;
2018-02-07 18:44:09 +01:00
}
2018-04-04 04:56:45 +02:00
- ( void ) clearQuotedMessagePreview
2018-02-07 18:44:09 +01:00
{
2019-01-22 19:13:54 +01:00
self . quotedReplyWrapper . hidden = YES ;
for ( UIView * subview in self . quotedReplyWrapper . subviews ) {
[ subview removeFromSuperview ] ;
2018-04-04 03:22:02 +02:00
}
2018-02-07 18:44:09 +01:00
}
2017-10-10 22:13:54 +02:00
- ( void ) beginEditingTextMessage
{
[ self . inputTextView becomeFirstResponder ] ;
}
- ( void ) endEditingTextMessage
{
[ self . inputTextView resignFirstResponder ] ;
}
2018-01-19 21:00:41 +01:00
- ( BOOL ) isInputTextViewFirstResponder
{
return self . inputTextView . isFirstResponder ;
}
2018-12-13 18:49:59 +01:00
- ( void ) ensureShouldShowVoiceMemoButtonAnimated : ( BOOL ) isAnimated doLayout : ( BOOL ) doLayout
2017-10-10 22:13:54 +02:00
{
2018-07-03 04:04:15 +02:00
void ( ^ updateBlock ) ( void ) = ^ {
if ( self . inputTextView . trimmedText . length > 0 ) {
2019-12-10 00:26:09 +01:00
if ( self . voiceMemoButton . alpha ! = 0 ) {
self . voiceMemoButton . alpha = 0 ;
2018-07-03 04:04:15 +02:00
}
2017-10-10 22:13:54 +02:00
2019-12-10 00:26:09 +01:00
if ( self . sendButton . alpha = = 0 ) {
self . sendButton . alpha = 1 ;
2018-07-03 04:04:15 +02:00
}
} else {
2019-12-10 00:26:09 +01:00
if ( self . voiceMemoButton . alpha = = 0 ) {
self . voiceMemoButton . alpha = 1 ;
2018-07-03 04:04:15 +02:00
}
2019-12-10 00:26:09 +01:00
if ( self . sendButton . alpha ! = 0 ) {
self . sendButton . alpha = 0 ;
2018-07-03 04:04:15 +02:00
}
}
2018-12-13 18:49:59 +01:00
if ( doLayout ) {
[ self layoutIfNeeded ] ;
}
2018-07-03 04:04:15 +02:00
} ;
2017-10-10 22:13:54 +02:00
2018-07-03 04:04:15 +02:00
if ( isAnimated ) {
[ UIView animateWithDuration : 0.1 animations : updateBlock ] ;
} else {
updateBlock ( ) ;
}
2017-10-10 22:13:54 +02:00
}
2019-01-11 15:24:24 +01:00
// iOS doesn ' t always update the safeAreaInsets correctly & in a timely
// way for the inputAccessoryView after a orientation change . The best
// workaround appears to be to use the safeAreaInsets from
// ConversationViewController ' s view . ConversationViewController updates
// this input toolbar using updateLayoutWithIsLandscape : .
- ( void ) updateContentLayout
{
if ( self . layoutContraints ) {
[ NSLayoutConstraint deactivateConstraints : self . layoutContraints ] ;
}
self . layoutContraints = @ [
2019-01-22 19:13:54 +01:00
[ self . hStack autoPinEdgeToSuperviewEdge : ALEdgeLeft withInset : self . receivedSafeAreaInsets . left ] ,
[ self . hStack autoPinEdgeToSuperviewEdge : ALEdgeRight withInset : self . receivedSafeAreaInsets . right ] ,
2019-01-11 15:24:24 +01:00
] ;
}
2019-01-15 22:33:54 +01:00
- ( void ) updateLayoutWithSafeAreaInsets : ( UIEdgeInsets ) safeAreaInsets
2019-01-11 15:24:24 +01:00
{
2019-01-15 22:33:54 +01:00
BOOL didChange = ! UIEdgeInsetsEqualToEdgeInsets ( self . receivedSafeAreaInsets , safeAreaInsets ) ;
2019-01-11 15:24:24 +01:00
BOOL hasLayout = self . layoutContraints ! = nil ;
self . receivedSafeAreaInsets = safeAreaInsets ;
if ( didChange || ! hasLayout ) {
[ self updateContentLayout ] ;
}
}
2017-10-10 22:13:54 +02:00
- ( void ) handleLongPress : ( UIGestureRecognizer * ) sender
{
switch ( sender . state ) {
case UIGestureRecognizerStatePossible :
case UIGestureRecognizerStateCancelled :
case UIGestureRecognizerStateFailed :
if ( self . isRecordingVoiceMemo ) {
// Cancel voice message if necessary .
2019-02-06 06:36:03 +01:00
self . voiceMemoRecordingState = VoiceMemoRecordingState_Idle ;
2017-10-10 22:13:54 +02:00
[ self . inputToolbarDelegate voiceMemoGestureDidCancel ] ;
}
break ;
case UIGestureRecognizerStateBegan :
2019-02-06 06:36:03 +01:00
switch ( self . voiceMemoRecordingState ) {
case VoiceMemoRecordingState_Idle :
break ;
case VoiceMemoRecordingState_RecordingHeld :
OWSFailDebug ( @ "while recording held, shouldn't be possible to restart gesture." ) ;
[ self . inputToolbarDelegate voiceMemoGestureDidCancel ] ;
break ;
case VoiceMemoRecordingState_RecordingLocked :
OWSFailDebug ( @ "once locked, shouldn't be possible to interact with gesture." ) ;
[ self . inputToolbarDelegate voiceMemoGestureDidCancel ] ;
break ;
2017-10-10 22:13:54 +02:00
}
// Start voice message .
2019-02-06 06:36:03 +01:00
self . voiceMemoRecordingState = VoiceMemoRecordingState_RecordingHeld ;
2017-10-10 22:13:54 +02:00
self . voiceMemoGestureStartLocation = [ sender locationInView : self ] ;
[ self . inputToolbarDelegate voiceMemoGestureDidStart ] ;
break ;
case UIGestureRecognizerStateChanged :
if ( self . isRecordingVoiceMemo ) {
// Check for "slide to cancel" gesture .
CGPoint location = [ sender locationInView : self ] ;
// For LTR / RTL , swiping in either direction will cancel .
// This is okay because there ' s only space on screen to perform the
// gesture in one direction .
2019-02-06 06:36:03 +01:00
CGFloat xOffset = fabs ( self . voiceMemoGestureStartLocation . x - location . x ) ;
CGFloat yOffset = fabs ( self . voiceMemoGestureStartLocation . y - location . y ) ;
// require a certain threshold before we consider the user to be
// interacting with the lock ui , otherwise there ' s perceptible wobble
// of the lock slider even when the user isn ' t intended to interact with it .
const CGFloat kLockThresholdPoints = 20. f ;
const CGFloat kLockOffsetPoints = 80. f ;
CGFloat yOffsetBeyondThreshold = MAX ( yOffset - kLockThresholdPoints , 0 ) ;
CGFloat lockAlpha = yOffsetBeyondThreshold / kLockOffsetPoints ;
BOOL isLocked = lockAlpha >= 1. f ;
if ( isLocked ) {
switch ( self . voiceMemoRecordingState ) {
case VoiceMemoRecordingState_RecordingHeld :
self . voiceMemoRecordingState = VoiceMemoRecordingState_RecordingLocked ;
[ self . inputToolbarDelegate voiceMemoGestureDidLock ] ;
[ self . inputToolbarDelegate voiceMemoGestureDidUpdateCancelWithRatioComplete : 0 ] ;
break ;
case VoiceMemoRecordingState_RecordingLocked :
// already locked
break ;
case VoiceMemoRecordingState_Idle :
OWSFailDebug ( @ "failure: unexpeceted idle state" ) ;
[ self . inputToolbarDelegate voiceMemoGestureDidCancel ] ;
break ;
}
2017-10-10 22:13:54 +02:00
} else {
2019-02-06 06:36:03 +01:00
[ self . voiceMemoLockView updateWithRatioComplete : lockAlpha ] ;
2019-04-02 23:40:35 +02:00
// The lower this value , the easier it is to cancel by accident .
// The higher this value , the harder it is to cancel .
const CGFloat kCancelOffsetPoints = 100. f ;
CGFloat cancelAlpha = xOffset / kCancelOffsetPoints ;
BOOL isCancelled = cancelAlpha >= 1. f ;
if ( isCancelled ) {
self . voiceMemoRecordingState = VoiceMemoRecordingState_Idle ;
[ self . inputToolbarDelegate voiceMemoGestureDidCancel ] ;
break ;
} else {
[ self . inputToolbarDelegate voiceMemoGestureDidUpdateCancelWithRatioComplete : cancelAlpha ] ;
}
2017-10-10 22:13:54 +02:00
}
}
break ;
case UIGestureRecognizerStateEnded :
2019-02-06 06:36:03 +01:00
switch ( self . voiceMemoRecordingState ) {
case VoiceMemoRecordingState_Idle :
break ;
case VoiceMemoRecordingState_RecordingHeld :
// End voice message .
self . voiceMemoRecordingState = VoiceMemoRecordingState_Idle ;
[ self . inputToolbarDelegate voiceMemoGestureDidComplete ] ;
break ;
case VoiceMemoRecordingState_RecordingLocked :
// Continue recording .
break ;
2017-10-10 22:13:54 +02:00
}
break ;
}
}
# pragma mark - Voice Memo
2019-02-06 06:36:03 +01:00
- ( BOOL ) isRecordingVoiceMemo
{
switch ( self . voiceMemoRecordingState ) {
case VoiceMemoRecordingState_Idle :
return NO ;
case VoiceMemoRecordingState_RecordingHeld :
case VoiceMemoRecordingState_RecordingLocked :
return YES ;
}
}
2017-10-10 22:13:54 +02:00
- ( void ) showVoiceMemoUI
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread ( ) ;
2017-10-10 22:13:54 +02:00
self . voiceMemoStartTime = [ NSDate date ] ;
[ self . voiceMemoUI removeFromSuperview ] ;
2019-02-06 06:36:03 +01:00
[ self . voiceMemoLockView removeFromSuperview ] ;
2017-10-10 22:13:54 +02:00
self . voiceMemoUI = [ UIView new ] ;
2019-12-10 00:12:51 +01:00
self . voiceMemoUI . backgroundColor = LKColors . composeViewBackground ;
2017-10-10 22:13:54 +02:00
[ self addSubview : self . voiceMemoUI ] ;
2019-04-02 23:16:38 +02:00
[ self . voiceMemoUI autoPinEdgesToSuperviewEdges ] ;
2019-03-22 18:38:02 +01:00
SET_SUBVIEW _ACCESSIBILITY _IDENTIFIER ( self , _voiceMemoUI ) ;
2017-10-10 22:13:54 +02:00
self . voiceMemoContentView = [ UIView new ] ;
[ self . voiceMemoUI addSubview : self . voiceMemoContentView ] ;
2019-12-10 00:12:51 +01:00
[ self . voiceMemoContentView autoPinLeadingToEdgeOfView : self . voiceMemoUI ] ;
[ self . voiceMemoContentView autoPinTopToSuperviewMargin ] ;
[ self . voiceMemoContentView autoPinTrailingToEdgeOfView : self . voiceMemoUI ] ;
[ self . voiceMemoContentView autoPinBottomToSuperviewMargin ] ;
2017-10-10 22:13:54 +02:00
self . recordingLabel = [ UILabel new ] ;
2019-12-10 00:12:51 +01:00
self . recordingLabel . textColor = LKColors . destructive ;
self . recordingLabel . font = [ UIFont systemFontOfSize : LKValues . smallFontSize ] ;
2017-10-10 22:13:54 +02:00
[ self . voiceMemoContentView addSubview : self . recordingLabel ] ;
2019-03-22 18:38:02 +01:00
SET_SUBVIEW _ACCESSIBILITY _IDENTIFIER ( self , _recordingLabel ) ;
2019-02-06 06:36:03 +01:00
VoiceMemoLockView * voiceMemoLockView = [ VoiceMemoLockView new ] ;
self . voiceMemoLockView = voiceMemoLockView ;
[ self addSubview : voiceMemoLockView ] ;
[ voiceMemoLockView autoPinTrailingToSuperviewMargin ] ;
[ voiceMemoLockView autoPinEdge : ALEdgeBottom toEdge : ALEdgeTop ofView : self . voiceMemoContentView ] ;
[ voiceMemoLockView setCompressionResistanceHigh ] ;
2017-10-10 22:13:54 +02:00
[ self updateVoiceMemo ] ;
2019-12-10 00:12:51 +01:00
UIImage * icon = [ UIImage imageNamed : @ "Microphone" ] ;
2018-09-06 19:01:24 +02:00
OWSAssertDebug ( icon ) ;
2017-10-10 22:13:54 +02:00
UIImageView * imageView =
[ [ UIImageView alloc ] initWithImage : [ icon imageWithRenderingMode : UIImageRenderingModeAlwaysTemplate ] ] ;
2019-12-10 00:12:51 +01:00
imageView . tintColor = LKColors . destructive ;
2019-02-06 06:36:03 +01:00
[ imageView setContentHuggingHigh ] ;
2017-10-10 22:13:54 +02:00
[ self . voiceMemoContentView addSubview : imageView ] ;
NSMutableAttributedString * cancelString = [ NSMutableAttributedString new ] ;
const CGFloat cancelArrowFontSize = ScaleFromIPhone5To7Plus ( 18.4 , 20. f ) ;
2019-12-10 00:12:51 +01:00
const CGFloat cancelFontSize = ScaleFromIPhone5To7Plus ( LKValues . smallFontSize , LKValues . mediumFontSize ) ;
2018-06-29 23:00:22 +02:00
NSString * arrowHead = ( CurrentAppContext ( ) . isRTL ? @ "\uf105" : @ "\uf104" ) ;
2017-10-10 22:13:54 +02:00
[ cancelString
appendAttributedString : [ [ NSAttributedString alloc ]
initWithString : arrowHead
attributes : @ {
NSFontAttributeName : [ UIFont ows_fontAwesomeFont : cancelArrowFontSize ] ,
2019-12-10 00:12:51 +01:00
NSForegroundColorAttributeName : LKColors . destructive ,
2017-10-10 22:13:54 +02:00
NSBaselineOffsetAttributeName : @ ( -1. f ) ,
} ] ] ;
[ cancelString
appendAttributedString : [ [ NSAttributedString alloc ]
initWithString : @ " "
attributes : @ {
NSFontAttributeName : [ UIFont ows_fontAwesomeFont : cancelArrowFontSize ] ,
2019-12-10 00:12:51 +01:00
NSForegroundColorAttributeName : LKColors . destructive ,
2017-10-10 22:13:54 +02:00
NSBaselineOffsetAttributeName : @ ( -1. f ) ,
} ] ] ;
[ cancelString
appendAttributedString : [ [ NSAttributedString alloc ]
initWithString : NSLocalizedString ( @ "VOICE_MESSAGE_CANCEL_INSTRUCTIONS" ,
@ "Indicates how to cancel a voice message." )
attributes : @ {
2019-12-10 00:12:51 +01:00
NSFontAttributeName : [ UIFont systemFontOfSize : cancelFontSize ] ,
NSForegroundColorAttributeName : LKColors . destructive ,
2017-10-10 22:13:54 +02:00
} ] ] ;
[ cancelString
appendAttributedString : [ [ NSAttributedString alloc ]
initWithString : @ " "
attributes : @ {
NSFontAttributeName : [ UIFont ows_fontAwesomeFont : cancelArrowFontSize ] ,
2019-12-10 00:12:51 +01:00
NSForegroundColorAttributeName : LKColors . destructive ,
2017-10-10 22:13:54 +02:00
NSBaselineOffsetAttributeName : @ ( -1. f ) ,
} ] ] ;
[ cancelString
appendAttributedString : [ [ NSAttributedString alloc ]
initWithString : arrowHead
attributes : @ {
NSFontAttributeName : [ UIFont ows_fontAwesomeFont : cancelArrowFontSize ] ,
2019-12-10 00:12:51 +01:00
NSForegroundColorAttributeName : LKColors . destructive ,
2017-10-10 22:13:54 +02:00
NSBaselineOffsetAttributeName : @ ( -1. f ) ,
} ] ] ;
UILabel * cancelLabel = [ UILabel new ] ;
2019-02-06 06:36:03 +01:00
self . voiceMemoCancelLabel = cancelLabel ;
2017-10-10 22:13:54 +02:00
cancelLabel . attributedText = cancelString ;
[ self . voiceMemoContentView addSubview : cancelLabel ] ;
const CGFloat kRedCircleSize = 100. f ;
UIView * redCircleView = [ UIView new ] ;
2019-02-06 06:36:03 +01:00
self . voiceMemoRedRecordingCircle = redCircleView ;
2019-12-10 00:12:51 +01:00
redCircleView . backgroundColor = LKColors . destructive ;
2017-10-10 22:13:54 +02:00
redCircleView . layer . cornerRadius = kRedCircleSize * 0.5 f ;
[ redCircleView autoSetDimension : ALDimensionWidth toSize : kRedCircleSize ] ;
[ redCircleView autoSetDimension : ALDimensionHeight toSize : kRedCircleSize ] ;
[ self . voiceMemoContentView addSubview : redCircleView ] ;
[ redCircleView autoAlignAxis : ALAxisHorizontal toSameAxisOfView : self . voiceMemoButton ] ;
[ redCircleView autoAlignAxis : ALAxisVertical toSameAxisOfView : self . voiceMemoButton ] ;
2019-12-10 00:12:51 +01:00
UIImage * whiteIcon = [ UIImage imageNamed : @ "Microphone" ] ;
2018-09-06 19:01:24 +02:00
OWSAssertDebug ( whiteIcon ) ;
2017-10-10 22:13:54 +02:00
UIImageView * whiteIconView = [ [ UIImageView alloc ] initWithImage : whiteIcon ] ;
[ redCircleView addSubview : whiteIconView ] ;
[ whiteIconView autoCenterInSuperview ] ;
[ imageView autoVCenterInSuperview ] ;
2019-12-10 00:12:51 +01:00
[ imageView autoPinLeadingToSuperviewMarginWithInset : LKValues . smallSpacing ] ;
2017-10-10 22:13:54 +02:00
[ self . recordingLabel autoVCenterInSuperview ] ;
2020-03-17 06:18:53 +01:00
[ self . recordingLabel autoPinLeadingToTrailingEdgeOfView : imageView offset : 12. f ] ;
2017-10-10 22:13:54 +02:00
[ cancelLabel autoVCenterInSuperview ] ;
[ cancelLabel autoHCenterInSuperview ] ;
2019-04-02 23:16:38 +02:00
[ self . voiceMemoUI layoutIfNeeded ] ;
2017-10-10 22:13:54 +02:00
// Slide in the "slide to cancel" label .
CGRect cancelLabelStartFrame = cancelLabel . frame ;
CGRect cancelLabelEndFrame = cancelLabel . frame ;
cancelLabelStartFrame . origin . x
2018-06-29 23:00:22 +02:00
= ( CurrentAppContext ( ) . isRTL ? - self . voiceMemoUI . bounds . size . width : self . voiceMemoUI . bounds . size . width ) ;
2017-10-10 22:13:54 +02:00
cancelLabel . frame = cancelLabelStartFrame ;
2019-02-06 06:36:03 +01:00
voiceMemoLockView . transform = CGAffineTransformMakeScale ( 0.0 , 0.0 ) ;
[ voiceMemoLockView layoutIfNeeded ] ;
[ UIView animateWithDuration : 0.2 f
delay : 1. f
options : 0
animations : ^ {
voiceMemoLockView . transform = CGAffineTransformIdentity ;
}
completion : nil ] ;
2017-10-10 22:13:54 +02:00
[ UIView animateWithDuration : 0.35 f
delay : 0. f
options : UIViewAnimationOptionCurveEaseOut
animations : ^ {
cancelLabel . frame = cancelLabelEndFrame ;
}
completion : nil ] ;
// Pulse the icon .
imageView . layer . opacity = 1. f ;
[ UIView animateWithDuration : 0.5 f
delay : 0.2 f
options : UIViewAnimationOptionRepeat | UIViewAnimationOptionAutoreverse
| UIViewAnimationOptionCurveEaseIn
animations : ^ {
imageView . layer . opacity = 0. f ;
}
completion : nil ] ;
// Fade in the view .
self . voiceMemoUI . layer . opacity = 0. f ;
[ UIView animateWithDuration : 0.2 f
animations : ^ {
self . voiceMemoUI . layer . opacity = 1. f ;
}
completion : ^ ( BOOL finished ) {
if ( finished ) {
self . voiceMemoUI . layer . opacity = 1. f ;
}
} ] ;
[ self . voiceMemoUpdateTimer invalidate ] ;
self . voiceMemoUpdateTimer = [ NSTimer weakScheduledTimerWithTimeInterval : 0.1 f
target : self
selector : @ selector ( updateVoiceMemo )
userInfo : nil
repeats : YES ] ;
}
- ( void ) hideVoiceMemoUI : ( BOOL ) animated
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread ( ) ;
2017-10-10 22:13:54 +02:00
2019-02-06 06:36:03 +01:00
self . voiceMemoRecordingState = VoiceMemoRecordingState_Idle ;
2017-10-10 22:13:54 +02:00
UIView * oldVoiceMemoUI = self . voiceMemoUI ;
2019-02-06 06:36:03 +01:00
UIView * oldVoiceMemoLockView = self . voiceMemoLockView ;
2017-10-10 22:13:54 +02:00
self . voiceMemoUI = nil ;
2019-02-06 06:36:03 +01:00
self . voiceMemoCancelLabel = nil ;
self . voiceMemoRedRecordingCircle = nil ;
2017-10-17 16:07:57 +02:00
self . voiceMemoContentView = nil ;
2019-02-06 06:36:03 +01:00
self . voiceMemoLockView = nil ;
2017-10-17 16:07:57 +02:00
self . recordingLabel = nil ;
2019-02-06 06:36:03 +01:00
[ self . voiceMemoUpdateTimer invalidate ] ;
2017-10-10 22:13:54 +02:00
self . voiceMemoUpdateTimer = nil ;
[ oldVoiceMemoUI . layer removeAllAnimations ] ;
if ( animated ) {
[ UIView animateWithDuration : 0.35 f
animations : ^ {
oldVoiceMemoUI . layer . opacity = 0. f ;
2019-02-06 06:36:03 +01:00
oldVoiceMemoLockView . layer . opacity = 0. f ;
2017-10-10 22:13:54 +02:00
}
completion : ^ ( BOOL finished ) {
[ oldVoiceMemoUI removeFromSuperview ] ;
2019-02-06 06:36:03 +01:00
[ oldVoiceMemoLockView removeFromSuperview ] ;
2017-10-10 22:13:54 +02:00
} ] ;
} else {
[ oldVoiceMemoUI removeFromSuperview ] ;
2019-02-06 06:36:03 +01:00
[ oldVoiceMemoLockView removeFromSuperview ] ;
2017-10-10 22:13:54 +02:00
}
}
2019-02-06 06:36:03 +01:00
- ( void ) lockVoiceMemoUI
{
__weak __typeof ( self ) weakSelf = self ;
UIButton * sendVoiceMemoButton = [ [ OWSButton alloc ] initWithBlock : ^ {
[ weakSelf . inputToolbarDelegate voiceMemoGestureDidComplete ] ;
} ] ;
[ sendVoiceMemoButton setTitle : MessageStrings . sendButton forState : UIControlStateNormal ] ;
2019-12-10 00:12:51 +01:00
[ sendVoiceMemoButton setTitleColor : LKColors . text forState : UIControlStateNormal ] ;
sendVoiceMemoButton . titleLabel . font = [ UIFont boldSystemFontOfSize : LKValues . mediumFontSize ] ;
2019-02-06 06:36:03 +01:00
sendVoiceMemoButton . alpha = 0 ;
[ self . voiceMemoContentView addSubview : sendVoiceMemoButton ] ;
2019-12-10 00:12:51 +01:00
[ sendVoiceMemoButton autoPinEdgeToSuperviewMargin : ALEdgeTrailing withInset : LKValues . smallSpacing ] ;
2019-02-07 16:23:22 +01:00
[ sendVoiceMemoButton autoVCenterInSuperview ] ;
2019-02-06 06:36:03 +01:00
[ sendVoiceMemoButton setCompressionResistanceHigh ] ;
[ sendVoiceMemoButton setContentHuggingHigh ] ;
2019-03-22 18:38:02 +01:00
SET_SUBVIEW _ACCESSIBILITY _IDENTIFIER ( self , sendVoiceMemoButton ) ;
2019-02-06 06:36:03 +01:00
UIButton * cancelButton = [ [ OWSButton alloc ] initWithBlock : ^ {
[ weakSelf . inputToolbarDelegate voiceMemoGestureDidCancel ] ;
} ] ;
[ cancelButton setTitle : CommonStrings . cancelButton forState : UIControlStateNormal ] ;
2019-12-10 00:12:51 +01:00
[ cancelButton setTitleColor : LKColors . destructive forState : UIControlStateNormal ] ;
cancelButton . titleLabel . font = [ UIFont boldSystemFontOfSize : LKValues . mediumFontSize ] ;
2019-02-06 06:36:03 +01:00
cancelButton . alpha = 0 ;
cancelButton . titleLabel . textAlignment = NSTextAlignmentCenter ;
2019-03-22 18:38:02 +01:00
SET_SUBVIEW _ACCESSIBILITY _IDENTIFIER ( self , cancelButton ) ;
2019-02-06 06:36:03 +01:00
[ self . voiceMemoContentView addSubview : cancelButton ] ;
OWSAssert ( self . recordingLabel ! = nil ) ;
[ self . recordingLabel setContentHuggingHigh ] ;
[ NSLayoutConstraint autoSetPriority : UILayoutPriorityDefaultLow
forConstraints : ^ {
[ cancelButton autoHCenterInSuperview ] ;
} ] ;
[ cancelButton autoPinEdge : ALEdgeLeading
toEdge : ALEdgeTrailing
ofView : self . recordingLabel
withOffset : 4
relation : NSLayoutRelationGreaterThanOrEqual ] ;
[ cancelButton autoPinEdge : ALEdgeTrailing
toEdge : ALEdgeLeading
ofView : sendVoiceMemoButton
withOffset : -4
relation : NSLayoutRelationLessThanOrEqual ] ;
[ cancelButton autoVCenterInSuperview ] ;
[ self . voiceMemoContentView layoutIfNeeded ] ;
[ UIView animateWithDuration : 0.35
animations : ^ {
self . voiceMemoCancelLabel . alpha = 0 ;
self . voiceMemoRedRecordingCircle . alpha = 0 ;
self . voiceMemoLockView . transform = CGAffineTransformMakeScale ( 0 , 0 ) ;
cancelButton . alpha = 1.0 ;
sendVoiceMemoButton . alpha = 1.0 ;
}
completion : ^ ( BOOL finished ) {
[ self . voiceMemoCancelLabel removeFromSuperview ] ;
[ self . voiceMemoRedRecordingCircle removeFromSuperview ] ;
[ self . voiceMemoLockView removeFromSuperview ] ;
} ] ;
}
2017-10-10 22:13:54 +02:00
- ( void ) setVoiceMemoUICancelAlpha : ( CGFloat ) cancelAlpha
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread ( ) ;
2017-10-10 22:13:54 +02:00
// Fade out the voice message views as the cancel gesture
// proceeds as feedback .
self . voiceMemoContentView . layer . opacity = MAX ( 0. f , MIN ( 1. f , 1. f - ( float ) cancelAlpha ) ) ;
}
- ( void ) updateVoiceMemo
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread ( ) ;
2017-10-10 22:13:54 +02:00
NSTimeInterval durationSeconds = fabs ( [ self . voiceMemoStartTime timeIntervalSinceNow ] ) ;
2017-12-01 23:10:14 +01:00
self . recordingLabel . text = [ OWSFormat formatDurationSeconds : ( long ) round ( durationSeconds ) ] ;
2017-10-10 22:13:54 +02:00
[ self . recordingLabel sizeToFit ] ;
}
- ( void ) cancelVoiceMemoIfNecessary
{
if ( self . isRecordingVoiceMemo ) {
2019-02-06 06:36:03 +01:00
self . voiceMemoRecordingState = VoiceMemoRecordingState_Idle ;
2017-10-10 22:13:54 +02:00
}
}
2017-10-13 17:17:55 +02:00
# pragma mark - Event Handlers
2017-10-10 22:13:54 +02:00
- ( void ) sendButtonPressed
{
2018-09-06 19:01:24 +02:00
OWSAssertDebug ( self . inputToolbarDelegate ) ;
2017-10-10 22:13:54 +02:00
2018-01-16 23:55:53 +01:00
[ self . inputToolbarDelegate sendButtonPressed ] ;
2017-10-10 22:13:54 +02:00
}
- ( void ) attachmentButtonPressed
{
2018-09-06 19:01:24 +02:00
OWSAssertDebug ( self . inputToolbarDelegate ) ;
2017-10-10 22:13:54 +02:00
[ self . inputToolbarDelegate attachmentButtonPressed ] ;
}
2017-10-17 16:07:57 +02:00
# pragma mark - ConversationTextViewToolbarDelegate
2017-10-10 22:13:54 +02:00
2018-07-03 04:04:15 +02:00
- ( void ) textViewDidChange : ( UITextView * ) textView
2017-10-10 22:13:54 +02:00
{
2018-09-06 19:01:24 +02:00
OWSAssertDebug ( self . inputToolbarDelegate ) ;
2018-12-13 18:49:59 +01:00
[ self ensureShouldShowVoiceMemoButtonAnimated : YES doLayout : YES ] ;
2018-07-03 04:04:15 +02:00
[ self updateHeightWithTextView : textView ] ;
2019-01-17 17:56:52 +01:00
[ self updateInputLinkPreview ] ;
2017-10-10 22:13:54 +02:00
}
2019-02-16 04:35:53 +01:00
- ( void ) textViewDidChangeSelection : ( UITextView * ) textView
{
[ self updateInputLinkPreview ] ;
}
2018-07-03 04:04:15 +02:00
- ( void ) updateHeightWithTextView : ( UITextView * ) textView
2017-10-13 17:17:55 +02:00
{
2018-07-03 04:04:15 +02:00
// compute new height assuming width is unchanged
CGSize currentSize = textView . frame . size ;
2018-12-07 23:56:50 +01:00
CGFloat fixedWidth = currentSize . width ;
CGSize contentSize = [ textView sizeThatFits : CGSizeMake ( fixedWidth , CGFLOAT_MAX ) ] ;
// ` textView . contentSize` isn ' t accurate when restoring a multiline draft , so we compute it here .
textView . contentSize = contentSize ;
CGFloat newHeight = CGFloatClamp ( contentSize . height , kMinTextViewHeight , kMaxTextViewHeight ) ;
2017-10-13 17:17:55 +02:00
2018-07-03 04:04:15 +02:00
if ( newHeight ! = self . textViewHeight ) {
self . textViewHeight = newHeight ;
2018-09-06 19:01:24 +02:00
OWSAssertDebug ( self . textViewHeightConstraint ) ;
2018-07-03 04:04:15 +02:00
self . textViewHeightConstraint . constant = newHeight ;
[ self invalidateIntrinsicContentSize ] ;
2017-10-13 17:17:55 +02:00
}
}
2018-04-04 03:59:19 +02:00
# pragma mark QuotedReplyPreviewViewDelegate
2018-04-04 03:22:02 +02:00
2018-04-04 03:59:19 +02:00
- ( void ) quotedReplyPreviewDidPressCancel : ( QuotedReplyPreview * ) preview
2018-04-04 03:22:02 +02:00
{
2018-04-09 15:11:56 +02:00
self . quotedReply = nil ;
2018-04-04 03:22:02 +02:00
}
2019-01-17 17:56:52 +01:00
# pragma mark - Link Preview
- ( void ) updateInputLinkPreview
{
OWSAssertIsOnMainThread ( ) ;
NSString * body =
[ [ self messageText ] stringByTrimmingCharactersInSet : [ NSCharacterSet whitespaceAndNewlineCharacterSet ] ] ;
if ( body . length < 1 ) {
2019-01-25 22:43:16 +01:00
[ self clearLinkPreviewStateAndView ] ;
2019-01-17 17:56:52 +01:00
self . wasLinkPreviewCancelled = NO ;
return ;
}
2019-01-29 16:36:54 +01:00
if ( self . wasLinkPreviewCancelled ) {
[ self clearLinkPreviewStateAndView ] ;
return ;
}
2019-01-18 22:44:04 +01:00
// Don ' t include link previews for oversize text messages .
if ( [ body lengthOfBytesUsingEncoding : NSUTF8StringEncoding ] >= kOversizeTextMessageSizeThreshold ) {
2019-01-25 22:43:16 +01:00
[ self clearLinkPreviewStateAndView ] ;
2019-01-18 22:44:04 +01:00
return ;
}
2019-02-16 04:35:53 +01:00
// It ' s key that we use the * raw / unstripped * text , so we can reconcile cursor position with the
// selectedRange .
2019-09-17 01:56:47 +02:00
NSString * _Nullable previewUrl = [ OWSLinkPreview previewUrlForRawBodyText : self . inputTextView . text selectedRange : self . inputTextView . selectedRange ] ;
if ( [ previewUrl hasSuffix : @ ".gif" ] ) {
return [ self clearLinkPreviewStateAndView ] ;
}
2019-01-17 17:56:52 +01:00
if ( previewUrl . length < 1 ) {
2019-09-17 01:56:47 +02:00
return [ self clearLinkPreviewStateAndView ] ;
2019-01-17 17:56:52 +01:00
}
if ( self . inputLinkPreview && [ self . inputLinkPreview . previewUrl isEqualToString : previewUrl ] ) {
2019-09-17 01:56:47 +02:00
return ; // No need to update .
2019-01-17 17:56:52 +01:00
}
InputLinkPreview * inputLinkPreview = [ InputLinkPreview new ] ;
self . inputLinkPreview = inputLinkPreview ;
self . inputLinkPreview . previewUrl = previewUrl ;
[ self ensureLinkPreviewViewWithState : [ LinkPreviewLoading new ] ] ;
__weak ConversationInputToolbar * weakSelf = self ;
2019-01-24 19:06:22 +01:00
[ [ OWSLinkPreview tryToBuildPreviewInfoObjcWithPreviewUrl : previewUrl ]
. then ( ^ ( OWSLinkPreviewDraft * linkPreviewDraft ) {
ConversationInputToolbar * _Nullable strongSelf = weakSelf ;
if ( ! strongSelf ) {
return ;
}
if ( strongSelf . inputLinkPreview ! = inputLinkPreview ) {
// Obsolete callback .
return ;
}
inputLinkPreview . linkPreviewDraft = linkPreviewDraft ;
LinkPreviewDraft * viewState = [ [ LinkPreviewDraft alloc ] initWithLinkPreviewDraft : linkPreviewDraft ] ;
[ strongSelf ensureLinkPreviewViewWithState : viewState ] ;
} )
. catch ( ^ ( id error ) {
// The link preview could not be loaded .
[ weakSelf clearLinkPreviewView ] ;
} ) retainUntilComplete ] ;
2019-01-17 17:56:52 +01:00
}
- ( void ) ensureLinkPreviewViewWithState : ( id < LinkPreviewState > ) state
{
OWSAssertIsOnMainThread ( ) ;
[ self clearLinkPreviewView ] ;
2019-01-23 19:10:25 +01:00
LinkPreviewView * linkPreviewView = [ [ LinkPreviewView alloc ] initWithDraftDelegate : self ] ;
2019-01-18 16:23:07 +01:00
linkPreviewView . state = state ;
2019-01-25 22:43:16 +01:00
linkPreviewView . hasAsymmetricalRounding = ! self . quotedReply ;
self . linkPreviewView = linkPreviewView ;
2019-01-22 19:13:54 +01:00
self . linkPreviewWrapper . hidden = NO ;
2020-01-13 05:50:30 +01:00
self . linkPreviewWrapper . backgroundColor = LKColors . composeViewTextFieldBackground ;
2019-01-22 19:13:54 +01:00
[ self . linkPreviewWrapper addSubview : linkPreviewView ] ;
[ linkPreviewView ows_autoPinToSuperviewMargins ] ;
2019-01-17 17:56:52 +01:00
}
2019-01-25 22:43:16 +01:00
- ( void ) clearLinkPreviewStateAndView
{
OWSAssertIsOnMainThread ( ) ;
self . inputLinkPreview = nil ;
self . linkPreviewView = nil ;
[ self clearLinkPreviewView ] ;
}
2019-01-17 17:56:52 +01:00
- ( void ) clearLinkPreviewView
{
OWSAssertIsOnMainThread ( ) ;
// Clear old link preview state .
2019-01-22 19:13:54 +01:00
for ( UIView * subview in self . linkPreviewWrapper . subviews ) {
[ subview removeFromSuperview ] ;
}
self . linkPreviewWrapper . hidden = YES ;
2019-01-17 17:56:52 +01:00
}
- ( nullable OWSLinkPreviewDraft * ) linkPreviewDraft
{
OWSAssertIsOnMainThread ( ) ;
if ( ! self . inputLinkPreview ) {
return nil ;
}
if ( self . wasLinkPreviewCancelled ) {
return nil ;
}
return self . inputLinkPreview . linkPreviewDraft ;
}
2019-01-23 19:10:25 +01:00
# pragma mark - LinkPreviewViewDraftDelegate
2019-01-17 17:56:52 +01:00
- ( BOOL ) linkPreviewCanCancel
{
OWSAssertIsOnMainThread ( ) ;
return YES ;
}
- ( void ) linkPreviewDidCancel
{
OWSAssertIsOnMainThread ( ) ;
self . wasLinkPreviewCancelled = YES ;
2019-01-25 22:43:16 +01:00
self . inputLinkPreview = nil ;
[ self clearLinkPreviewStateAndView ] ;
2019-01-17 17:56:52 +01:00
}
2019-09-06 08:30:40 +02:00
- ( void ) hideInputMethod
{
self . hStack . hidden = YES ;
self . borderView . hidden = YES ;
}
2019-10-11 06:52:56 +02:00
# pragma mark - Mention Candidate Selection View
2019-10-09 05:46:21 +02:00
2019-10-11 06:52:56 +02:00
- ( void ) showMentionCandidateSelectionViewFor : ( NSArray < LKMention * > * ) mentionCandidates in : ( TSThread * ) thread
2019-10-09 05:46:21 +02:00
{
2020-11-19 05:24:09 +01:00
SNOpenGroup * publicChat = [ LKStorage . shared getOpenGroupForThreadID : thread . uniqueId ] ;
2019-10-15 01:29:41 +02:00
if ( publicChat ! = nil ) {
self . mentionCandidateSelectionView . publicChatServer = publicChat . server ;
2019-10-15 01:50:06 +02:00
[ self . mentionCandidateSelectionView setPublicChatChannel : publicChat . channel ] ;
2019-10-15 01:29:41 +02:00
}
2019-10-11 06:52:56 +02:00
self . mentionCandidateSelectionView . mentionCandidates = mentionCandidates ;
2019-12-11 04:07:27 +01:00
self . mentionCandidateSelectionViewSizeConstraint . constant = MIN ( mentionCandidates . count , 4 ) * 42 ;
self . mentionCandidateSelectionView . alpha = 1 ;
2019-10-09 06:16:07 +02:00
[ self setNeedsLayout ] ;
[ self layoutIfNeeded ] ;
2019-10-09 05:46:21 +02:00
}
2019-10-11 06:52:56 +02:00
- ( void ) hideMentionCandidateSelectionView
2019-10-09 05:46:21 +02:00
{
2019-10-11 06:52:56 +02:00
self . mentionCandidateSelectionViewSizeConstraint . constant = 0 ;
2019-12-11 04:07:27 +01:00
self . mentionCandidateSelectionView . alpha = 0 ;
2019-10-09 06:16:07 +02:00
[ self setNeedsLayout ] ;
[ self layoutIfNeeded ] ;
2019-10-11 06:52:56 +02:00
[ self . mentionCandidateSelectionView . tableView setContentOffset : CGPointMake ( 0 , 0 ) ] ;
2019-10-09 05:46:21 +02:00
}
2019-10-11 06:52:56 +02:00
- ( void ) handleMentionCandidateSelected : ( LKMention * ) mentionCandidate from : ( LKMentionCandidateSelectionView * ) mentionCandidateSelectionView
2019-10-09 05:46:21 +02:00
{
2019-10-11 06:52:56 +02:00
[ self . inputToolbarDelegate handleMentionCandidateSelected : mentionCandidate from : mentionCandidateSelectionView ] ;
2019-10-09 05:46:21 +02:00
}
2017-10-10 22:13:54 +02:00
@ end
NS_ASSUME _NONNULL _END