@ -49,6 +49,7 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
// S c r o l l i n g & p a g i n g
var isUserScrolling = false
var hasPerformedInitialScroll = false
var didFinishInitialLayout = false
var scrollDistanceToBottomBeforeUpdate : CGFloat ?
var baselineKeyboardHeight : CGFloat = 0
@ -420,16 +421,8 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
override func viewDidAppear ( _ animated : Bool ) {
super . viewDidAppear ( animated )
// P e r f o r m t h e i n i t i a l s c r o l l a n d h i g h l i g h t i f n e e d e d ( i f w e s t a r t e d w i t h a f o c u s e d m e s s a g e
// t h i s w i l l h a v e a l r e a d y b e e n c a l l e d t o i n s t a n t l y s n a p t o t h e d e s t i n a t i o n b u t w e d o n ' t
// t r i g g e r t h e h i g h l i g h t u n t i l a f t e r t h e s c r e e n h a s a p p e a r e d t o m a k e i t m o r e o b v i o u s )
performInitialScrollIfNeeded ( )
// F l a g t h a t t h e i n i t i a l l a y o u t h a s b e e n c o m p l e t e d ( t h e f l a g b l o c k s a n d u n b l o c k s a n u m b e r
// o f d i f f e r e n t b e h a v i o u r s )
//
// N o t e : T h i s M U S T b e s e t a f t e r t h e a b o v e ' p e r f o r m I n i t i a l S c r o l l I f N e e d e d ' i s c a l l e d a s i t
// w o n ' t r u n i f t h i s f l a g i s s e t t o t r u e
didFinishInitialLayout = true
if delayFirstResponder || isShowingSearchUI {
@ -516,13 +509,16 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
// T h e d e f a u l t s c h e d u l e r e m i t s c h a n g e s o n t h e m a i n t h r e a d
self ? . handleThreadUpdates ( threadData )
self ? . performInitialScrollIfNeeded ( )
// N o t e : W e w a n t t o l o a d t h e i n t e r a c t i o n d a t a i n t o t h e U I a f t e r t h e i n i t i a l t h r e a d d a t a
// h a s l o a d e d t o p r e v e n t a n i s s u e w h e r e t h e c o n v e r s a t i o n l o a d s w i t h t h e w r o n g o f f s e t
if self ? . viewModel . onInteractionChange = = nil {
self ? . viewModel . onInteractionChange = { [ weak self ] updatedInteractionData in
self ? . handleInteractionUpdates ( updatedInteractionData )
}
}
}
)
self . viewModel . onInteractionChange = { [ weak self ] updatedInteractionData in
self ? . handleInteractionUpdates ( updatedInteractionData )
}
}
private func stopObservingChanges ( ) {
@ -617,42 +613,42 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
return
}
// D e t e r m i n e i f w e a r e i n s e r t i n g c o n t e n t a t t h e t o p o f t h e c o l l e c t i o n V i e w
struct ItemChangeInfo {
enum InsertLocation {
case top
case bottom
case other
case none
}
// M a r k r e c e i v e d m e s s a g e s a s r e a d
let didSendMessageBeforeUpdate : Bool = self . viewModel . sentMessageBeforeUpdate
self . viewModel . markAllAsRead ( )
self . viewModel . sentMessageBeforeUpdate = false
// W h e n s e n d i n g a m e s s a g e w e w a n t t o r e l o a d t h e U I i n s t a n t l y ( w i t h a n y f o r m o f a n i m a t i o n t h e m e s s a g e
// s e n d i n g f e e l s s o m e w h a t u n r e s p o n s i v e b u t a n i n s t a n t u p d a t e f e e l s s n a p p y )
guard ! didSendMessageBeforeUpdate else {
self . viewModel . updateInteractionData ( updatedData )
self . tableView . reloadData ( )
let insertLocation : InsertLocation
let wasCloseToBottom : Bool
let sentMessageBeforeUpdate : Bool
// N o t e : T h e s c r o l l b u t t o n a l p h a w o n ' t g e t s e t c o r r e c t l y i n t h i s c a s e s o w e f o r c i b l y s e t i t t o
// h a v e a n a l p h a o f 0 t o s t o p i t a p p e a r i n g b u g g y
self . scrollToBottom ( isAnimated : false )
self . scrollButton . alpha = 0
self . unreadCountView . alpha = scrollButton . alpha
return
}
// R e l o a d t h e t a b l e c o n t e n t a n i m a t i n g c h a n g e s i f t h e y ' l l l o o k g o o d
struct ItemChangeInfo {
let isInsertAtTop : Bool
let firstIndexIsVisible : Bool
let visibleInteractionId : Int64
let visibleIndexPath : IndexPath
let oldVisibleIndexPath : IndexPath
let lastVisibleIndexPath : IndexPath
init (
insertLocation : InsertLocation ,
wasCloseToBottom : Bool ,
sentMessageBeforeUpdate : Bool ,
isInsertAtTop : Bool = false ,
firstIndexIsVisible : Bool = false ,
visibleInteractionId : Int64 = - 1 ,
visibleIndexPath : IndexPath = IndexPath ( row : 0 , section : 0 ) ,
oldVisibleIndexPath : IndexPath = IndexPath ( row : 0 , section : 0 ) ,
lastVisibleIndexPath : IndexPath = IndexPath ( row : 0 , section : 0 )
oldVisibleIndexPath : IndexPath = IndexPath ( row : 0 , section : 0 )
) {
self . insertLocation = insertLocation
self . wasCloseToBottom = wasCloseToBottom
self . sentMessageBeforeUpdate = sentMessageBeforeUpdate
self . isInsertAtTop = isInsertAtTop
self . firstIndexIsVisible = firstIndexIsVisible
self . visibleInteractionId = visibleInteractionId
self . visibleIndexPath = visibleIndexPath
self . oldVisibleIndexPath = oldVisibleIndexPath
self . lastVisibleIndexPath = lastVisibleIndexPath
}
}
@ -660,96 +656,79 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
source : viewModel . interactionData ,
target : updatedData
)
let isInsert : Bool = ( changeset . map ( { $0 . elementInserted . count } ) . reduce ( 0 , + ) > 0 )
let wasLoadingMore : Bool = self . isLoadingMore
let wasOffsetCloseToBottom : Bool = self . isCloseToBottom
let numItemsInUpdatedData : [ Int ] = updatedData . map { $0 . elements . count }
let itemChangeInfo : ItemChangeInfo = {
let itemChangeInfo : ItemChangeInfo ? = {
guard
changeset. map ( { $0 . elementInserted . count } ) . reduce ( 0 , + ) > 0 ,
isInsert ,
let oldSectionIndex : Int = self . viewModel . interactionData . firstIndex ( where : { $0 . model = = . messages } ) ,
let newSectionIndex : Int = updatedData . firstIndex ( where : { $0 . model = = . messages } ) ,
let newFirstItemIndex : Int = updatedData [ newSectionIndex ] . elements
. firstIndex ( where : { item -> Bool in
item . id = = self . viewModel . interactionData [ oldSectionIndex ] . elements . first ? . id
} ) ,
let newLastItemIndex : Int = updatedData [ newSectionIndex ] . elements
. lastIndex ( where : { item -> Bool in
item . id = = self . viewModel . interactionData [ oldSectionIndex ] . elements . last ? . id
} ) ,
let firstVisibleIndexPath : IndexPath = self . tableView . indexPathsForVisibleRows ?
. filter ( { $0 . section = = oldSectionIndex } )
. sorted ( )
. first ,
let lastVisibleIndexPath : IndexPath = self . tableView . indexPathsForVisibleRows ?
. filter ( { $0 . section = = oldSectionIndex } )
. sorted ( )
. last ,
let newVisibleIndex : Int = updatedData [ newSectionIndex ] . elements
. firstIndex ( where : { item in
item . id = = self . viewModel . interactionData [ oldSectionIndex ]
. elements [ firstVisibleIndexPath . row ]
. id
} ) ,
let newLastVisibleIndex : Int = updatedData [ newSectionIndex ] . elements
. firstIndex ( where : { item in
item . id = = self . viewModel . interactionData [ oldSectionIndex ]
. elements [ lastVisibleIndexPath . row ]
. id
} )
else {
return ItemChangeInfo (
insertLocation : . none ,
wasCloseToBottom : isCloseToBottom ,
sentMessageBeforeUpdate : self . viewModel . sentMessageBeforeUpdate
)
}
else { return nil }
return ItemChangeInfo (
insertLocation : {
let insertedAtTop : Bool = (
newSectionIndex > oldSectionIndex ||
newFirstItemIndex > 0
)
let insertedAtBot : Bool = (
newSectionIndex < oldSectionIndex ||
newLastItemIndex < ( updatedData [ newSectionIndex ] . elements . count - 1 )
)
// I f a n y t h i n g w a s i n s e r t e d a t t h e t o p t h e n w e n e e d t o m a i n t a i n t h e c u r r e n t
// o f f s e t s o a l w a y s r e t u r n a ' t o p ' i n s e r t l o c a t i o n
switch ( insertedAtTop , insertedAtBot , isLoadingMore ) {
case ( true , _ , true ) , ( true , false , false ) : return . top
case ( false , true , _ ) : return . bottom
case ( false , false , _ ) , ( true , true , false ) : return . other
}
} ( ) ,
wasCloseToBottom : isCloseToBottom ,
sentMessageBeforeUpdate : self . viewModel . sentMessageBeforeUpdate ,
isInsertAtTop : (
newSectionIndex > oldSectionIndex ||
newFirstItemIndex > 0
) ,
firstIndexIsVisible : ( firstVisibleIndexPath . row = = 0 ) ,
visibleInteractionId : updatedData [ newSectionIndex ] . elements [ newVisibleIndex ] . id ,
visibleIndexPath : IndexPath ( row : newVisibleIndex , section : newSectionIndex ) ,
oldVisibleIndexPath : firstVisibleIndexPath ,
lastVisibleIndexPath : IndexPath ( row : newLastVisibleIndex , section : newSectionIndex )
oldVisibleIndexPath : firstVisibleIndexPath
)
} ( )
guard ! isInsert || wasLoadingMore || itemChangeInfo ? . isInsertAtTop = = true else {
self . viewModel . updateInteractionData ( updatedData )
self . tableView . reloadData ( )
// A n i m a t e t o t h e t a r g e t i n t e r a c t i o n ( o r t h e b o t t o m ) a f t e r a s l i g h t l y d e l a y t o p r e v e n t b u g g y
// a n i m a t i o n c o n f l i c t s
if let focusedInteractionId : Int64 = self . focusedInteractionId {
// I f w e h a d a f o c u s e d I n t e r a c t i o n I d t h e n s c r o l l t o i t ( a n d h i d e t h e s e a r c h
// r e s u l t b a r l o a d i n g i n d i c a t o r )
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + . milliseconds ( 100 ) ) { [ weak self ] in
self ? . searchController . resultsBar . stopLoading ( )
self ? . scrollToInteractionIfNeeded (
with : focusedInteractionId ,
isAnimated : true ,
highlight : ( self ? . shouldHighlightNextScrollToInteraction = = true )
)
}
}
else if wasOffsetCloseToBottom {
// S c r o l l t o t h e b o t t o m i f a n i n t e r a c t i o n w a s j u s t i n s e r t e d a n d w e e i t h e r
// j u s t s e n t a m e s s a g e o r a r e c l o s e e n o u g h t o t h e b o t t o m ( w a i t a t i n y f r a c t i o n
// t o a v o i d b u g g y a n i m a t i o n b e h a v i o u r )
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + . milliseconds ( 100 ) ) { [ weak self ] in
self ? . scrollToBottom ( isAnimated : true )
}
}
return
}
// / U I T a b l e V i e w d o e s n ' t r e a l l y s u p p o r t b o t t o m - a l i g n e d c o n t e n t v e r y w e l l a n d a s s u c h j u m p s a r o u n d a l o t w h e n i n s e r t i n g c o n t e n t b u t
// / w e w a n t t o m a i n t a i n t h e c u r r e n t o f f s e t f r o m b e f o r e t h e d a t a w a s i n s e r t e d ( e x c e p t w h e n a d d i n g a t t h e b o t t o m w h i l e t h e u s e r i s a t
// / t h e b o t t o m , i n w h i c h c a s e w e w a n t t o s c r o l l d o w n )
// /
// / U n f o r t u n a t e l y t h e U I T a b l e V i e w a l s o d o e s s o m e w e i r d t h i n g s w h e n u p d a t i n g ( w h e r e i t w o n ' t h a v e u p d a t e d i t ' s i n t e r n a l d a t a u n t i l
// / a f t e r i t p e r f o r m s t h e n e x t l a y o u t ) ; t h e b e l o w c o d e c h e c k s a c o n d i t i o n o n l a y o u t a n d i f i t p a s s e s i t c a l l s a c l o s u r e
if itemChangeInfo . insertLocation = = . top {
let cellSorting : ( MessageCell , MessageCell ) -> Bool = { lhs , rhs -> Bool in
if ! lhs . isHidden && rhs . isHidden { return true }
if lhs . isHidden && ! rhs . isHidden { return false }
return ( lhs . frame . minY < rhs . frame . minY )
}
let oldRect : CGRect = ( self . tableView . subviews
. compactMap { $0 as ? MessageCell }
. sorted ( by : cellSorting )
. first ( where : { cell -> Bool in cell . viewModel ? . id = = itemChangeInfo . visibleInteractionId } ) ?
. frame )
. defaulting ( to : self . tableView . rectForRow ( at : itemChangeInfo . oldVisibleIndexPath ) )
if let itemChangeInfo : ItemChangeInfo = itemChangeInfo , ( itemChangeInfo . isInsertAtTop || wasLoadingMore ) {
let oldCellHeight : CGFloat = self . tableView . rectForRow ( at : itemChangeInfo . oldVisibleIndexPath ) . height
// T h e t h e u s e r t r i g g e r e d t h e ' s c r o l l T o T o p ' a n i m a t i o n ( b y t a p p i n g i n t h e n a v b a r ) t h e n w e
// n e e d t o s t o p t h e a n i m a t i o n b e f o r e a t t e m p t i n g t o l o c k t h e o f f s e t ( o t h e r w i s e t h i n g s b r e a k )
@ -778,8 +757,10 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
. height )
. defaulting ( to : 0 )
}
let newTargetRect : CGRect ? = self ? . tableView . rectForRow ( at : itemChangeInfo . visibleIndexPath )
let heightDiff : CGFloat = ( oldRect . height - ( newTargetRect ? ? oldRect ) . height )
let newTargetHeight : CGFloat ? = self ? . tableView
. rectForRow ( at : itemChangeInfo . visibleIndexPath )
. height
let heightDiff : CGFloat = ( oldCellHeight - ( newTargetHeight ? ? oldCellHeight ) )
self ? . tableView . contentOffset . y += ( calculatedRowHeights - heightDiff )
}
@ -803,61 +784,31 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
}
)
}
else if itemChangeInfo . insertLocation = = . bottom || itemChangeInfo . insertLocation = = . other {
CATransaction . begin ( )
CATransaction . setCompletionBlock { [ weak self ] in
if let focusedInteractionId : Int64 = self ? . focusedInteractionId {
// I f w e h a d a f o c u s e d I n t e r a c t i o n I d t h e n s c r o l l t o i t ( a n d h i d e t h e s e a r c h
// r e s u l t b a r l o a d i n g i n d i c a t o r )
self ? . searchController . resultsBar . stopLoading ( )
self ? . scrollToInteractionIfNeeded (
with : focusedInteractionId ,
isAnimated : true ,
highlight : ( self ? . shouldHighlightNextScrollToInteraction = = true )
)
}
else if itemChangeInfo . sentMessageBeforeUpdate || itemChangeInfo . wasCloseToBottom {
// S c r o l l t o t h e b o t t o m i f a n i n t e r a c t i o n w a s j u s t i n s e r t e d a n d w e e i t h e r
// j u s t s e n t a m e s s a g e o r a r e c l o s e e n o u g h t o t h e b o t t o m
self ? . scrollToBottom ( isAnimated : true )
}
}
}
// R e l o a d t h e t a b l e c o n t e n t ( a n i m a t e c h a n g e s i f w e a r e n ' t i n s e r t i n g a t t h e t o p )
self . tableView . reload (
using : changeset ,
deleteSectionsAnimation : . none ,
insertSectionsAnimation : . none ,
reloadSectionsAnimation : . none ,
deleteRowsAnimation : . bottom ,
insertRowsAnimation : . bottom ,
insertRowsAnimation : . none ,
reloadRowsAnimation : . none ,
interrupt : { itemChangeInfo .insertLocation = = . top || $0 . changeCount > ConversationViewModel . pageSize }
interrupt : { itemChangeInfo ? . isInsertAtTop = = true || $0 . changeCount > ConversationViewModel . pageSize }
) { [ weak self ] updatedData in
self ? . viewModel . updateInteractionData ( updatedData )
}
if itemChangeInfo . insertLocation = = . bottom || itemChangeInfo . insertLocation = = . other {
CATransaction . commit ( )
}
// M a r k r e c e i v e d m e s s a g e s a s r e a d
viewModel . markAllAsRead ( )
viewModel . sentMessageBeforeUpdate = false
}
private func performInitialScrollIfNeeded ( ) {
guard ! didFinishInitialLayout && hasLoadedInitialThreadData && hasLoadedInitialInteractionData else { return }
guard ! hasPerformedInitialScroll && hasLoadedInitialThreadData && hasLoadedInitialInteractionData else {
return
}
// S c r o l l t o t h e l a s t u n r e a d m e s s a g e i f p o s s i b l e ; o t h e r w i s e s c r o l l t o t h e b o t t o m .
// W h e n t h e u n r e a d m e s s a g e c o u n t i s m o r e t h a n t h e n u m b e r o f v i e w i t e m s o f a p a g e ,
// t h e s c r e e n w i l l s c r o l l t o t h e b o t t o m i n s t e a d o f t h e f i r s t u n r e a d m e s s a g e
if let focusedInteractionId : Int64 = self . viewModel . focusedInteractionId {
self . scrollToInteractionIfNeeded ( with : focusedInteractionId , isAnimated : false , highlight : true )
}
else if let firstUnreadInteractionId : Int64 = self . viewModel . threadData . threadFirstUnreadInteractionId {
self . scrollToInteractionIfNeeded ( with : firstUnreadInteractionId , position : . top , isAnimated : false )
self . unreadCountView . alpha = self . scrollButton . alpha
}
else {
@ -865,6 +816,7 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
}
self . scrollButton . alpha = self . getScrollButtonOpacity ( )
self . hasPerformedInitialScroll = true
// N o w t h a t t h e d a t a h a s l o a d e d w e n e e d t o c h e c k i f e i t h e r o f t h e " l o a d m o r e " s e c t i o n s a r e
// v i s i b l e a n d t r i g g e r t h e m i f s o
@ -1187,7 +1139,7 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
}
func tableView ( _ tableView : UITableView , willDisplayHeaderView view : UIView , forSection section : Int ) {
guard self . didFinishInitialLayout && ! self . isLoadingMore else { return }
guard self . hasPerformedInitialScroll && ! self . isLoadingMore else { return }
let section : ConversationViewModel . SectionModel = self . viewModel . interactionData [ section ]
@ -1264,6 +1216,7 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
self . shouldHighlightNextScrollToInteraction
else {
self . focusedInteractionId = nil
self . shouldHighlightNextScrollToInteraction = false
return
}
@ -1439,16 +1392,16 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
animated : ( self . didFinishInitialLayout && isAnimated )
)
// D o n ' t c l e a r t h e s e v a l u e s i f w e h a v e ' t d o n e t h e i n i t i a l l a y o u t ( w e w i l l c a l l t h i s
// m e t h o d a s e c o n d t i m e t o t r i g g e r t h e h i g h l i g h t a f t e r t h e s c r e e n a p p e a r s )
guard self . didFinishInitialLayout else { return }
self . focusedInteractionId = nil
self . shouldHighlightNextScrollToInteraction = false
// I f w e h a v e n ' t f i n i s h e d t h e i n i t i a l l a y o u t t h e n w e w a n t t o d e l a y t h e h i g h l i g h t s l i g h t l y
// s o i t d o e s n ' t l o o k b u g g y w i t h t h e p u s h t r a n s i t i o n
if highlight {
self . highlightCellIfNeeded ( interactionId : interactionId )
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + . milliseconds ( self . didFinishInitialLayout ? 0 : 150 ) ) { [ weak self ] in
self ? . highlightCellIfNeeded ( interactionId : interactionId )
}
}
self . shouldHighlightNextScrollToInteraction = false
self . focusedInteractionId = nil
return
}