mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-05 18:38:54 +00:00
* bumps minSdkVersion to 30 Older versions are past end-of-life for security patches. voximplant service patch is also updated with minSdkVersion set to 30. * removes manifest attributes no longer needed when minSdk is 30 * removes guard to for version lower than O * removes permission check for versions lower than 28 * remove tests related to unsupported android sdk versions * bumps iOS minSdkVersion to 18 * removes iOS 13/15 guards in hardware-keyboard * removes iOS 15 guards in NotificationService * removes iOS 15 guard in Gekidou network delegate since gekidou is iOS only other platform guards are not needed * removes version guard no longer needed in voximplant patch * removes hasWriteStoragePermission dead stub and its call site * removes unused setSoftKeyboardToAdjustResize from rnutils The function had no call sites in JS/TS code and the iOS implementation was a no-op. * removes version guards and updates config after min version bump * updates stale iOS 17 simulator references in CI to iOS 18.1 Co-Authored-By: Carlos Garcia <carlos.garcia@mattermost.com> * fixes malformed patch file headers - voximplant patch: restore missing diff --git header and index line for VIForegroundServiceModule.java, fix stale hunk counter (+11 -> +7) - notifications patch: fix stale hunk line count (+91 -> +89) after KITKAT branch removal Co-Authored-By: Carlos Garcia <carlos.garcia@mattermost.com> * remove code below the supported OS versions * Bump IntuneMAM SDK to 21.8.0 and MSAL to 2.15.0 --------- Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
190 lines
6.8 KiB
TypeScript
190 lines
6.8 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import React, {useCallback, useEffect, useRef, type ReactNode} from 'react';
|
|
import {BackHandler, DeviceEventEmitter, Platform, type StyleProp, StyleSheet, type ViewStyle, View, type LayoutChangeEvent} from 'react-native';
|
|
import {KeyboardGestureArea} from 'react-native-keyboard-controller';
|
|
import Animated, {scrollTo, useAnimatedStyle} from 'react-native-reanimated';
|
|
import {scheduleOnUI} from 'react-native-worklets';
|
|
|
|
import CustomEmojiPicker from '@components/post_draft/custom_emoji_picker';
|
|
import {Events} from '@constants';
|
|
import {useKeyboardState} from '@context/keyboard_state';
|
|
import useDidMount from '@hooks/did_mount';
|
|
import {dismissKeyboard} from '@utils/keyboard';
|
|
|
|
type Props = {
|
|
children: ReactNode;
|
|
renderList: () => ReactNode;
|
|
textInputNativeID: string;
|
|
containerStyle?: StyleProp<ViewStyle>;
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
gestureArea: {
|
|
justifyContent: 'flex-end',
|
|
flex: 1,
|
|
},
|
|
inputContainer: {
|
|
position: 'absolute',
|
|
bottom: 0,
|
|
left: 0,
|
|
right: 0,
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Wrapper component that provides keyboard-aware behavior for post draft screens
|
|
* Handles keyboard animations, scroll adjustments, and input container positioning
|
|
*/
|
|
export const KeyboardAwarePostDraftContainer = ({
|
|
children,
|
|
renderList,
|
|
textInputNativeID,
|
|
containerStyle,
|
|
}: Props) => {
|
|
const {
|
|
stateContext,
|
|
showInputAccessoryView,
|
|
closeInputAccessoryView,
|
|
listRef,
|
|
} = useKeyboardState();
|
|
|
|
const inputContainerAnimatedStyle = useAnimatedStyle(
|
|
() => {
|
|
return {
|
|
transform: [{translateY: -stateContext.postInputTranslateY.value}],
|
|
};
|
|
},
|
|
[],
|
|
);
|
|
|
|
useDidMount(() => {
|
|
return () => {
|
|
dismissKeyboard();
|
|
};
|
|
});
|
|
|
|
// Ref to track if a layout update is already scheduled
|
|
const layoutUpdateScheduledRef = useRef(false);
|
|
const pendingHeightRef = useRef<number | null>(null);
|
|
|
|
// Helper to apply the batched height update
|
|
const applyBatchedHeightUpdate = useCallback(() => {
|
|
layoutUpdateScheduledRef.current = false;
|
|
|
|
if (pendingHeightRef.current !== null) {
|
|
const heightToSet = pendingHeightRef.current;
|
|
pendingHeightRef.current = null;
|
|
|
|
// Only update if the rounded height changed by more than 0.5px (a real change).
|
|
// This prevents jitter in FlatList paddingTop and improves performance.
|
|
const currentHeight = stateContext.postInputContainerHeight.value;
|
|
const roundedCurrentHeight = Math.round(currentHeight);
|
|
if (roundedCurrentHeight !== heightToSet) {
|
|
stateContext.postInputContainerHeight.value = heightToSet;
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
const onLayout = useCallback((e: LayoutChangeEvent) => {
|
|
const newHeight = e.nativeEvent.layout.height;
|
|
const roundedHeight = Math.round(newHeight);
|
|
|
|
// Store the latest height value
|
|
pendingHeightRef.current = roundedHeight;
|
|
|
|
// If an update is already scheduled, skip scheduling another one
|
|
// This batches all layout updates during animations to a single update per frame
|
|
if (layoutUpdateScheduledRef.current) {
|
|
return;
|
|
}
|
|
|
|
// Schedule update for next frame to batch rapid layout changes during animations
|
|
layoutUpdateScheduledRef.current = true;
|
|
requestAnimationFrame(applyBatchedHeightUpdate);
|
|
}, [applyBatchedHeightUpdate]);
|
|
|
|
useEffect(() => {
|
|
if (Platform.OS !== 'android') {
|
|
return undefined;
|
|
}
|
|
|
|
const backHandler = BackHandler.addEventListener('hardwareBackPress', () => {
|
|
if (showInputAccessoryView) {
|
|
closeInputAccessoryView();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
});
|
|
|
|
return () => backHandler.remove();
|
|
}, [closeInputAccessoryView, showInputAccessoryView]);
|
|
|
|
useEffect(() => {
|
|
const listener = DeviceEventEmitter.addListener(Events.CLOSE_INPUT_ACCESSORY_VIEW, () => {
|
|
closeInputAccessoryView();
|
|
});
|
|
|
|
return () => listener.remove();
|
|
}, [closeInputAccessoryView]);
|
|
|
|
const scrollToEmojiPickerCompensation = useCallback((offset: number) => {
|
|
scheduleOnUI(() => scrollTo(listRef, 0, offset, false));
|
|
}, [listRef]);
|
|
|
|
// After emoji picker renders, adjust heights and scroll to keep messages visible
|
|
// On iOS, contentInset changes cause the list to shift, so we need to scroll to compensate
|
|
// On Android, marginBottom is used instead and doesn't require scroll adjustment
|
|
useEffect(() => {
|
|
if (showInputAccessoryView) {
|
|
|
|
// Wait one frame to ensure emoji picker has rendered
|
|
requestAnimationFrame(() => {
|
|
// Use targetHeight instead of inputAccessoryHeight because inputAccessoryHeight
|
|
// is still animating (via withTiming) when this runs, so we'd capture a tiny intermediate value
|
|
const emojiPickerHeight = stateContext.targetHeight.value;
|
|
const currentScroll = stateContext.scrollPosition.value;
|
|
|
|
// Only perform scroll adjustment on iOS
|
|
// Android uses marginBottom which doesn't require scroll compensation
|
|
if (Platform.OS === 'ios' && listRef.current) {
|
|
// For inverted list: when bottomInset increases, content shifts UP visually. Scroll UP to compensate.
|
|
scrollToEmojiPickerCompensation(currentScroll - emojiPickerHeight);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Only depend on showInputAccessoryView - the effect should only run when emoji picker visibility changes
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [showInputAccessoryView]);
|
|
|
|
return (
|
|
<KeyboardGestureArea
|
|
textInputNativeID={textInputNativeID}
|
|
style={styles.gestureArea}
|
|
enableSwipeToDismiss={false} // this applies only to Android
|
|
>
|
|
<View style={styles.gestureArea}>
|
|
<View style={containerStyle}>
|
|
{renderList()}
|
|
</View>
|
|
<Animated.View
|
|
style={[
|
|
inputContainerAnimatedStyle,
|
|
styles.inputContainer,
|
|
]}
|
|
>
|
|
<View onLayout={onLayout}>
|
|
{children}
|
|
</View>
|
|
</Animated.View>
|
|
{showInputAccessoryView && <CustomEmojiPicker/>}
|
|
</View>
|
|
</KeyboardGestureArea>
|
|
);
|
|
};
|