//
// C o p y r i g h t ( c ) 2 0 1 9 O p e n W h i s p e r S y s t e m s . A l l r i g h t s r e s e r v e d .
//
import Foundation
import UIKit
protocol AttachmentCaptionToolbarDelegate : class {
func attachmentCaptionToolbarDidEdit ( _ attachmentCaptionToolbar : AttachmentCaptionToolbar )
func attachmentCaptionToolbarDidComplete ( )
}
// MARK: -
class AttachmentCaptionToolbar : UIView , UITextViewDelegate {
private let kMaxCaptionCharacterCount = 240
weak var attachmentCaptionToolbarDelegate : AttachmentCaptionToolbarDelegate ?
var messageText : String ? {
get { return textView . text }
set {
textView . text = newValue
}
}
// L a y o u t C o n s t a n t s
let kMinTextViewHeight : CGFloat = 38
var maxTextViewHeight : CGFloat {
// A b o u t ~ 4 l i n e s i n p o r t r a i t a n d ~ 3 l i n e s i n l a n d s c a p e .
// O t h e r w i s e w e r i s k o b s c u r i n g t o o m u c h o f t h e c o n t e n t .
return UIDevice . current . orientation . isPortrait ? 160 : 100
}
var textViewHeightConstraint : NSLayoutConstraint !
var textViewHeight : CGFloat
// MARK: - I n i t i a l i z e r s
init ( ) {
self . textViewHeight = kMinTextViewHeight
super . init ( frame : CGRect . zero )
// S p e c i f y i n g a u t o r s i z i n g m a s k a n d a n i n t r i n s i c c o n t e n t s i z e a l l o w s p r o p e r
// s i z i n g w h e n u s e d a s a n i n p u t a c c e s s o r y v i e w .
self . autoresizingMask = . flexibleHeight
self . translatesAutoresizingMaskIntoConstraints = false
self . backgroundColor = UIColor . clear
textView . delegate = self
// L a y o u t
let kToolbarMargin : CGFloat = 8
self . textViewHeightConstraint = textView . autoSetDimension ( . height , toSize : kMinTextViewHeight )
lengthLimitLabel . setContentHuggingHigh ( )
lengthLimitLabel . setCompressionResistanceHigh ( )
let contentView = UIStackView ( arrangedSubviews : [ textContainer , lengthLimitLabel ] )
// W e h a v e t o w r a p t h e t o o l b a r i t e m s i n a c o n t e n t v i e w b e c a u s e i O S ( a t l e a s t o n i O S 1 0 . 3 ) a s s i g n s t h e i n p u t A c c e s s o r y V i e w . l a y o u t M a r g i n s
// w h e n r e s i g n i n g f i r s t r e s p o n d e r ( v e r i f i e d b y a u d i t i n g w i t h ` l a y o u t M a r g i n s D i d C h a n g e ` ) .
// T h e e f f e c t o f t h i s i s t h a t i f w e w e r e t o a s s i g n t h e s e m a r g i n s t o s e l f . l a y o u t M a r g i n s , t h e y ' d b e b l o w n a w a y i f t h e
// u s e r d i s m i s s e s t h e k e y b o a r d , g i v i n g t h e i n p u t a c c e s s o r y v i e w a w o n k y l a y o u t .
contentView . layoutMargins = UIEdgeInsets ( top : kToolbarMargin , left : kToolbarMargin , bottom : kToolbarMargin , right : kToolbarMargin )
contentView . axis = . vertical
addSubview ( contentView )
contentView . autoPinEdgesToSuperviewEdges ( )
}
required init ? ( coder aDecoder : NSCoder ) {
notImplemented ( )
}
// MARK: - U I V i e w O v e r r i d e s
override var intrinsicContentSize : CGSize {
get {
// S i n c e w e h a v e ` s e l f . a u t o r e s i z i n g M a s k = U I V i e w A u t o r e s i z i n g F l e x i b l e H e i g h t ` , w e m u s t s p e c i f y
// a n i n t r i n s i c C o n t e n t S i z e . S p e c i f y i n g C G S i z e . z e r o c a u s e s t h e h e i g h t t o b e d e t e r m i n e d b y a u t o l a y o u t .
return CGSize . zero
}
}
// MARK: - S u b v i e w s
private lazy var lengthLimitLabel : UILabel = {
let lengthLimitLabel = UILabel ( )
// L e n g t h L i m i t L a b e l s h o w n w h e n t h e u s e r i n p u t s t o o l o n g o f a m e s s a g e
lengthLimitLabel . textColor = . white
lengthLimitLabel . text = NSLocalizedString ( " ATTACHMENT_APPROVAL_MESSAGE_LENGTH_LIMIT_REACHED " , comment : " One-line label indicating the user can add no more text to the media message field. " )
lengthLimitLabel . textAlignment = . center
// A d d s h a d o w i n c a s e o v e r l a y e d o n w h i t e c o n t e n t
lengthLimitLabel . layer . shadowColor = UIColor . black . cgColor
lengthLimitLabel . layer . shadowOffset = . zero
lengthLimitLabel . layer . shadowOpacity = 0.8
lengthLimitLabel . layer . shadowRadius = 2.0
lengthLimitLabel . isHidden = true
return lengthLimitLabel
} ( )
lazy var textView : UITextView = {
let textView = buildTextView ( )
textView . returnKeyType = . done
textView . scrollIndicatorInsets = UIEdgeInsets ( top : 5 , left : 0 , bottom : 5 , right : 3 )
return textView
} ( )
private lazy var textContainer : UIView = {
let textContainer = UIView ( )
textContainer . clipsToBounds = true
textContainer . addSubview ( textView )
textView . autoPinEdgesToSuperviewEdges ( )
return textContainer
} ( )
private func buildTextView ( ) -> UITextView {
let textView = AttachmentTextView ( )
textView . keyboardAppearance = isLightMode ? . default : . dark
textView . backgroundColor = . clear
textView . tintColor = . white
textView . font = UIFont . ows_dynamicTypeBody
textView . textColor = . white
textView . textContainerInset = UIEdgeInsets ( top : 7 , left : 7 , bottom : 7 , right : 7 )
return textView
}
// MARK: - U I T e x t V i e w D e l e g a t e
public func textViewDidChange ( _ textView : UITextView ) {
updateHeight ( textView : textView )
attachmentCaptionToolbarDelegate ? . attachmentCaptionToolbarDidEdit ( self )
}
public func textView ( _ textView : UITextView , shouldChangeTextIn range : NSRange , replacementText text : String ) -> Bool {
if ! FeatureFlags . sendingMediaWithOversizeText {
let existingText : String = textView . text ? ? " "
let proposedText : String = ( existingText as NSString ) . replacingCharacters ( in : range , with : text )
// D o n ' t c o m p l i c a t e t h i n g s b y m i x i n g m e d i a a t t a c h m e n t s w i t h o v e r s i z e t e x t a t t a c h m e n t s
guard proposedText . utf8 . count < kOversizeTextMessageSizeThreshold else {
Logger . debug ( " long text was truncated " )
self . lengthLimitLabel . isHidden = false
// ` r a n g e ` r e p r e s e n t s t h e s e c t i o n o f t h e e x i s t i n g t e x t w e w i l l r e p l a c e . W e c a n r e - u s e t h a t s p a c e .
// R a n g e i s i n u n i t s o f N S S t r i n g s ' s s t a n d a r d U T F - 1 6 c h a r a c t e r s . S i n c e s o m e o f t h o s e c h a r s c o u l d b e
// r e p r e s e n t e d a s s i n g l e b y t e s i n u t f - 8 , w h i l e o t h e r s m a y b e 8 o r m o r e , t h e o n l y w a y t o b e s u r e i s
// t o j u s t m e a s u r e t h e u t f 8 e n c o d e d b y t e s o f t h e r e p l a c e d s u b s t r i n g .
let bytesAfterDelete : Int = ( existingText as NSString ) . replacingCharacters ( in : range , with : " " ) . utf8 . count
// A c c e p t a s m u c h o f t h e i n p u t a s w e c a n
let byteBudget : Int = Int ( kOversizeTextMessageSizeThreshold ) - bytesAfterDelete
if byteBudget >= 0 , let acceptableNewText = text . truncated ( toByteCount : UInt ( byteBudget ) ) {
textView . text = ( existingText as NSString ) . replacingCharacters ( in : range , with : acceptableNewText )
}
return false
}
self . lengthLimitLabel . isHidden = true
// A f t e r v e r i f y i n g t h e b y t e - l e n g t h i s s u f f i c i e n t l y s m a l l , v e r i f y t h e c h a r a c t e r c o u n t i s w i t h i n b o u n d s .
guard proposedText . count < kMaxCaptionCharacterCount else {
Logger . debug ( " hit attachment message body character count limit " )
self . lengthLimitLabel . isHidden = false
// ` r a n g e ` r e p r e s e n t s t h e s e c t i o n o f t h e e x i s t i n g t e x t w e w i l l r e p l a c e . W e c a n r e - u s e t h a t s p a c e .
let charsAfterDelete : Int = ( existingText as NSString ) . replacingCharacters ( in : range , with : " " ) . count
// A c c e p t a s m u c h o f t h e i n p u t a s w e c a n
let charBudget : Int = Int ( kMaxCaptionCharacterCount ) - charsAfterDelete
if charBudget >= 0 {
let acceptableNewText = String ( text . prefix ( charBudget ) )
textView . text = ( existingText as NSString ) . replacingCharacters ( in : range , with : acceptableNewText )
}
return false
}
}
// T h o u g h w e c a n w r a p t h e t e x t , w e d o n ' t w a n t t o e n c o u r a g e m u l t l i n e c a p t i o n s , p l u s a " d o n e " b u t t o n
// a l l o w s t h e u s e r t o g e t t h e k e y b o a r d o u t o f t h e w a y w h i l e i n t h e a t t a c h m e n t a p p r o v a l v i e w .
if text = = " \n " {
attachmentCaptionToolbarDelegate ? . attachmentCaptionToolbarDidComplete ( )
return false
} else {
return true
}
}
// MARK: - H e l p e r s
private func updateHeight ( textView : UITextView ) {
// c o m p u t e n e w h e i g h t a s s u m i n g w i d t h i s u n c h a n g e d
let currentSize = textView . frame . size
let newHeight = clampedTextViewHeight ( fixedWidth : currentSize . width )
if newHeight != textViewHeight {
Logger . debug ( " TextView height changed: \( textViewHeight ) -> \( newHeight ) " )
textViewHeight = newHeight
textViewHeightConstraint ? . constant = textViewHeight
invalidateIntrinsicContentSize ( )
}
}
private func clampedTextViewHeight ( fixedWidth : CGFloat ) -> CGFloat {
let contentSize = textView . sizeThatFits ( CGSize ( width : fixedWidth , height : CGFloat . greatestFiniteMagnitude ) )
return CGFloatClamp ( contentSize . height , kMinTextViewHeight , maxTextViewHeight )
}
}