@ -197,6 +197,49 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
}
self . setCurrentItem ( firstItem , direction : . forward , animated : false )
// A s a r e f r e s h e r , t h e _ I n f o r m a t i o n A r c h i t e c t u r e _ h e r e i s :
//
// Y o u a r e a p p r o v i n g a n " A l b u m " , w h i c h h a s m u l t i p l e " A t t a c h m e n t s "
//
// T h e " m e d i a m e s s a g e t e x t " a n d t h e " m e d i a r a i l " b e l o n g t o t h e A l b u m a s a w h o l e , w h e r e a s
// e a c h c a p t i o n b e l o n g s t o t h e i n d i v i d u a l A t t a c h m e n t .
//
// T h e _ U I A r c h i t e c t u r e _ r e f l e c t s t h i s h i e r a r c h y b y p u t t i n g t h e M e d i a R a i l a n d
// M e d i a M e s s a g e T e x t i n p u t i n t o t h e b o t t o m T o o l V i e w w h i c h i s t h e 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 ' s
// i n p u t A c c e s s o r y V i e w .
//
// W h e r e a s a C a p t i o n V i e w l i v e s i n e a c h p a g e o f t h e P a g e V i e w C o n t r o l l e r , p e r A t t a c h m e n t .
//
// S o a s y o u p a g e , t h e C a p t i o n V i e w s m o v e o u t o f v i e w w i t h i t s p a g e , w h e r e a s t h e i n p u t
// a c c e s s o r y v i e w ( r a i l / m e d i a m e s s a g e t e x t ) w i l l r e m a i n f i x e d i n t h e v i e w p o r t .
//
// H o w e v e r ( a n d h e r e ' s t h e k i c k e r ) , a t r e s t , t h e m e d i a ' s C a p t i o n V i e w r e s t s j u s t a b o v e t h e
// i n p u t a c c e s s o r y v i e w . S o w h e n t h i n g s a r e s t a t i c , t h e y a p p e a r a s a s i n g l e p i e c e o f
// i n t e r f a c e .
//
// I ' m n o t t o t a l l y s u r e i f t h i s i s w h a t M y l e s h a d i n m i n d , b u t t h e s c r e e n s h o t s l e f t a l o t o f
// b e h a v i o r a m b i g u o u s , a n d t h i s w a s m y b e s t i n t e r p r e t a t i o n .
//
// B e c a u s e o f t h i s c o m p l e x i t y , i t i s i n s u f f i c i e n t t o o b s e r v e o n l y t h e
// K e y b o a r d W i l l C h a n g e F r a m e , s i n c e t h e k e y b o a r d c o u l d b e c h a n g i n g f r a m e w h e n t h e C a p t i o n V i e w
// b e c a m e / r e s i g n e d f i r s t r e s p o n d e r , w h e n A t t a c h m e n t A p p r o v a l V i e w C o n t r o l l e r b e c a m e / r e s i g n e d
// f i r s t r e s p o n d e r , o r w h e 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 ' s i n p u t A c c e s s o r y V i e w . t e x t V i e w
// b e c a m e / r e s i g n e d f i r s t r e s p o n d e r , a n d b e c a u s e t h e s e t h i n g s c a n h a p p e n i n i m m e d i a t r e
// s e q u e n c e , g e t t i n g a s i n g l e s m o o t h a n i m a t i o n r e q u i r e s h a n d l i n g e a c h n o t i f i c a t i o n s l i g h t l y
// d i f f e r e n t l y .
NotificationCenter . default . addObserver ( self ,
selector : #selector ( keyboardWillShow ( notification : ) ) ,
name : . UIKeyboardWillShow ,
object : nil )
NotificationCenter . default . addObserver ( self ,
selector : #selector ( keyboardDidShow ( notification : ) ) ,
name : . UIKeyboardDidShow ,
object : nil )
NotificationCenter . default . addObserver ( self ,
selector : #selector ( keyboardWillHide ( notification : ) ) ,
name : . UIKeyboardWillHide ,
object : nil )
}
override public func viewWillAppear ( _ animated : Bool ) {
@ -230,6 +273,66 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
return true
}
var lastObservedKeyboardTop : CGFloat = 0
var inputAccessorySnapshotView : UIView ?
@objc
func keyboardDidShow ( notification : Notification ) {
// I f t h i s i s a r e s u l t o f t h e v c b e c o m i n g f i r s t r e s p o n d e r , t h e k e y b o a r d i s n ' t a c t u a l l y
// s h o w i n g , r a t h e r t h e i n p u t A c c e s s o r y V i e w i s n o w s h o w i n g , s o w e w a n t t o r e m o v e a n y
// p r e v i o u s l y a d d e d t o o l b a r s n a p s h o t .
if isFirstResponder , inputAccessorySnapshotView != nil {
removeToolbarSnapshot ( )
}
}
@objc
func keyboardWillShow ( notification : Notification ) {
guard let userInfo = notification . userInfo else {
owsFailDebug ( " userInfo was unexpectedly nil " )
return
}
guard let keyboardStartFrame = userInfo [ UIKeyboardFrameBeginUserInfoKey ] as ? CGRect else {
owsFailDebug ( " keyboardEndFrame was unexpectedly nil " )
return
}
guard let keyboardEndFrame = userInfo [ UIKeyboardFrameEndUserInfoKey ] as ? CGRect else {
owsFailDebug ( " keyboardEndFrame was unexpectedly nil " )
return
}
Logger . debug ( " \( keyboardStartFrame ) -> \( keyboardEndFrame ) " )
lastObservedKeyboardTop = keyboardEndFrame . size . height
currentPageController . updateCaptionViewBottomInset ( )
}
@objc
func keyboardWillHide ( notification : Notification ) {
guard let userInfo = notification . userInfo else {
owsFailDebug ( " userInfo was unexpectedly nil " )
return
}
guard let keyboardStartFrame = userInfo [ UIKeyboardFrameBeginUserInfoKey ] as ? CGRect else {
owsFailDebug ( " keyboardEndFrame was unexpectedly nil " )
return
}
guard let keyboardEndFrame = userInfo [ UIKeyboardFrameEndUserInfoKey ] as ? CGRect else {
owsFailDebug ( " keyboardEndFrame was unexpectedly nil " )
return
}
Logger . debug ( " \( keyboardStartFrame ) -> \( keyboardEndFrame ) " )
lastObservedKeyboardTop = keyboardEndFrame . size . height + keyboardStartFrame . minY - keyboardEndFrame . minY
currentPageController . updateCaptionViewBottomInset ( )
}
// MARK: - V i e w H e l p e r s
func remove ( attachmentItem : SignalAttachmentItem ) {
@ -263,29 +366,27 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
} )
}
func addDeleteIcon ( cellViews : [ GalleryRailCellView ] ) {
for cellView in cellViews {
guard let attachmentItem = cellView . item as ? SignalAttachmentItem else {
owsFailDebug ( " attachmentItem was unexpectedly nil " )
return
}
func addDeleteIcon ( cellView : GalleryRailCellView ) {
guard let attachmentItem = cellView . item as ? SignalAttachmentItem else {
owsFailDebug ( " attachmentItem was unexpectedly nil " )
return
}
let button = OWSButton { [ weak self ] in
guard let strongSelf = self else { return }
strongSelf . remove ( attachmentItem : attachmentItem )
}
button . setImage ( # imageLiteral ( resourceName : " ic_small_x " ) , for : . normal )
let button = OWSButton { [ weak self ] in
guard let strongSelf = self else { return }
strongSelf . remove ( attachmentItem : attachmentItem )
}
button . setImage ( # imageLiteral ( resourceName : " ic_small_x " ) , for : . normal )
let kInsetDistance : CGFloat = 5
button . imageEdgeInsets = UIEdgeInsets ( top : kInsetDistance , left : kInsetDistance , bottom : kInsetDistance , right : kInsetDistance )
let kInsetDistance : CGFloat = 5
button . imageEdgeInsets = UIEdgeInsets ( top : kInsetDistance , left : kInsetDistance , bottom : kInsetDistance , right : kInsetDistance )
cellView . addSubview ( button )
cellView . addSubview ( button )
let kButtonWidth : CGFloat = 9 + kInsetDistance * 2
button . autoSetDimensions ( to : CGSize ( width : kButtonWidth , height : kButtonWidth ) )
button . autoPinEdge ( toSuperviewMargin : . top )
button . autoPinEdge ( toSuperviewMargin : . trailing )
}
let kButtonWidth : CGFloat = 9 + kInsetDistance * 2
button . autoSetDimensions ( to : CGSize ( width : kButtonWidth , height : kButtonWidth ) )
button . autoPinEdge ( toSuperviewMargin : . top )
button . autoPinEdge ( toSuperviewMargin : . trailing )
}
var pagerScrollView : UIScrollView ?
@ -323,6 +424,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
// u s e c o m p a c t s c a l e w h e n k e y b o a r d i s p o p p e d .
let scale : AttachmentPrepViewController . AttachmentViewScale = self . isFirstResponder ? . fullsize : . compact
pendingPage . setAttachmentViewScale ( scale , animated : false )
pendingPage . updateCaptionViewBottomInset ( )
}
}
@ -428,8 +530,12 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
return
}
galleryRailView . configureCellViews ( itemProvider : attachmentItemCollection , focusedItem : currentItem )
addDeleteIcon ( cellViews : galleryRailView . cellViews )
let cellViewDecoratorBlock = { ( cellView : GalleryRailCellView ) in
self . addDeleteIcon ( cellView : cellView )
}
galleryRailView . configureCellViews ( itemProvider : attachmentItemCollection ,
focusedItem : currentItem ,
cellViewDecoratorBlock : cellViewDecoratorBlock )
galleryRailView . isHidden = attachmentItemCollection . attachmentItems . count < 2
}
@ -514,6 +620,100 @@ extension AttachmentApprovalViewController: AttachmentPrepViewControllerDelegate
func prepViewController ( _ prepViewController : AttachmentPrepViewController , didUpdateCaptionForAttachmentItem attachmentItem : SignalAttachmentItem ) {
self . approvalDelegate ? . attachmentApproval ? ( self , changedCaptionOfAttachment : attachmentItem . attachment )
}
func prepViewController ( _ prepViewController : AttachmentPrepViewController , willBeginEditingCaptionView captionView : CaptionView ) {
// W h e n t h e C a p t i o n V i e w b e c o m e s f i r s t r e s p o n d e r , 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 C o n t r o l l e r w i l l
// c o n s e q u e n t l y r e s i g n F i r s t R e s p o n d e r , w h i c h m e a n s t h e b o t t o m T o o l V i e w w o u l d d i s a p p e a r f r o m
// t h e s c r e e n , s o b e f o r e t h a t h a p p e n s , w e a d d a s n a p s h o t t o h o l d s i t ' s p l a c e .
addInputAccessorySnapshot ( )
}
func prepViewController ( _ prepViewController : AttachmentPrepViewController , didBeginEditingCaptionView captionView : CaptionView ) {
// D i s a b l e p a g i n g w h i l e c a p t i o n s a r e b e i n g e d i t e d t o a v o i d a c l u n k y a n i m a t i o n .
//
// L o a d i n g t h e n e x t p a g e c a u s e s t h e C a p t i o n V i e w t o r e s i g n f i r s t r e s p o n d e r , w h i c h i n t u r n
// d i s m i s s e s t h e k e y b o a r d , w h i c h i n t u r n a f f e c t s t h e v e r t i c a l o f f s e t o f b o t h t h e C a p t i o n V i e w
// f r o m t h e p a g e w e ' r e l e a v i n g a s w e l l a s t h e p a g e w e ' r e e n t e r i n g . I n s t e a d w e r e q u i r e t h e
// u s e r t o d i s m i s s * t h e n * s w i p e .
disablePaging ( )
}
func addInputAccessorySnapshot ( ) {
assert ( inputAccessorySnapshotView = = nil )
// T o f i x a l a y o u t g l i t c h w h e r e t h e s n a p s h o t v i e w i s 1 / 2 t h e w i d t h o f t h e s c r e e n , i t ' s k e y
// t h a t w e u s e ` b o t t o m T o o l V i e w ` a n d n o t ` i n p u t A c c e s s o r y V i e w ` w h i c h c a n t r i g g e r a l a y o u t o f
// t h e ` b o t t o m T o o l V i e w ` .
// P r e s u m a b l y t h e f r a m e o f t h e i n p u t A c c e s s o r y V i e w h a s j u s t c h a n g e d b e c a u s e w e ' r e i n t h e
// m i d d l e o f s w i t c h i n g f i r s t r e s p o n d e r s . W e w a n t a s n a p s h o t a s i t * w a s * , n o t r e f l e c t i n g a n y
// j u s t - a p p l i e d s u p e r v i e w l a y o u t c h a n g e s .
inputAccessorySnapshotView = bottomToolView . snapshotView ( afterScreenUpdates : true )
guard let inputAccessorySnapshotView = inputAccessorySnapshotView else {
owsFailDebug ( " inputAccessorySnapshotView was unexpectedly nil " )
return
}
view . addSubview ( inputAccessorySnapshotView )
inputAccessorySnapshotView . autoSetDimension ( . height , toSize : bottomToolView . bounds . height )
inputAccessorySnapshotView . autoPinEdgesToSuperviewEdges ( with : . zero , excludingEdge : . top )
}
func removeToolbarSnapshot ( ) {
guard let inputAccessorySnapshotView = self . inputAccessorySnapshotView else {
owsFailDebug ( " inputAccessorySnapshotView was unexpectedly nil " )
return
}
inputAccessorySnapshotView . removeFromSuperview ( )
self . inputAccessorySnapshotView = nil
}
func prepViewController ( _ prepViewController : AttachmentPrepViewController , didEndEditingCaptionView captionView : CaptionView ) {
enablePaging ( )
}
var desiredCaptionViewBottomInset : CGFloat {
let safeAreaInset : CGFloat
if #available ( iOS 11 , * ) {
safeAreaInset = view . safeAreaInsets . bottom
} else {
safeAreaInset = 0
}
// C a p t i o n V i e w b o t t o m o f f s e t s c e n a r i o s :
//
// 1 . w h e n n o k e y b o a r d i s p o p p e d ( e . g . i n i t i a l l y ) t o b e * j u s t * a b o v e t h e r a i l
// 2 . w h e n t h e C a p t i o n V i e w b e c o m e s f i r s t r e s p o n d e r , t o b e * j u s t * a b o v e t h e k e y b o a r d , s o t h e
// u s e r c a n s e e w h a t t h e y ' r e t y p i n g .
//
// F o r b o t h t h e s e c a s e s w e a p p l y t h e ` l a s t O b s e r v e d K e y b o a r d T o p `
guard bottomToolView . mediaMessageTextToolbar . textView . isFirstResponder else {
// 3 . I m m e d i a t e l y a f t e r d i s m i s s i n g t h e C a p t i o n V i e w b u t b e f o r e t h e V i e w C o n t r o l l e r
// r e g a i n s f i r s t R e s p o n d e r , t h e r e i s a n i n s t a n t w h e r e t h e i n p u t A c c e s s o r y V i e w i s
// n o t s h o w n , s o t h e l a s t O b s e r v e d K e y b o a r d T o p i s e f f e c t i v e l y 0 . A m o m e n t l a t e r
// w h e n t h e V i e w C o n t r o l l e r r e g a i n s f i r s t R e s p o n d e r , t h e i n p u t A c c e s s o r y V i e w w i l l b e
// p r e s e n t e d . N a i v e l y , t h i s w o u l d r e s u l t i n t h e C a p t i o n V i e w u n d e s i r a b l y b o u n c i n g t o
// t h e b o t t o m o f t h e V i e w C o n t r o l l e r , a n d t h e n i m m e d i a t e l y b a c k u p a s t h e
// i n p u t A c c e s s o r y V i e w i s p r e s e n t e d .
// I n s t e a d , w e p o s i t i o n t h e C a p t i o n V i e w w h e r e i t w i l l e n d u p , b y u s i n g
// ` b o t t o m T o o l V i e w . h e i g h t ` , w h i c h w i l l o n l y b e g r e a t e r t h a n
// ` l a s t O b s e r v e r e d K e y b o a r d T o p ` w h e n t h e k e y b o a r d i s n o t p r e s e n t e d .
return max ( bottomToolView . bounds . height , lastObservedKeyboardTop ) - safeAreaInset
}
// 4 . w h e n t h e M e s s a g e T e x t V i e w b e c o m e s f i r s t r e s p o n d e r , t h e k e y b o a r d s h o u l d s h i f t u p
// " i n f r o n t " o f t h e C a p t i o n V i e w
return bottomToolView . bounds . height - safeAreaInset
}
// MARK: H e l p e r s
func disablePaging ( ) {
pagerScrollView ? . panGestureRecognizer . isEnabled = false
}
func enablePaging ( ) {
self . pagerScrollView ? . panGestureRecognizer . isEnabled = true
}
}
// MARK: G a l l e r y R a i l
@ -561,6 +761,12 @@ extension AttachmentApprovalViewController: GalleryRailViewDelegate {
protocol AttachmentPrepViewControllerDelegate : class {
func prepViewController ( _ prepViewController : AttachmentPrepViewController , didUpdateCaptionForAttachmentItem attachmentItem : SignalAttachmentItem )
func prepViewController ( _ prepViewController : AttachmentPrepViewController , willBeginEditingCaptionView captionView : CaptionView )
func prepViewController ( _ prepViewController : AttachmentPrepViewController , didBeginEditingCaptionView captionView : CaptionView )
func prepViewController ( _ prepViewController : AttachmentPrepViewController , didEndEditingCaptionView captionView : CaptionView )
var desiredCaptionViewBottomInset : CGFloat { get }
}
public class AttachmentPrepViewController : OWSViewController , PlayerProgressBarDelegate , OWSVideoPlayerDelegate {
@ -598,12 +804,22 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD
fatalError ( " init(coder:) has not been implemented " )
}
// MARK: - Vi e w L i f e c y c l e
// MARK: - Su b v i e w s
lazy var captionView : CaptionView = {
return CaptionView ( attachmentItem : attachmentItem )
} ( )
lazy var touchInterceptorView : UIView = {
let touchInterceptorView = UIView ( )
let tapGesture = UITapGestureRecognizer ( target : self , action : #selector ( didTapTouchInterceptorView ( gesture : ) ) )
touchInterceptorView . addGestureRecognizer ( tapGesture )
return touchInterceptorView
} ( )
// MARK: - V i e w L i f e c y c l e
override public func loadView ( ) {
self . view = UIView ( )
@ -707,20 +923,17 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD
// C a p t i o n
view . addSubview ( touchInterceptorView )
touchInterceptorView . autoPinEdgesToSuperviewEdges ( )
touchInterceptorView . isHidden = true
view . addSubview ( captionView )
captionView . delegate = self
captionView . autoPinWidthToSuperview ( )
// M J K T O D O i d e a l C a p t i o n V i e w p l a c e m e n t
// 1 . w h e n n o k e y b o a r d i s p o p p e d ( e . g . i n i t i a l l y ) t o b e * j u s t * a b o v e t h e r a i l
// 2 . w h e n t h e C a p t i o n T e x t V i e w i s f i r s t r e s p o n d e r , t o b e * j u s t * a b o v e t h e k e y b o a r d
// 3 . w h e n t h e M e s s a g e T e x t V i e w i s f i r s t r e s p o n d e r , t o b e b e h i n d t h e k e y b o a r d
captionView . autoPinEdge ( toSuperviewMargin : . bottom , withInset : 136 )
captionViewBottomConstraint = captionView . autoPinEdge ( toSuperviewMargin : . bottom )
}
var captionViewBottomConstraint : NSLayoutConstraint !
override public func viewWillLayoutSubviews ( ) {
Logger . debug ( " " )
super . viewWillLayoutSubviews ( )
@ -731,15 +944,49 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD
ensureAttachmentViewScale ( animated : false )
}
// MARK: -
// MARK: Ca p t i o n V i e w l i f t s w i t h k e y b o a r d
@objc public func didTapPlayerView ( _ gestureRecognizer : UIGestureRecognizer ) {
assert ( self . videoPlayer != nil )
self . pauseVideo ( )
var hasLaidOutCaptionView : Bool = false
var captionViewBottomConstraint : NSLayoutConstraint !
func updateCaptionViewBottomInset ( ) {
guard let prepDelegate = self . prepDelegate else {
owsFailDebug ( " prepDelegate was unexpectedly nil " )
return
}
let changeBlock = {
let offset : CGFloat = - 1 * prepDelegate . desiredCaptionViewBottomInset
self . captionViewBottomConstraint . constant = offset
self . captionView . superview ? . layoutIfNeeded ( )
}
// T o a v o i d a n a n i m a t i o n g l i t c h , w e a p p l y t h i s u p d a t e w i t h o u t a n i m a t i o n b e f o r e i n i t i a l
// a p p e a r a n c e . B u t a f t e r t h a t , w e w a n t t o a p p l y t h e c o n s t r a i n t c h a n g e w i t h i n t h e e x i s t i n g
// a n i m a t i o n c o n t e x t , s i n c e w e c a l l t h i s w h i l e h a n d l i n g a U I K e y b o a r d n o t i f i c a t i o n , w h i c h
// a l l o w s u s t o s l i d e u p t h e C a p t i o n V i e w i n l o c k s t e p w i t h t h e k e y b o a r d .
if hasLaidOutCaptionView {
changeBlock ( )
} else {
hasLaidOutCaptionView = true
UIView . performWithoutAnimation { changeBlock ( ) }
}
}
// MARK: - E v e n t H a n d l e r s
@objc
func didTapTouchInterceptorView ( gesture : UITapGestureRecognizer ) {
Logger . info ( " " )
captionView . endEditing ( )
touchInterceptorView . isHidden = true
}
@objc
public func didTapPlayerView ( _ gestureRecognizer : UIGestureRecognizer ) {
assert ( self . videoPlayer != nil )
self . pauseVideo ( )
}
@objc
public func playButtonTapped ( ) {
self . playVideo ( )
@ -885,11 +1132,29 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD
}
extension AttachmentPrepViewController : CaptionViewDelegate {
func captionViewWillBeginEditing ( _ captionView : CaptionView ) {
prepDelegate ? . prepViewController ( self , willBeginEditingCaptionView : captionView )
}
func captionView ( _ captionView : CaptionView , didChangeCaptionText captionText : String ? , attachmentItem : SignalAttachmentItem ) {
let attachment = attachmentItem . attachment
attachment . captionText = captionText
prepDelegate ? . prepViewController ( self , didUpdateCaptionForAttachmentItem : attachmentItem )
}
func captionViewDidBeginEditing ( _ captionView : CaptionView ) {
// D o n ' t a l l o w u s e r t o p a n u n t i l t h e y ' v e d i s m i s s e d t h e k e y b o a r d .
// T h i s a v o i d s a r e a l l y u g l y a n i m a t i o n f r o m s i m u l t a n e o u s l y d i s m i s s i n g t h e k e y b o a r d
// w h i l e l o a d i n g a n e w P r e p V i e w C o n t r o l l e r , a n d i t ' s C a p t i o n V i e w , w h o s e l a y o u t d e p e n d s
// o n t h e k e y b o a r d ' s p o s i t i o n .
touchInterceptorView . isHidden = false
prepDelegate ? . prepViewController ( self , didBeginEditingCaptionView : captionView )
}
func captionViewDidEndEditing ( _ captionView : CaptionView ) {
touchInterceptorView . isHidden = true
prepDelegate ? . prepViewController ( self , didEndEditingCaptionView : captionView )
}
}
extension AttachmentPrepViewController : UIScrollViewDelegate {
@ -987,10 +1252,7 @@ class BottomToolView: UIView {
stackView . axis = . vertical
addSubview ( stackView )
stackView . autoPinEdge ( toSuperviewEdge : . leading )
stackView . autoPinEdge ( toSuperviewEdge : . trailing )
stackView . autoPinEdge ( toSuperviewEdge : . top )
stackView . autoPinEdge ( toSuperviewMargin : . bottom )
stackView . autoPinEdgesToSuperviewEdges ( )
}
required init ? ( coder aDecoder : NSCoder ) {
@ -1010,6 +1272,9 @@ class BottomToolView: UIView {
protocol CaptionViewDelegate : class {
func captionView ( _ captionView : CaptionView , didChangeCaptionText captionText : String ? , attachmentItem : SignalAttachmentItem )
func captionViewWillBeginEditing ( _ captionView : CaptionView )
func captionViewDidBeginEditing ( _ captionView : CaptionView )
func captionViewDidEndEditing ( _ captionView : CaptionView )
}
class CaptionView : UIView {
@ -1032,8 +1297,22 @@ class CaptionView: UIView {
private let kMinTextViewHeight : CGFloat = 38
private var textViewHeightConstraint : NSLayoutConstraint !
// T O D O s h o w l e n g t h l i m i t l a b e l
private let lengthLimitLabel : UILabel = UILabel ( )
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_CAPTION_LENGTH_LIMIT_REACHED " , comment : " One-line label indicating the user can add no more text to the attachment caption. " )
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 = CGSize ( width : 0.0 , height : 0.0 )
lengthLimitLabel . layer . shadowOpacity = 0.8
lengthLimitLabel . isHidden = true
return lengthLimitLabel
} ( )
// MARK: I n i t i a l i z e r s
@ -1042,17 +1321,31 @@ class CaptionView: UIView {
super . init ( frame : . zero )
backgroundColor = UIColor . black . withAlphaComponent ( 0.6 )
self . captionText = attachmentItem . captionText
textView . delegate = self
addSubview ( placeholderTextView )
placeholderTextView . autoPinEdgesToSuperviewMargins ( )
let textContainer = UIView ( )
textContainer . addSubview ( placeholderTextView )
placeholderTextView . autoPinEdgesToSuperviewEdges ( )
backgroundColor = UIColor . black . withAlphaComponent ( 0.6 )
addSubview ( textView )
textView . autoPinEdgesToSuperviewMargins ( )
textView . delegate = self
textContainer . addSubview ( textView )
textView . autoPinEdgesToSuperviewEdges ( )
textViewHeightConstraint = textView . autoSetDimension ( . height , toSize : kMinTextViewHeight )
self . textViewHeightConstraint = textView . autoSetDimension ( . height , toSize : kMinTextViewHeight )
let hStack = UIStackView ( arrangedSubviews : [ addCaptionButton , textContainer , doneButton ] )
doneButton . isHidden = true
addSubview ( hStack )
hStack . autoPinEdgesToSuperviewMargins ( )
addSubview ( lengthLimitLabel )
lengthLimitLabel . autoPinEdge ( toSuperviewMargin : . left )
lengthLimitLabel . autoPinEdge ( toSuperviewMargin : . right )
lengthLimitLabel . autoPinEdge ( . bottom , to : . top , of : textView , withOffset : - 9 )
lengthLimitLabel . setContentHuggingHigh ( )
lengthLimitLabel . setCompressionResistanceHigh ( )
}
required init ? ( coder aDecoder : NSCoder ) {
@ -1061,7 +1354,12 @@ class CaptionView: UIView {
// MARK:
func endEditing ( ) {
textView . resignFirstResponder ( )
}
override var inputAccessoryView : UIView ? {
// D o n ' t i n h e r i t t h e v c ' s i n p u t A c c e s s o r y V i e w
return nil
}
@ -1095,8 +1393,9 @@ class CaptionView: UIView {
placeholderTextView . backgroundColor = . clear
placeholderTextView . keyboardAppearance = Theme . keyboardAppearance
placeholderTextView . font = UIFont . ows_dynamicTypeBody
// M J K F I X M E a l w a y s d a r k t h e m e
placeholderTextView . textColor = Theme . placeholderColor
placeholderTextView . textColor = Theme . darkThemePrimaryColor
placeholderTextView . tintColor = Theme . darkThemePrimaryColor
placeholderTextView . returnKeyType = . done
return placeholderTextView
@ -1108,27 +1407,65 @@ class CaptionView: UIView {
textView . keyboardAppearance = Theme . keyboardAppearance
textView . font = UIFont . ows_dynamicTypeBody
textView . textColor = Theme . darkThemePrimaryColor
textView . returnKeyType = . done
textView . tintColor = Theme . darkThemePrimaryColor
return textView
} ( )
lazy var addCaptionButton : UIButton = {
let addCaptionButton = OWSButton { [ weak self ] in
self ? . textView . becomeFirstResponder ( )
}
let icon = # imageLiteral ( resourceName : " ic_add_caption " ) . withRenderingMode ( . alwaysTemplate )
addCaptionButton . setImage ( icon , for : . normal )
addCaptionButton . tintColor = Theme . darkThemePrimaryColor
return addCaptionButton
} ( )
lazy var doneButton : UIButton = {
let doneButton = OWSButton { [ weak self ] in
self ? . textView . resignFirstResponder ( )
}
doneButton . setTitle ( CommonStrings . doneButton , for : . normal )
doneButton . tintColor = Theme . darkThemePrimaryColor
return doneButton
} ( )
}
let kMaxCaptionCharacterCount = 240
extension CaptionView : UITextViewDelegate {
public func textViewShouldBeginEditing ( _ textView : UITextView ) -> Bool {
delegate ? . captionViewWillBeginEditing ( self )
return true
}
public func textViewDidBeginEditing ( _ textView : UITextView ) {
updatePlaceholderTextViewVisibility ( )
doneButton . isHidden = false
addCaptionButton . isHidden = true
delegate ? . captionViewDidBeginEditing ( self )
}
public func textViewDidEndEditing ( _ textView : UITextView ) {
updatePlaceholderTextViewVisibility ( )
doneButton . isHidden = true
addCaptionButton . isHidden = false
delegate ? . captionViewDidEndEditing ( self )
}
public func textView ( _ textView : UITextView , shouldChangeTextIn range : NSRange , replacementText text : String ) -> Bool {
let existingText : String = textView . text ? ? " "
let proposedText : String = ( existingText as NSString ) . replacingCharacters ( in : range , with : text )
guard proposedText . utf8 . count <= kOversizeTextMessageSizeThreshold else {
Logger . debug ( " long text was truncated " )
let kMaxCaptionByteCount = kOversizeTextMessageSizeThreshold / 4
guard proposedText . utf8 . count <= kMaxCaptionByteCount else {
Logger . debug ( " hit caption byte 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 .
@ -1145,16 +1482,29 @@ extension CaptionView: UITextViewDelegate {
return false
}
self . lengthLimitLabel . isHidden = true
// 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 " {
textView . resignFirstResponder ( )
// 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 .
// N o r m a l l y t h i s c h a r a c t e r c o u n t s h o u l d e n t a i l * m u c h * l e s s b y t e c o u n t .
guard proposedText . count <= kMaxCaptionCharacterCount else {
Logger . debug ( " hit caption 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
} else {
return true
}
self . lengthLimitLabel . isHidden = true
return true
}
public func textViewDidChange ( _ textView : UITextView ) {
@ -1174,14 +1524,29 @@ class MediaMessageTextToolbar: UIView, UITextViewDelegate {
weak var mediaMessageTextToolbarDelegate : MediaMessageTextToolbarDelegate ?
private let addMoreButton : UIButton
private let sendButton : UIButton
private let textView : UITextView
let textView : UITextView
var messageText : String ? {
get { return self . textView . text }
set { self . textView . text = newValue }
}
private let lengthLimitLabel : UILabel = UILabel ( )
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 = CGSize ( width : 0.0 , height : 0.0 )
lengthLimitLabel . layer . shadowOpacity = 0.8
lengthLimitLabel . isHidden = true
return lengthLimitLabel
} ( )
// L a y o u t C o n s t a n t s
@ -1217,7 +1582,7 @@ class MediaMessageTextToolbar: UIView, UITextViewDelegate {
init ( isAddMoreVisible : Bool ) {
self . addMoreButton = UIButton ( type : . custom )
self . sendButton = UIButton ( type : . system )
self . textView = MessageTextView ( )
self . textView = MessageTextView ( )
self . textViewHeight = kMinTextViewHeight
super . init ( frame : CGRect . zero )
@ -1231,6 +1596,7 @@ class MediaMessageTextToolbar: UIView, UITextViewDelegate {
textView . delegate = self
textView . keyboardAppearance = Theme . keyboardAppearance
textView . backgroundColor = Theme . darkThemeBackgroundColor
textView . tintColor = Theme . darkThemePrimaryColor
textView . layer . borderColor = Theme . darkThemePrimaryColor . cgColor
textView . layer . borderWidth = 0.5
textView . layer . cornerRadius = kMinTextViewHeight / 2
@ -1241,7 +1607,9 @@ class MediaMessageTextToolbar: UIView, UITextViewDelegate {
textView . textContainerInset = UIEdgeInsets ( top : 7 , left : 7 , bottom : 7 , right : 7 )
textView . scrollIndicatorInsets = UIEdgeInsets ( top : 5 , left : 0 , bottom : 5 , right : 3 )
addMoreButton . setImage ( UIImage ( named : " album_add_more " ) , for : . normal )
let addMoreIcon = # imageLiteral ( resourceName : " album_add_more " ) . withRenderingMode ( . alwaysTemplate )
addMoreButton . setImage ( addMoreIcon , for : . normal )
addMoreButton . tintColor = Theme . darkThemePrimaryColor
addMoreButton . addTarget ( self , action : #selector ( didTapAddMore ) , for : . touchUpInside )
let sendTitle = NSLocalizedString ( " ATTACHMENT_APPROVAL_SEND_BUTTON " , comment : " Label for 'send' button in the 'attachment approval' dialog. " )
@ -1255,17 +1623,6 @@ class MediaMessageTextToolbar: UIView, UITextViewDelegate {
// I n c r e a s e h i t a r e a o f s e n d b u t t o n
sendButton . contentEdgeInsets = UIEdgeInsets ( top : 6 , left : 8 , bottom : 6 , right : 8 )
// 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_CAPTION_LENGTH_LIMIT_REACHED " , comment : " One-line label indicating the user can add no more text to the attachment caption. " )
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 = CGSize ( width : 0.0 , height : 0.0 )
lengthLimitLabel . layer . shadowOpacity = 0.8
self . lengthLimitLabel . isHidden = true
let contentView = UIView ( )
contentView . addSubview ( sendButton )
contentView . addSubview ( textView )