mattermost-mobile-upstream/app/keyboard/state_machine/keyboard_utils.ts
Elias Nahum b6fc85385e
Migrate from RNN to Expo router (#9402)
* Expo-Router partial migration

* Full migration to ExpoRouter

* i18n

* add tests

* Merge branch 'main' into expo-router

* align package-lock.json

* i18n

* feedback review

* feedback review

* android 16kb patch

* Refactored emoji picker keyboard using a state machine

* set android launch_screen in drawables instead of layout

* no conditional hooks

* remove duplicate call in test

* address feedback

* fix tests and address more feedback

* small corrections

* small corrections

* small corrections

* fix draft test

* fix draft test 2

* remove navigateToScreen in draff test

* fix nit in draft test

* remove doc file

* Feedback review

* fix snapshots

* improve test coverage

* fix i18n

* Upgrade t o RN 0.83, other dependencies and new architecture (#9737)

* library hardware-keyboard android new arch support fix

* library rnshare android new arch support fix

* library rnutils android new arch support fix

* library secure-pdf-viewer add new arch support

* Enable new arch on iOS and Android

* update emm and network client with new-arch

* update intune to use new-arch

* performance improvements

* fix tests

* fixes post input on new-arch

* fix android emoji picker

* remove doc

* run tests in parallel

* upgrade to React Native 0.83.9, Expo 55, React 19, and New Architecture

Bumps core dependencies to their latest versions:
- React Native 0.77.3 → 0.83.9 (New Architecture enabled)
- React 18.3.1 → 19.2.5
- Expo SDK 52 → 55
- TypeScript 5.8.3 → 6.0.3
- Node.js 22.16.0 → 24.15.0
- react-native-reanimated 3.17.3 → 4.3.0
- Various other dependency upgrades

Migrates iOS AppDelegate from Objective-C to Swift, updates Android
build tooling and Gradle wrapper, refreshes all patches and font
assets, and adapts app code, tests, and CI workflows throughout for
API changes introduced by these upgrades.

* remove unecessary dismissKeyboard in notifications

* feedback review

* update intune to use new-arch

* update @mattermost deps

* missing i18n string

* additional fixes

* update fastlane
2026-05-13 17:44:55 +08:00

124 lines
5.1 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Platform} from 'react-native';
import {isEdgeToEdge} from '@constants/device';
import {DEFAULT_INPUT_ACCESSORY_HEIGHT, MINIMAL_EMOJI_LIST_HEIGHT, SEARCH_BAR_HEIGHT, SEARCH_CONTAINER_PADDING, SEARCH_VISIBILITY_OFFSET} from '@keyboard/constants';
import {InputContainerStateType, type ActionUpdates, type StateEvent, type StateSnapshot} from '@keyboard/state_machine/types';
/**
* Guard function to check if keyboard height indicates a software keyboard
* Heights > 75px are considered meaningful software keyboard heights
*/
export const isSoftwareKeyboard = (snapshot: StateSnapshot, event: StateEvent): boolean => {
'worklet';
const height = event.height ?? snapshot.keyboardEventHeight;
return height > 75;
};
/**
* Guard function to check if keyboard height is near zero
* Heights < 75px are considered zero (keyboard fully dismissed or external keyboard).
* hasZeroKeyboardHeight extends this to catch hardware keyboard events that arrive with
* a small but non-zero height — but only when the event height is itself below the
* software keyboard threshold. A clear software keyboard height (> 75) always wins.
*/
export const isZeroHeight = (snapshot: StateSnapshot, event: StateEvent): boolean => {
'worklet';
const height = event.height ?? snapshot.keyboardEventHeight;
if (height > 75) {
return false;
}
return height < 75 || snapshot.hasZeroKeyboardHeight;
};
/**
* Calculate adjusted keyboard height accounting for tab bar and safe area
* Uses progressive calculation based on keyboard progress
*/
export function calculateAdjustedHeight(
lastKeyboardHeight: number,
tabBarHeight: number,
safeAreaBottom: number,
rawHeight: number,
): number {
'worklet';
const targetHeight = lastKeyboardHeight > 0 ? lastKeyboardHeight : rawHeight;
const keyboardProgress = rawHeight > 0 ? Math.min(1, rawHeight / Math.max(targetHeight, DEFAULT_INPUT_ACCESSORY_HEIGHT)) : 0;
const tabBarAdjustment = tabBarHeight + (safeAreaBottom * keyboardProgress);
return Math.max(0, rawHeight - tabBarAdjustment);
}
/**
* Calculate keyboard-related updates (postInputTranslateY, scrollOffset)
* This drives the scroll padding and scroll compensation
*/
export function calculateKeyboardUpdates(snapshot: StateSnapshot, adjustedHeight: number, rawKeyboardHeight: number): ActionUpdates {
'worklet';
const currentState = snapshot.currentState;
const isOpening = currentState === InputContainerStateType.KEYBOARD_OPENING;
const isOpen = currentState === InputContainerStateType.KEYBOARD_OPEN;
const needsConstantSubtraction = isOpening || isOpen;
const tabBarAdjustment = snapshot.tabBarHeight + snapshot.safeAreaBottom;
// Check if this is an adjustment event (adjustedHeight !== rawKeyboardHeight)
// When adjustment event fires, adjustedHeight is set to lastHeight by processEvent
// We should use that value directly instead of recalculating from rawKeyboardHeight
const needsAdjustment = Math.abs(adjustedHeight - rawKeyboardHeight) > 50;
let effectiveHeight: number;
if (needsAdjustment) {
effectiveHeight = adjustedHeight;
} else if (needsConstantSubtraction) {
// During opening/open/closing: constant subtraction to eliminate gap
if (rawKeyboardHeight <= tabBarAdjustment) {
effectiveHeight = 0;
} else {
effectiveHeight = Math.max(0, rawKeyboardHeight - tabBarAdjustment);
}
} else {
effectiveHeight = adjustedHeight;
}
const updates: ActionUpdates = {
postInputTranslateY: {value: effectiveHeight, animated: false},
};
if (Platform.OS === 'ios') {
updates.scrollOffset = {value: effectiveHeight, animated: false};
}
return updates;
}
/**
* Calculate the total height of search UI components (search bar + padding + visibility offset)
* This is the additional height added on top of keyboard height when emoji search is active
* @param tabBarHeight - Tab bar height (for tablets in split view)
* @param safeAreaBottom - Bottom safe area height (for devices with notch or home indicator)
* @returns The total height of search components
*/
export function getEmojiSearchActiveHeight(tabBarHeight: number, safeAreaBottom: number): number {
'worklet';
const offset = isEdgeToEdge ? SEARCH_VISIBILITY_OFFSET : SEARCH_CONTAINER_PADDING;
return (SEARCH_BAR_HEIGHT + SEARCH_CONTAINER_PADDING + offset + MINIMAL_EMOJI_LIST_HEIGHT + tabBarHeight) - safeAreaBottom;
}
/**
* Calculate emoji search height based on keyboard height
* @param keyboardHeight - Current keyboard height (0 if no keyboard or hardware keyboard)
* @param tabBarHeight - Tab bar height (for tablets in split view)
* @param safeAreaBottom - Bottom safe area height
* @returns The calculated search height
*/
export function calculateSearchHeight(keyboardHeight: number, tabBarHeight: number, safeAreaBottom: number): number {
'worklet';
return keyboardHeight + getEmojiSearchActiveHeight(tabBarHeight, safeAreaBottom);
}