* Initial setup for new keyboard * fix the offset calculation onMove by adding isKeyboardFullyOpened * Done with the keyboard handling implementation * Handled keyboard focus and blured state using context * Added default height for input container * Android support * Tablet state handling * Fix for refreshing offset in list * Created a default context for mention post list * Fix linter errors * Fix tests * Minor * Fix the height issue for tablet view * Review comments * Dependency fix * Reveiw comment * keyboard animation only enabled with screen on top navigation * added tests * Added extra keyboard component for emoji picker (#9328) * Added extra keyboard component * handled swipe geature for extra keyboard * scroll to bottom visible on emoji picker * Check for stale event for keyboard geature area * fix keyboard stale event for mid-gesture change swipe direction * Closing emoji picker when message priority is opened * changing to thread view closes emoji picker * Close emoij picker and keyboard when attachment icons are clicked * handle android emoji picker behaviour * Remove emoji picker code * fix tests * Address reviev comments * Test fixes * usePreventDoubleTab for race condition and corrected comment on isInputAccessoryViewMode flag * File attachments option in bottom sheet quick action (#9331) * Added extra keyboard component * handled swipe geature for extra keyboard * File attachments option in bottom sheet quick action * Updated tests * i18n * fix test * Added tests * Review comment * fix tests * Integrated Emoji picker to Extra keyboard component. (#9339) * Added extra keyboard component * handled swipe geature for extra keyboard * scroll to bottom visible on emoji picker * Fix the post input container height change issue * Added emoji picker with search functionality * keyboard height not recorded fallback to default height, set search to false closing emoji picker * fix search funcitonality in android * scroll to end bottom dismissed with pressed * Fixed the scroll issue for android * fix the flickering post input issue * Wired up the emoji picker * Added keyboard and spaceback in emoji picker * intl extract * initial cursor position and simple calculation review comment * separated grapheme to utils file * Review comments * nitpick * Fix the bottom safe area and navigation stack restore fix * Fix test * disabled emoji picker in edit post screen * change the name of the variable to name it clarier * fix the tab gap issue in andriod * disabled the emoji skin tone change on andriod * fix the input container jump issue * fix the failing test * UX review * added white space after the attachment icon * When @ or / is press open keyboard on andriod also fix the scroll issue when keyboard is open * back bottom closes emoji picker and opening keyboard jump fix * reverted code * fixed the flickering issue of input and scroll position fix * Fix the gap issue in thread * fix the warning reaminated issue when opening a channel * Fix the extra space issue between input and emoij picker * Minor fix for extra space between input and emoji picker * Fix thread view bottom safe area and gap between keyboard and searched emojis * Fix the keyboard issue in tablet * Fix the warning issue react-native-keyboard-controller * Fix tests * Fix the tablet search hight issue * Replaced the ExtraKeyboardProvider with KeyboardProvider from rnkc for permalink * Bottom sheet jump issue resolved * Search do not close after selecting emoji in search list * Added the bottom inset height in search emoji for android * fix buid issue * Fix the cancel state to emoji picker * use portals for autocomplete * fix permalink extra space in post list * Portal only for android --------- Co-authored-by: Mattermost Build <build@mattermost.com> Co-authored-by: Rahim Rahman <rahim.rahman@mattermost.com> Co-authored-by: Harshil Sharma <harshilsharma63@gmail.com> Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
525 lines
24 KiB
TypeScript
525 lines
24 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import {useKeyboardHandler} from 'react-native-keyboard-controller';
|
|
import {useAnimatedScrollHandler, useDerivedValue, useSharedValue} from 'react-native-reanimated';
|
|
|
|
import {BOTTOM_TAB_HEIGHT} from '@constants/view';
|
|
|
|
export const useKeyboardAnimation = (
|
|
postInputContainerHeight: number,
|
|
enableAnimation = true,
|
|
isTablet = false,
|
|
safeAreaBottom = 0,
|
|
isThreadView = false,
|
|
enabled = true,
|
|
) => {
|
|
/**
|
|
* progress: Keyboard animation progress (0 = closed, 1 = fully open)
|
|
* Used for: Tracking keyboard animation state
|
|
*/
|
|
const progress = useSharedValue(0);
|
|
|
|
/**
|
|
* keyboardTranslateY: Keyboard height (adjusted for tab bar) used to animate input container position
|
|
*
|
|
* How it works: This value represents the keyboard height and is used with negative translateY
|
|
* in useKeyboardAwarePostDraft.ts (transform: [{translateY: -keyboardTranslateY.value}]) to move the input
|
|
* container UP by the keyboard height amount. Higher keyboard height = input moves up more.
|
|
*
|
|
* Smoothed during interactive gestures to prevent jerky movements
|
|
*/
|
|
const keyboardTranslateY = useSharedValue(0);
|
|
|
|
/**
|
|
* bottomInset: Bottom inset for the scroll view
|
|
* Adds padding at the bottom of scroll content so it doesn't hide behind keyboard
|
|
*/
|
|
const bottomInset = useSharedValue(0);
|
|
|
|
/**
|
|
* scrollOffset: Scroll offset adjustment when keyboard opens
|
|
* Ensures the scroll view scrolls to the right position when keyboard appears
|
|
*/
|
|
const scrollOffset = useSharedValue(0);
|
|
|
|
/**
|
|
* scroll: Tracks the current scroll position of the ScrollView
|
|
* Used to calculate proper offset when keyboard opens
|
|
*/
|
|
const scrollPosition = useSharedValue(0);
|
|
|
|
/**
|
|
* keyboardHeight: The exact height of the keyboard from events
|
|
* Always reflects the true keyboard height without smoothing
|
|
*/
|
|
const keyboardHeight = useSharedValue(0);
|
|
|
|
/**
|
|
* isKeyboardClosing: Tracks if keyboard is currently closing (detected in onInteractive)
|
|
* Used to prevent height jumps in onMove when user releases finger mid-swipe
|
|
*/
|
|
const isKeyboardClosing = useSharedValue(false);
|
|
|
|
/**
|
|
* isInteractiveGesture: Tracks if we're in an interactive gesture (user touching keyboard)
|
|
* Used to: Distinguish between normal keyboard opening vs stale events after gesture
|
|
*/
|
|
const isInteractiveGesture = useSharedValue(false);
|
|
|
|
/**
|
|
* isKeyboardFullyOpen: True when keyboard is fully open (height > 0 and progress === 1)
|
|
*/
|
|
const isKeyboardFullyOpen = useSharedValue(false);
|
|
|
|
/**
|
|
* isKeyboardFullyClosed: True when keyboard is fully closed (height === 0)
|
|
*/
|
|
const isKeyboardFullyClosed = useSharedValue(true);
|
|
|
|
/**
|
|
* isKeyboardInTransition: True when keyboard is animating between open/closed states
|
|
*/
|
|
const isKeyboardInTransition = useSharedValue(false);
|
|
|
|
/**
|
|
* isEnabled: Shared value to track if keyboard handling is enabled for this screen
|
|
* Used to prevent processing keyboard events when screen is not visible
|
|
* useDerivedValue automatically tracks the enabled prop and updates reactively
|
|
*/
|
|
const isEnabled = useDerivedValue(() => enabled, [enabled]);
|
|
|
|
// Calculate tab bar adjustment (only for tablets, not in thread view)
|
|
// This accounts for the tab bar height + safe area bottom that gets hidden when keyboard opens
|
|
// Thread views don't have a tab bar, so no adjustment is needed
|
|
// For tablets in thread view, also don't subtract safeAreaBottom since the input container
|
|
// should move up by the full keyboard height to clear the keyboard completely
|
|
let tabBarAdjustment: number;
|
|
if (isTablet && !isThreadView) {
|
|
// Channel view on tablet: account for tab bar + safe area
|
|
tabBarAdjustment = BOTTOM_TAB_HEIGHT + safeAreaBottom;
|
|
} else if (isTablet && isThreadView) {
|
|
// Thread view on tablet: no adjustment needed (no tab bar, full keyboard height)
|
|
tabBarAdjustment = 0;
|
|
} else {
|
|
// Mobile devices: account for safe area only
|
|
tabBarAdjustment = safeAreaBottom;
|
|
}
|
|
|
|
/**
|
|
* isInputAccessoryViewMode: Whether we're showing input accessory view (emoji picker) instead of keyboard
|
|
* When true, keyboard handlers are ignored to prevent interference with the emoji picker
|
|
*/
|
|
const isInputAccessoryViewMode = useSharedValue(false);
|
|
|
|
/**
|
|
* isTransitioningFromCustomView: Special mode when transitioning from custom view to keyboard
|
|
* Prevents height updates during the transition to avoid jumps
|
|
*/
|
|
const isTransitioningFromCustomView = useSharedValue(false);
|
|
|
|
// ------------------------------------------------------------------
|
|
// KEYBOARD EVENT HANDLERS
|
|
// ------------------------------------------------------------------
|
|
|
|
/**
|
|
* useKeyboardHandler: Subscribe to keyboard lifecycle events
|
|
*
|
|
* IMPORTANT: All callbacks here are WORKLETS (run on UI thread)
|
|
* The "worklet" directive tells Reanimated to compile this function for the UI thread
|
|
* This enables 60fps smooth animations without JavaScript thread delays
|
|
*
|
|
* Event lifecycle:
|
|
* 1. onStart: Keyboard animation begins
|
|
* 2. onInteractive: User is dragging keyboard interactively
|
|
* 3. onMove: Keyboard position changes during animation
|
|
* 4. onEnd: Keyboard animation completes
|
|
*/
|
|
useKeyboardHandler({
|
|
|
|
/**
|
|
* onStart: Called when keyboard animation starts
|
|
* @param e - Event object with keyboard information
|
|
*/
|
|
onStart: (e) => {
|
|
'worklet';
|
|
|
|
// Ignore keyboard events when showing custom view (emoji picker)
|
|
if (isInputAccessoryViewMode.value) {
|
|
return;
|
|
}
|
|
|
|
// Ignore keyboard events during transition from custom view to keyboard
|
|
if (isTransitioningFromCustomView.value) {
|
|
return;
|
|
}
|
|
|
|
// Skip processing if screen is not enabled/visible or if animations are disabled
|
|
if (!isEnabled.value || !enableAnimation) {
|
|
return;
|
|
}
|
|
|
|
// Ignore adjustment event from KeyboardGestureArea (fires after keyboard fully opens)
|
|
// After keyboard reaches full height, KeyboardGestureArea sends offset-adjusted event
|
|
// We use both offset prop AND manual animation, so this would cause flicker
|
|
// Example: keyboard opens at 346px → then adjustment event at 229px (346 - 117 offset)
|
|
if (parseInt(e.height.toString()) === keyboardHeight.value - postInputContainerHeight) {
|
|
return;
|
|
}
|
|
|
|
progress.value = e.progress;
|
|
|
|
// Store the exact keyboard height
|
|
keyboardHeight.value = e.height;
|
|
|
|
const adjustedHeight = e.height - (tabBarAdjustment * e.progress);
|
|
|
|
// CRITICAL FIX: On real iOS devices, onStart can fire with progress: 1 before animation completes
|
|
// Even if progress is 1, we should NOT set isKeyboardFullyOpen to true in onStart because:
|
|
// 1. onMove events may still come with lower progress values
|
|
// 2. Only onEnd with progress === 1 should mark the keyboard as fully open
|
|
// This prevents jerky behavior where the input container jumps down and back up
|
|
const wasAlreadyOpen = keyboardTranslateY.value > 0;
|
|
|
|
// Always treat as transitioning if we might receive more events (even with progress: 1)
|
|
// Only exception: if height is 0 (keyboard closing)
|
|
const shouldTreatAsTransitioning = e.height > 0 && (e.progress < 1 || wasAlreadyOpen);
|
|
|
|
keyboardTranslateY.value = adjustedHeight;
|
|
|
|
// Update keyboard state flags
|
|
// NEVER mark as fully open in onStart - always wait for onEnd to confirm
|
|
isKeyboardFullyClosed.value = e.height === 0;
|
|
isKeyboardFullyOpen.value = false; // Always false in onStart - onEnd will set it correctly
|
|
isKeyboardInTransition.value = e.height > 0 && shouldTreatAsTransitioning;
|
|
|
|
// When keyboard closes (height: 0), preserve isKeyboardClosing flag if it was true
|
|
// This prevents onMove events from processing stale closing animation values
|
|
// Only reset if keyboard was actually open (not already closing)
|
|
if (e.height === 0) {
|
|
// If keyboard was closing, keep the flag true so onMove knows to ignore stale events
|
|
// If keyboard was opening, reset the flag
|
|
if (!isKeyboardClosing.value) {
|
|
isKeyboardClosing.value = false;
|
|
}
|
|
|
|
// Always reset interactive gesture flag when keyboard closes
|
|
isInteractiveGesture.value = false;
|
|
}
|
|
|
|
// Update scroll view insets and offsets
|
|
// bottomInset: Adds bottom padding to scroll content
|
|
bottomInset.value = adjustedHeight;
|
|
|
|
// CRITICAL FIX: Don't update scrollOffset in onStart if progress === 1 and keyboard wasn't already open
|
|
// On real iOS devices, onStart can fire with progress: 1 BEFORE the animation starts
|
|
// This causes scrollOffset to jump immediately to the final value, triggering a scroll
|
|
// Then onMove events come with intermediate values, causing jerky "down then up" behavior
|
|
// Solution: Only update scrollOffset in onStart if progress < 1 OR keyboard was already open
|
|
// Let onMove handle the smooth animation for opening keyboards
|
|
// Note: wasAlreadyOpen is checked BEFORE keyboardTranslateY.value is updated, so use that
|
|
if (e.progress < 1 || wasAlreadyOpen || e.height === 0) {
|
|
// scrollOffset: Ensures the scroll view scrolls with the keyboard animation
|
|
scrollOffset.value = adjustedHeight;
|
|
} else {
|
|
// Don't update scrollOffset yet - wait for onMove to animate smoothly
|
|
// This prevents the initial jump when keyboard opens
|
|
}
|
|
},
|
|
|
|
/**
|
|
* onInteractive: Called continuously while user drags keyboard interactively
|
|
* This provides smooth real-time updates as the user swipes
|
|
*
|
|
* @param e - Event object with keyboard information
|
|
*/
|
|
onInteractive: (e) => {
|
|
'worklet';
|
|
|
|
// Ignore keyboard events when showing custom view (emoji picker)
|
|
if (isInputAccessoryViewMode.value) {
|
|
return;
|
|
}
|
|
|
|
// Ignore keyboard events during transition from custom view to keyboard
|
|
if (isTransitioningFromCustomView.value) {
|
|
return;
|
|
}
|
|
|
|
// On Android, use native keyboard behavior (no custom animations)
|
|
if (!isEnabled.value || !enableAnimation) {
|
|
return;
|
|
}
|
|
|
|
if (parseInt(e.height.toString()) === postInputContainerHeight) {
|
|
return;
|
|
}
|
|
|
|
// Ignore stale interactive events during screen navigation
|
|
// When navigating from channel (keyboard open) to thread, the channel's keyboard dismissal
|
|
// can trigger stale onInteractive events that arrive at the newly mounted thread screen.
|
|
// If keyboard is fully closed (keyboardTranslateY === 0), any onInteractive event is stale and should be ignored.
|
|
if (keyboardTranslateY.value === 0) {
|
|
return;
|
|
}
|
|
|
|
const previousKeyboardTranslateY = keyboardTranslateY.value;
|
|
|
|
if (keyboardTranslateY.value > 0) {
|
|
isInteractiveGesture.value = true;
|
|
}
|
|
|
|
// Calculate adjusted height first to compare properly
|
|
const adjustedHeight = e.height - (tabBarAdjustment * e.progress);
|
|
|
|
// Track if keyboard is closing (adjusted height decreasing) or opening (adjusted height increasing)
|
|
// Compare adjusted heights, not raw event height vs adjusted height
|
|
// This detects direction changes mid-gesture for smooth animations
|
|
if (adjustedHeight < previousKeyboardTranslateY) {
|
|
// Keyboard is closing - height is decreasing
|
|
isKeyboardClosing.value = true;
|
|
} else if (adjustedHeight > previousKeyboardTranslateY) {
|
|
// User changed direction - swiped back up
|
|
isKeyboardClosing.value = false;
|
|
}
|
|
|
|
// If adjustedHeight === previousKeyboardTranslateY, keep current isKeyboardClosing state
|
|
|
|
keyboardTranslateY.value = adjustedHeight;
|
|
scrollOffset.value = adjustedHeight;
|
|
bottomInset.value = adjustedHeight;
|
|
|
|
// Update keyboard state flags
|
|
isKeyboardFullyClosed.value = e.height === 0;
|
|
isKeyboardFullyOpen.value = e.height > 0 && e.progress === 1;
|
|
isKeyboardInTransition.value = e.height > 0 && e.progress < 1;
|
|
},
|
|
|
|
/**
|
|
* onMove: Called continuously as keyboard animates (programmatic or gesture)
|
|
* Similar to onInteractive but for all keyboard movements
|
|
*
|
|
* @param e - Event object with keyboard information
|
|
*/
|
|
onMove: (e) => {
|
|
'worklet';
|
|
|
|
// Ignore keyboard events when showing custom view (emoji picker)
|
|
if (isInputAccessoryViewMode.value) {
|
|
return;
|
|
}
|
|
|
|
// During transition from custom view, don't update keyboardHeight from onMove events
|
|
// These events can be stale/out-of-order. Wait for onEnd to get the final correct height.
|
|
if (isTransitioningFromCustomView.value) {
|
|
return;
|
|
}
|
|
|
|
// On Android, use native keyboard behavior (no custom animations)
|
|
if (!isEnabled.value || !enableAnimation) {
|
|
return;
|
|
}
|
|
|
|
// CRITICAL FIX: Ignore stale onMove events after keyboard closes
|
|
// When keyboard closes, onStart fires with height: 0, but onMove may still fire with stale values
|
|
// If keyboardHeight is 0 (closed), ignore any onMove events that try to set positive heights
|
|
if (keyboardHeight.value === 0 && e.height > 0) {
|
|
return;
|
|
}
|
|
|
|
// If keyboard is closing (detected in onInteractive), ignore onMove events
|
|
// This prevents stale closing animation values from causing jumps
|
|
if (isKeyboardClosing.value) {
|
|
return;
|
|
}
|
|
|
|
// Ignore adjustment event from KeyboardGestureArea (fires after keyboard fully opens)
|
|
// After keyboard reaches full height, KeyboardGestureArea sends offset-adjusted event
|
|
// We use both offset prop AND manual animation, so this would cause flicker
|
|
// Example: keyboard opens at 346px → then adjustment event at 229px (346 - 117 offset)
|
|
if (parseInt(e.height.toString()) === keyboardHeight.value - postInputContainerHeight) {
|
|
return;
|
|
}
|
|
|
|
// CRITICAL FIX: Ignore invalid onMove events with progress > 1
|
|
// On real iOS devices, sometimes onMove fires with progress > 1 (e.g., 1.266)
|
|
// These are invalid events that can cause the input container to jump
|
|
// Progress should always be between 0 and 1 for valid keyboard animation events
|
|
if (e.progress > 1) {
|
|
return;
|
|
}
|
|
|
|
const absHeight = Math.abs(e.height) - (tabBarAdjustment * e.progress); // Use Math.abs because programmatic dismiss (KeyboardController.dismiss()) reports negative heights
|
|
|
|
// CRITICAL FIX: On real iOS devices, onStart can fire with progress: 1 before animation completes
|
|
// Then onMove events come with lower heights/progress, causing jerky behavior
|
|
// If we've already reached the final keyboard height (isKeyboardFullyOpen or keyboardHeight matches final),
|
|
// ignore onMove events that would reduce keyboardTranslateY below the final value
|
|
// BUT: Always allow scrollOffset and bottomInset to update during animation for smooth scrolling
|
|
const finalHeight = keyboardHeight.value;
|
|
const finalAdjustedHeight = finalHeight > 0 ? finalHeight - tabBarAdjustment : 0;
|
|
const hasReachedFinalState = finalHeight > 0 && keyboardTranslateY.value >= finalAdjustedHeight * 0.95; // 95% threshold to account for rounding
|
|
|
|
if (hasReachedFinalState && absHeight < keyboardTranslateY.value && e.progress < 1) {
|
|
// Still update scrollOffset and bottomInset even if we ignore keyboardTranslateY
|
|
// This ensures the list scrolls smoothly with the keyboard animation
|
|
scrollOffset.value = absHeight;
|
|
bottomInset.value = absHeight;
|
|
return;
|
|
}
|
|
|
|
// Ignore stale/incorrect events ONLY during/after interactive gestures
|
|
// After user releases finger mid-swipe up, onMove sometimes gets wrong height values
|
|
// Example: keyboard at 346px, user releases, onMove reports 80px (stale from earlier)
|
|
// This check only applies during interactive gestures, not during normal keyboard opening
|
|
if (isInteractiveGesture.value && absHeight < keyboardTranslateY.value && e.progress < 1) {
|
|
return;
|
|
}
|
|
|
|
keyboardTranslateY.value = absHeight;
|
|
scrollOffset.value = absHeight;
|
|
bottomInset.value = absHeight;
|
|
|
|
// Update keyboard state flags
|
|
isKeyboardFullyClosed.value = absHeight === 0;
|
|
isKeyboardFullyOpen.value = absHeight > 0 && e.progress === 1;
|
|
isKeyboardInTransition.value = absHeight > 0 && e.progress < 1;
|
|
},
|
|
|
|
onEnd: (e) => {
|
|
'worklet';
|
|
|
|
// Skip processing if screen is not enabled/visible or if animations are disabled
|
|
if (!isEnabled.value) {
|
|
return;
|
|
}
|
|
|
|
// Ignore keyboard events when showing custom view (emoji picker)
|
|
if (isInputAccessoryViewMode.value) {
|
|
isKeyboardClosing.value = false;
|
|
isTransitioningFromCustomView.value = false;
|
|
return;
|
|
}
|
|
|
|
// On Android, use native keyboard behavior (no custom animations)
|
|
if (!enableAnimation) {
|
|
isKeyboardClosing.value = false;
|
|
isTransitioningFromCustomView.value = false;
|
|
return;
|
|
}
|
|
|
|
// Ignore adjustment event from KeyboardGestureArea (can fire in onEnd too)
|
|
// After keyboard reaches full height, KeyboardGestureArea sends offset-adjusted event
|
|
// Example: keyboard opens at 346px → then adjustment event at 255px (346 - 91 offset)
|
|
if (parseInt(e.height.toString()) === keyboardHeight.value - postInputContainerHeight) {
|
|
return;
|
|
}
|
|
|
|
// Store if we were transitioning from custom view before clearing the flag
|
|
const wasTransitioningFromCustomView = isTransitioningFromCustomView.value;
|
|
|
|
// Reset state flags
|
|
isKeyboardClosing.value = false;
|
|
isTransitioningFromCustomView.value = false;
|
|
isInteractiveGesture.value = false;
|
|
|
|
// Use e.progress (from event) not progress.value (shared value might be stale)
|
|
if (e.progress === 1) {
|
|
// CRITICAL FIX: Update keyboardHeight FIRST from onEnd event (most reliable source)
|
|
// During transition, onMove events can be stale/out-of-order, so onEnd's height is authoritative
|
|
// Store previous value for stale check before updating
|
|
const previousKeyboardHeight = keyboardHeight.value;
|
|
keyboardHeight.value = e.height;
|
|
|
|
// Use same calculation as onInteractive/onMove for consistency
|
|
const adjustedHeight = e.height - (tabBarAdjustment * e.progress);
|
|
|
|
// Ignore stale/out-of-order events
|
|
// If keyboard is supposed to be closed (previousKeyboardHeight = 0) but we get an open event,
|
|
// it's a stale event from before the close - ignore it
|
|
if (previousKeyboardHeight === 0 && e.height > 0) {
|
|
return;
|
|
}
|
|
|
|
// If transitioning from custom view, always update keyboardTranslateY to match keyboard
|
|
// This ensures correct positioning when emoji picker height != keyboard height
|
|
if (wasTransitioningFromCustomView) {
|
|
keyboardTranslateY.value = adjustedHeight;
|
|
} else if (Math.abs(keyboardTranslateY.value - adjustedHeight) > 1) {
|
|
// For normal keyboard opening, only adjust if significantly different
|
|
keyboardTranslateY.value = adjustedHeight;
|
|
}
|
|
|
|
// Update bottomInset and scrollOffset to match final keyboard height
|
|
// This ensures messages don't get hidden behind keyboard
|
|
bottomInset.value = adjustedHeight;
|
|
scrollOffset.value = adjustedHeight;
|
|
|
|
isKeyboardFullyOpen.value = true;
|
|
isKeyboardFullyClosed.value = false;
|
|
isKeyboardInTransition.value = false;
|
|
}
|
|
|
|
// Use e.progress (from event) not progress.value (shared value might be stale)
|
|
if (e.progress === 0) {
|
|
// Only set to 0 if not already close to 0
|
|
if (Math.abs(keyboardTranslateY.value) > 0.5) {
|
|
keyboardTranslateY.value = 0;
|
|
}
|
|
isKeyboardFullyOpen.value = false;
|
|
isKeyboardFullyClosed.value = true;
|
|
isKeyboardInTransition.value = false;
|
|
scrollOffset.value = 0;
|
|
|
|
// CRITICAL FIX: Reset scrollPosition when keyboard closes
|
|
// scrollPosition = contentOffsetY + bottomInset
|
|
// Before bottomInset becomes 0, calculate actual content offset: scrollPosition - bottomInset
|
|
// This preserves the user's scroll position even if iOS resets the list to 0
|
|
const currentBottomInset = bottomInset.value;
|
|
if (scrollPosition.value > currentBottomInset) {
|
|
scrollPosition.value = scrollPosition.value - currentBottomInset;
|
|
}
|
|
|
|
bottomInset.value = 0;
|
|
|
|
// Reset closing flag when keyboard fully closes
|
|
isKeyboardClosing.value = false;
|
|
isInteractiveGesture.value = false;
|
|
}
|
|
},
|
|
});
|
|
|
|
// ------------------------------------------------------------------
|
|
// SCROLL HANDLER
|
|
// ------------------------------------------------------------------
|
|
|
|
/**
|
|
* Handle scroll events on the UI thread for smooth performance
|
|
* Tracks scroll position to calculate proper keyboard offset
|
|
*/
|
|
const onScroll = useAnimatedScrollHandler({
|
|
onScroll: (e) => {
|
|
const newScrollPosition = e.contentOffset.y + bottomInset.value;
|
|
|
|
// Preserve scrollPosition when keyboard is closed and list resets to 0
|
|
// This prevents iOS's list reset from overwriting the preserved scroll position
|
|
if (bottomInset.value > 0 || e.contentOffset.y > 0 || scrollPosition.value === 0) {
|
|
scrollPosition.value = newScrollPosition;
|
|
}
|
|
},
|
|
});
|
|
|
|
return {
|
|
keyboardTranslateY,
|
|
onScroll,
|
|
bottomInset,
|
|
scrollOffset,
|
|
keyboardHeight,
|
|
scrollPosition,
|
|
isKeyboardFullyOpen,
|
|
isKeyboardFullyClosed,
|
|
isKeyboardInTransition,
|
|
isInputAccessoryViewMode,
|
|
isTransitioningFromCustomView,
|
|
};
|
|
};
|