Фикс: исправлен скролл реплай-сообщений под композер при отправке
This commit is contained in:
@@ -86,6 +86,13 @@ final class NativeMessageListController: UIViewController {
|
||||
/// Cleared only when isInputFocused becomes true (user tap or reply action).
|
||||
fileprivate(set) var keyboardBlockedUntilUserTap = false
|
||||
private var currentKeyboardHeight: CGFloat = 0
|
||||
/// Timestamp of the last animated scrollToBottom call.
|
||||
/// When recent (< 1s), applyInsets/keyboardWillChangeFrame snap to
|
||||
/// -newInsetTop instead of compensating from the intermediate offset.
|
||||
/// A timestamp is used instead of a boolean flag because UIKit cancels
|
||||
/// the scroll animation when contentInset changes, firing
|
||||
/// scrollViewDidEndScrollingAnimation prematurely.
|
||||
private var scrollToBottomTimestamp: CFAbsoluteTime = 0
|
||||
|
||||
// MARK: - Scroll-to-Bottom Button
|
||||
private var scrollToBottomButton: UIButton?
|
||||
@@ -762,16 +769,27 @@ final class NativeMessageListController: UIViewController {
|
||||
// Capture offset BEFORE setting insets — UIKit may auto-clamp after.
|
||||
let oldOffset = collectionView.contentOffset.y
|
||||
|
||||
let recentScrollToBottom = CFAbsoluteTimeGetCurrent() - scrollToBottomTimestamp < 1.0
|
||||
|
||||
collectionView.contentInset = UIEdgeInsets(top: newInsetTop, left: 0, bottom: topInset, right: 0)
|
||||
collectionView.scrollIndicatorInsets = collectionView.contentInset
|
||||
|
||||
// Always compensate symmetrically: open AND close.
|
||||
// Setting exact `oldOffset - delta` overrides any UIKit auto-clamping.
|
||||
let shouldCompensate = abs(delta) > 0.5
|
||||
&& !collectionView.isDragging
|
||||
&& !collectionView.isDecelerating
|
||||
if shouldCompensate {
|
||||
collectionView.contentOffset.y = oldOffset - delta
|
||||
if recentScrollToBottom && abs(delta) > 0.5 {
|
||||
// An animated scroll-to-bottom was recently started.
|
||||
// Reading contentOffset during the animation returns an intermediate
|
||||
// value — compensating from it produces a wrong final position.
|
||||
// Snap to the new bottom instead.
|
||||
collectionView.setContentOffset(
|
||||
CGPoint(x: 0, y: -newInsetTop), animated: false
|
||||
)
|
||||
} else {
|
||||
// Normal compensation: preserve reading position.
|
||||
let shouldCompensate = abs(delta) > 0.5
|
||||
&& !collectionView.isDragging
|
||||
&& !collectionView.isDecelerating
|
||||
if shouldCompensate {
|
||||
collectionView.contentOffset.y = oldOffset - delta
|
||||
}
|
||||
}
|
||||
updateScrollToBottomBadge()
|
||||
}
|
||||
@@ -779,6 +797,7 @@ final class NativeMessageListController: UIViewController {
|
||||
/// Scroll to the newest message (visual bottom = offset 0 in inverted scroll).
|
||||
func scrollToBottom(animated: Bool) {
|
||||
guard !messages.isEmpty else { return }
|
||||
if animated { scrollToBottomTimestamp = CFAbsoluteTimeGetCurrent() }
|
||||
collectionView.setContentOffset(
|
||||
CGPoint(x: 0, y: -collectionView.contentInset.top),
|
||||
animated: animated
|
||||
@@ -874,7 +893,10 @@ final class NativeMessageListController: UIViewController {
|
||||
// Capture offset BEFORE animation — UIKit may auto-clamp after inset change.
|
||||
// Skip compensation during swipe-back — let content slide down naturally.
|
||||
let oldOffset = collectionView.contentOffset.y
|
||||
let shouldCompensate = !isDisappearing
|
||||
let recentScroll = CFAbsoluteTimeGetCurrent() - scrollToBottomTimestamp < 1.0
|
||||
let snapToBottom = recentScroll && abs(delta) > 0.5
|
||||
let shouldCompensate = !snapToBottom
|
||||
&& !isDisappearing
|
||||
&& abs(delta) > 0.5
|
||||
&& !collectionView.isDragging
|
||||
&& !collectionView.isDecelerating
|
||||
@@ -886,8 +908,10 @@ final class NativeMessageListController: UIViewController {
|
||||
self.collectionView.contentInset = UIEdgeInsets(top: newInsetTop, left: 0, bottom: topInset, right: 0)
|
||||
self.collectionView.scrollIndicatorInsets = self.collectionView.contentInset
|
||||
|
||||
// Enforce exact offset — overrides any UIKit auto-clamping.
|
||||
if shouldCompensate {
|
||||
if snapToBottom {
|
||||
// Recent scroll-to-bottom — maintain bottom target.
|
||||
self.collectionView.contentOffset.y = -newInsetTop
|
||||
} else if shouldCompensate {
|
||||
self.collectionView.contentOffset.y = oldOffset - delta
|
||||
}
|
||||
|
||||
@@ -951,6 +975,7 @@ extension NativeMessageListController: UICollectionViewDelegate {
|
||||
onPaginationTrigger?()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - ComposerViewDelegate
|
||||
@@ -1146,6 +1171,11 @@ struct NativeMessageListView: UIViewControllerRepresentable {
|
||||
// Sync composer state (iOS < 26)
|
||||
if useUIKitComposer {
|
||||
syncComposerState(controller)
|
||||
// Force immediate layout to process pending height change from reply
|
||||
// bar show/hide. Without this, height report is deferred to
|
||||
// DispatchQueue.main.async, and scrollToBottom may calculate offset
|
||||
// before contentInset reflects the new composer height.
|
||||
controller.composerView?.layoutIfNeeded()
|
||||
}
|
||||
|
||||
// Update messages
|
||||
@@ -1223,11 +1253,6 @@ struct NativeMessageListView: UIViewControllerRepresentable {
|
||||
private func syncComposerState(_ controller: NativeMessageListController) {
|
||||
guard let composer = controller.composerView else { return }
|
||||
composer.setText(messageText)
|
||||
#if DEBUG
|
||||
if replySenderName != nil {
|
||||
print("📋 syncComposer: sender=\(replySenderName ?? "nil") preview=\(replyPreviewText ?? "nil")")
|
||||
}
|
||||
#endif
|
||||
composer.setReply(senderName: replySenderName, previewText: replyPreviewText)
|
||||
|
||||
// After swipe-back dismisses keyboard, block programmatic re-focus
|
||||
|
||||
Reference in New Issue
Block a user