Improve autocomplete behaviour (#6559)
* Fix positioning issues with Autocomplete * Fix positioning for iOS and iPad * Adapt search to new autocomplete approach * Adapt for android * Fix lint * Fix calculations on channel edit * Address feedback * Address feedback Co-authored-by: Daniel Espino <danielespino@MacBook-Pro-de-Daniel.local>
This commit is contained in:
parent
6e3b2c2bd9
commit
afd818996e
12 changed files with 303 additions and 175 deletions
|
|
@ -3,9 +3,8 @@
|
|||
|
||||
import React, {useMemo, useState} from 'react';
|
||||
import {Platform, useWindowDimensions, View} from 'react-native';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import {LIST_BOTTOM, MAX_LIST_DIFF, MAX_LIST_HEIGHT, MAX_LIST_TABLET_DIFF} from '@constants/autocomplete';
|
||||
import {MAX_LIST_HEIGHT, MAX_LIST_TABLET_DIFF} from '@constants/autocomplete';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
|
@ -54,11 +53,9 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
|||
|
||||
type Props = {
|
||||
cursorPosition: number;
|
||||
postInputTop: number;
|
||||
paddingTop?: number;
|
||||
position: number;
|
||||
rootId?: string;
|
||||
channelId?: string;
|
||||
fixedBottomPosition?: boolean;
|
||||
isSearch?: boolean;
|
||||
value: string;
|
||||
enableDateSuggestion?: boolean;
|
||||
|
|
@ -66,20 +63,19 @@ type Props = {
|
|||
nestedScrollEnabled?: boolean;
|
||||
updateValue: (v: string) => void;
|
||||
hasFilesAttached?: boolean;
|
||||
maxHeightOverride?: number;
|
||||
availableSpace: number;
|
||||
inPost?: boolean;
|
||||
growDown?: boolean;
|
||||
}
|
||||
|
||||
const Autocomplete = ({
|
||||
cursorPosition,
|
||||
postInputTop,
|
||||
paddingTop,
|
||||
position,
|
||||
rootId,
|
||||
channelId,
|
||||
isSearch = false,
|
||||
fixedBottomPosition,
|
||||
value,
|
||||
maxHeightOverride,
|
||||
availableSpace,
|
||||
|
||||
//enableDateSuggestion = false,
|
||||
isAppsEnabled,
|
||||
|
|
@ -87,12 +83,12 @@ const Autocomplete = ({
|
|||
updateValue,
|
||||
hasFilesAttached,
|
||||
inPost = false,
|
||||
growDown = false,
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
const isTablet = useIsTablet();
|
||||
const dimensions = useWindowDimensions();
|
||||
const style = getStyleFromTheme(theme);
|
||||
const insets = useSafeAreaInsets();
|
||||
const dimensions = useWindowDimensions();
|
||||
|
||||
const [showingAtMention, setShowingAtMention] = useState(false);
|
||||
const [showingChannelMention, setShowingChannelMention] = useState(false);
|
||||
|
|
@ -106,44 +102,31 @@ const Autocomplete = ({
|
|||
const appsTakeOver = showingAppCommand;
|
||||
const showCommands = !(showingChannelMention || showingEmoji || showingAtMention);
|
||||
|
||||
const maxListHeight = useMemo(() => {
|
||||
if (maxHeightOverride) {
|
||||
return maxHeightOverride;
|
||||
}
|
||||
const isLandscape = dimensions.width > dimensions.height;
|
||||
let postInputDiff = 0;
|
||||
if (isTablet && postInputTop && isLandscape) {
|
||||
postInputDiff = MAX_LIST_TABLET_DIFF;
|
||||
} else if (postInputTop) {
|
||||
postInputDiff = MAX_LIST_DIFF;
|
||||
}
|
||||
return MAX_LIST_HEIGHT - postInputDiff;
|
||||
}, [maxHeightOverride, postInputTop, isTablet, dimensions.width]);
|
||||
|
||||
const wrapperStyles = useMemo(() => {
|
||||
const s = [];
|
||||
if (Platform.OS === 'ios') {
|
||||
s.push(style.shadow);
|
||||
}
|
||||
if (isSearch) {
|
||||
s.push(style.base, paddingTop ? {top: paddingTop} : style.searchContainer, {maxHeight: maxListHeight});
|
||||
}
|
||||
return s;
|
||||
}, [style, isSearch && maxListHeight, paddingTop]);
|
||||
}, [style]);
|
||||
|
||||
const containerStyles = useMemo(() => {
|
||||
const s = [];
|
||||
if (!isSearch && !fixedBottomPosition) {
|
||||
const iOSInsets = Platform.OS === 'ios' && (!isTablet || rootId) ? insets.bottom : 0;
|
||||
s.push(style.base, {bottom: postInputTop + LIST_BOTTOM + iOSInsets});
|
||||
} else if (fixedBottomPosition) {
|
||||
s.push(style.base, {bottom: 0});
|
||||
const s = [style.base];
|
||||
if (growDown) {
|
||||
s.push({top: -position});
|
||||
} else {
|
||||
s.push({bottom: position});
|
||||
}
|
||||
if (hasElements) {
|
||||
s.push(style.borders);
|
||||
}
|
||||
return s;
|
||||
}, [!isSearch, isTablet, hasElements, postInputTop]);
|
||||
}, [hasElements, position, growDown, style]);
|
||||
|
||||
const isLandscape = dimensions.width > dimensions.height;
|
||||
const maxHeightAdjust = (isTablet && isLandscape) ? MAX_LIST_TABLET_DIFF : 0;
|
||||
const defaultMaxHeight = MAX_LIST_HEIGHT - maxHeightAdjust;
|
||||
const maxListHeight = Math.min(availableSpace, defaultMaxHeight);
|
||||
|
||||
return (
|
||||
<View
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ type Props = {
|
|||
nestedScrollEnabled: boolean;
|
||||
skinTone: string;
|
||||
hasFilesAttached?: boolean;
|
||||
inPost?: boolean;
|
||||
inPost: boolean;
|
||||
}
|
||||
const EmojiSuggestion = ({
|
||||
cursorPosition,
|
||||
|
|
@ -88,7 +88,7 @@ const EmojiSuggestion = ({
|
|||
nestedScrollEnabled,
|
||||
skinTone,
|
||||
hasFilesAttached = false,
|
||||
inPost = true,
|
||||
inPost,
|
||||
}: Props) => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const theme = useTheme();
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import {Platform, StyleProp, Switch, Text, TextStyle, TouchableOpacity, View, ViewStyle} from 'react-native';
|
||||
import {LayoutChangeEvent, Platform, StyleProp, Switch, Text, TextStyle, TouchableOpacity, View, ViewStyle} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
|
|
@ -114,6 +114,7 @@ export type OptionItemProps = {
|
|||
testID?: string;
|
||||
type: OptionType;
|
||||
value?: string;
|
||||
onLayout?: (event: LayoutChangeEvent) => void;
|
||||
}
|
||||
|
||||
const OptionItem = ({
|
||||
|
|
@ -134,6 +135,7 @@ const OptionItem = ({
|
|||
testID = 'optionItem',
|
||||
type,
|
||||
value,
|
||||
onLayout,
|
||||
}: OptionItemProps) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
|
@ -227,6 +229,7 @@ const OptionItem = ({
|
|||
<View
|
||||
testID={testID}
|
||||
style={[styles.container, containerStyle]}
|
||||
onLayout={onLayout}
|
||||
>
|
||||
<View style={styles.row}>
|
||||
<View style={styles.labelContainer}>
|
||||
|
|
|
|||
|
|
@ -4,15 +4,18 @@
|
|||
import React, {RefObject, useEffect, useState} from 'react';
|
||||
import {Platform, View} from 'react-native';
|
||||
import {KeyboardTrackingView, KeyboardTrackingViewRef} from 'react-native-keyboard-tracking-view';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import Autocomplete from '@components/autocomplete';
|
||||
import {View as ViewConstants} from '@constants';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {useIsTablet, useKeyboardHeight} from '@hooks/device';
|
||||
import {useDefaultHeaderHeight} from '@hooks/header';
|
||||
|
||||
import Archived from './archived';
|
||||
import DraftHandler from './draft_handler';
|
||||
import ReadOnly from './read_only';
|
||||
|
||||
const AUTOCOMPLETE_ADJUST = -5;
|
||||
type Props = {
|
||||
testID?: string;
|
||||
accessoriesContainerID?: string;
|
||||
|
|
@ -27,6 +30,8 @@ type Props = {
|
|||
rootId?: string;
|
||||
scrollViewNativeID?: string;
|
||||
keyboardTracker: RefObject<KeyboardTrackingViewRef>;
|
||||
containerHeight: number;
|
||||
isChannelScreen: boolean;
|
||||
}
|
||||
|
||||
const {KEYBOARD_TRACKING_OFFSET} = ViewConstants;
|
||||
|
|
@ -45,11 +50,16 @@ function PostDraft({
|
|||
rootId = '',
|
||||
scrollViewNativeID,
|
||||
keyboardTracker,
|
||||
containerHeight,
|
||||
isChannelScreen,
|
||||
}: Props) {
|
||||
const [value, setValue] = useState(message);
|
||||
const [cursorPosition, setCursorPosition] = useState(message.length);
|
||||
const [postInputTop, setPostInputTop] = useState(0);
|
||||
const isTablet = useIsTablet();
|
||||
const keyboardHeight = useKeyboardHeight(keyboardTracker);
|
||||
const insets = useSafeAreaInsets();
|
||||
const headerHeight = useDefaultHeaderHeight();
|
||||
|
||||
// Update draft in case we switch channels or threads
|
||||
useEffect(() => {
|
||||
|
|
@ -92,9 +102,17 @@ function PostDraft({
|
|||
/>
|
||||
);
|
||||
|
||||
const keyboardAdjustment = (isTablet && isChannelScreen) ? KEYBOARD_TRACKING_OFFSET : 0;
|
||||
const insetsAdjustment = (isTablet && isChannelScreen) ? 0 : insets.bottom;
|
||||
const autocompletePosition = AUTOCOMPLETE_ADJUST + Platform.select({
|
||||
ios: (keyboardHeight ? keyboardHeight - keyboardAdjustment : (postInputTop + insetsAdjustment)),
|
||||
default: postInputTop + insetsAdjustment,
|
||||
});
|
||||
const autocompleteAvailableSpace = containerHeight - autocompletePosition - (isChannelScreen ? headerHeight + insets.top : 0);
|
||||
|
||||
const autoComplete = (
|
||||
<Autocomplete
|
||||
postInputTop={postInputTop}
|
||||
position={autocompletePosition}
|
||||
updateValue={setValue}
|
||||
rootId={rootId}
|
||||
channelId={channelId}
|
||||
|
|
@ -102,6 +120,8 @@ function PostDraft({
|
|||
value={value}
|
||||
isSearch={isSearch}
|
||||
hasFilesAttached={Boolean(files?.length)}
|
||||
inPost={true}
|
||||
availableSpace={autocompleteAvailableSpace}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -124,7 +144,7 @@ function PostDraft({
|
|||
>
|
||||
{draftHandler}
|
||||
</KeyboardTrackingView>
|
||||
<View nativeID={accessoriesContainerID}>
|
||||
<View>
|
||||
{autoComplete}
|
||||
</View>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -17,11 +17,8 @@ export const ALL_SEARCH_FLAGS_REGEX = /\b\w+:/g;
|
|||
|
||||
export const CODE_REGEX = /(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)| *(`{3,}|~{3,})[ .]*(\S+)? *\n([\s\S]*?\s*)\3 *(?:\n+|$)/g;
|
||||
|
||||
export const LIST_BOTTOM = -5;
|
||||
|
||||
export const MAX_LIST_HEIGHT = 280;
|
||||
export const MAX_LIST_DIFF = 50;
|
||||
export const MAX_LIST_TABLET_DIFF = 140;
|
||||
export const MAX_LIST_HEIGHT = 230;
|
||||
export const MAX_LIST_TABLET_DIFF = 90;
|
||||
|
||||
export default {
|
||||
ALL_SEARCH_FLAGS_REGEX,
|
||||
|
|
@ -32,7 +29,5 @@ export default {
|
|||
CHANNEL_MENTION_SEARCH_REGEX,
|
||||
CODE_REGEX,
|
||||
DATE_MENTION_SEARCH_REGEX,
|
||||
LIST_BOTTOM,
|
||||
MAX_LIST_HEIGHT,
|
||||
MAX_LIST_DIFF,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
import {AppState, Keyboard, NativeModules, useWindowDimensions} from 'react-native';
|
||||
import React, {RefObject, useEffect, useRef, useState} from 'react';
|
||||
import {AppState, Keyboard, NativeModules, Platform, useWindowDimensions, View} from 'react-native';
|
||||
import {KeyboardTrackingViewRef} from 'react-native-keyboard-tracking-view';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import {Device} from '@constants';
|
||||
|
||||
|
|
@ -43,15 +45,44 @@ export function useIsTablet() {
|
|||
return Device.IS_TABLET && !isSplitView;
|
||||
}
|
||||
|
||||
export function useKeyboardHeight() {
|
||||
export function useKeyboardHeight(keyboardTracker?: React.RefObject<KeyboardTrackingViewRef>) {
|
||||
const [keyboardHeight, setKeyboardHeight] = useState(0);
|
||||
const updateTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
// This is a magic number. With tracking view, to properly get the final position, this had to be added.
|
||||
const KEYBOARD_TRACKINGVIEW_SEPARATION = 4;
|
||||
|
||||
const updateValue = (v: number) => {
|
||||
if (updateTimeout.current != null) {
|
||||
clearTimeout(updateTimeout.current);
|
||||
updateTimeout.current = null;
|
||||
}
|
||||
updateTimeout.current = setTimeout(() => {
|
||||
setKeyboardHeight(v);
|
||||
updateTimeout.current = null;
|
||||
}, 200);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const show = Keyboard.addListener('keyboardWillShow', (event) => {
|
||||
setKeyboardHeight(event.endCoordinates.height);
|
||||
const show = Keyboard.addListener(Platform.select({ios: 'keyboardWillShow', default: 'keyboardDidShow'}), async (event) => {
|
||||
if (keyboardTracker?.current) {
|
||||
const props = await keyboardTracker.current.getNativeProps();
|
||||
if (props.keyboardHeight) {
|
||||
updateValue((props.trackingViewHeight + props.keyboardHeight) - KEYBOARD_TRACKINGVIEW_SEPARATION);
|
||||
} else {
|
||||
updateValue((props.trackingViewHeight + insets.bottom) - KEYBOARD_TRACKINGVIEW_SEPARATION);
|
||||
}
|
||||
} else {
|
||||
updateValue(event.endCoordinates.height);
|
||||
}
|
||||
});
|
||||
|
||||
const hide = Keyboard.addListener('keyboardWillHide', () => {
|
||||
const hide = Keyboard.addListener(Platform.select({ios: 'keyboardWillHide', default: 'keyboardDidHide'}), () => {
|
||||
if (updateTimeout.current != null) {
|
||||
clearTimeout(updateTimeout.current);
|
||||
updateTimeout.current = null;
|
||||
}
|
||||
setKeyboardHeight(0);
|
||||
});
|
||||
|
||||
|
|
@ -59,7 +90,24 @@ export function useKeyboardHeight() {
|
|||
show.remove();
|
||||
hide.remove();
|
||||
};
|
||||
}, []);
|
||||
}, [keyboardTracker && insets.bottom]);
|
||||
|
||||
return keyboardHeight;
|
||||
}
|
||||
|
||||
export function useModalPosition(viewRef: RefObject<View>, deps?: React.DependencyList) {
|
||||
const [modalPosition, setModalPosition] = useState(0);
|
||||
const isTablet = useIsTablet();
|
||||
|
||||
useEffect(() => {
|
||||
if (Platform.OS === 'ios' && isTablet) {
|
||||
viewRef.current?.measureInWindow((_, y) => {
|
||||
if (y !== modalPosition) {
|
||||
setModalPosition(y);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, deps);
|
||||
|
||||
return modalPosition;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect, useRef, useState} from 'react';
|
||||
import {BackHandler, DeviceEventEmitter, NativeEventSubscription, StyleSheet, View} from 'react-native';
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react';
|
||||
import {BackHandler, DeviceEventEmitter, LayoutChangeEvent, NativeEventSubscription, StyleSheet, View} from 'react-native';
|
||||
import {KeyboardTrackingViewRef} from 'react-native-keyboard-tracking-view';
|
||||
import {Edge, SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
|
|
@ -61,6 +61,7 @@ const Channel = ({
|
|||
const switchingChannels = useChannelSwitch();
|
||||
const defaultHeight = useDefaultHeaderHeight();
|
||||
const postDraftRef = useRef<KeyboardTrackingViewRef>(null);
|
||||
const [containerHeight, setContainerHeight] = useState(0);
|
||||
|
||||
const shouldRender = !switchingTeam && !switchingChannels && shouldRenderPosts && Boolean(channelId);
|
||||
|
||||
|
|
@ -113,6 +114,10 @@ const Channel = ({
|
|||
};
|
||||
}, [channelId]);
|
||||
|
||||
const onLayout = useCallback((e: LayoutChangeEvent) => {
|
||||
setContainerHeight(e.nativeEvent.layout.height);
|
||||
}, []);
|
||||
|
||||
let callsComponents: JSX.Element | null = null;
|
||||
const showJoinCallBanner = isCallInCurrentChannel && !isInCurrentChannelCall;
|
||||
if (isCallsPluginEnabled && (showJoinCallBanner || isInACall)) {
|
||||
|
|
@ -136,6 +141,7 @@ const Channel = ({
|
|||
mode='margin'
|
||||
edges={edges}
|
||||
testID='channel.screen'
|
||||
onLayout={onLayout}
|
||||
>
|
||||
<ChannelHeader
|
||||
channelId={channelId}
|
||||
|
|
@ -159,6 +165,8 @@ const Channel = ({
|
|||
scrollViewNativeID={channelId}
|
||||
accessoriesContainerID={ACCESSORIES_CONTAINER_NATIVE_ID}
|
||||
testID='channel.post_draft'
|
||||
containerHeight={containerHeight}
|
||||
isChannelScreen={true}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useState, useRef, useCallback} from 'react';
|
||||
import React, {useState, useRef, useCallback, useEffect} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {
|
||||
LayoutChangeEvent,
|
||||
|
|
@ -12,10 +12,10 @@ import {
|
|||
NativeSyntheticEvent,
|
||||
NativeScrollEvent,
|
||||
Platform,
|
||||
KeyboardEvent,
|
||||
useWindowDimensions,
|
||||
} from 'react-native';
|
||||
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
|
||||
import {SafeAreaView} from 'react-native-safe-area-context';
|
||||
import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import Autocomplete from '@components/autocomplete';
|
||||
import ErrorText from '@components/error_text';
|
||||
|
|
@ -25,8 +25,7 @@ import Loading from '@components/loading';
|
|||
import OptionItem from '@components/option_item';
|
||||
import {General, Channel} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import useHeaderHeight from '@hooks/header';
|
||||
import {useIsTablet, useKeyboardHeight, useModalPosition} from '@hooks/device';
|
||||
import {t} from '@i18n';
|
||||
import {
|
||||
changeOpacity,
|
||||
|
|
@ -35,12 +34,18 @@ import {
|
|||
} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
const FIELD_MARGIN_BOTTOM = 24;
|
||||
const MAKE_PRIVATE_MARGIN_BOTTOM = 32;
|
||||
const BOTTOM_AUTOCOMPLETE_SEPARATION = Platform.select({ios: 10, default: 10});
|
||||
const LIST_PADDING = 32;
|
||||
const AUTOCOMPLETE_ADJUST = 5;
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => ({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollView: {
|
||||
paddingVertical: 32,
|
||||
paddingVertical: LIST_PADDING,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
errorContainer: {
|
||||
|
|
@ -56,10 +61,10 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => ({
|
|||
justifyContent: 'center',
|
||||
},
|
||||
makePrivateContainer: {
|
||||
marginBottom: 32,
|
||||
marginBottom: MAKE_PRIVATE_MARGIN_BOTTOM,
|
||||
},
|
||||
fieldContainer: {
|
||||
marginBottom: 24,
|
||||
marginBottom: FIELD_MARGIN_BOTTOM,
|
||||
},
|
||||
helpText: {
|
||||
...typography('Body', 75, 'Regular'),
|
||||
|
|
@ -101,8 +106,7 @@ export default function ChannelInfoForm({
|
|||
}: Props) {
|
||||
const intl = useIntl();
|
||||
const {formatMessage} = intl;
|
||||
const isTablet = useIsTablet();
|
||||
const headerHeight = useHeaderHeight();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
|
@ -115,10 +119,22 @@ export default function ChannelInfoForm({
|
|||
|
||||
const updateScrollTimeout = useRef<NodeJS.Timeout>();
|
||||
|
||||
const mainView = useRef<View>(null);
|
||||
const modalPosition = useModalPosition(mainView);
|
||||
|
||||
const dimensions = useWindowDimensions();
|
||||
const isTablet = useIsTablet();
|
||||
|
||||
const keyboardHeight = useKeyboardHeight();
|
||||
const [keyboardVisible, setKeyBoardVisible] = useState(false);
|
||||
const [keyboardHeight, setKeyboardHeight] = useState(0);
|
||||
const [scrollPosition, setScrollPosition] = useState(0);
|
||||
|
||||
const [wrapperHeight, setWrapperHeight] = useState(0);
|
||||
const [errorHeight, setErrorHeight] = useState(0);
|
||||
const [displayNameFieldHeight, setDisplayNameFieldHeight] = useState(0);
|
||||
const [makePrivateHeight, setMakePrivateHeight] = useState(0);
|
||||
const [purposeFieldHeight, setPurposeFieldHeight] = useState(0);
|
||||
const [headerFieldHeight, setHeaderFieldHeight] = useState(0);
|
||||
const [headerPosition, setHeaderPosition] = useState(0);
|
||||
|
||||
const optionalText = formatMessage({id: t('channel_modal.optional'), defaultMessage: '(optional)'});
|
||||
|
|
@ -150,28 +166,12 @@ export default function ChannelInfoForm({
|
|||
scrollViewRef.current?.scrollToPosition(0, 0, true);
|
||||
}, []);
|
||||
|
||||
const onHeaderLayout = useCallback(({nativeEvent}: LayoutChangeEvent) => {
|
||||
setHeaderPosition(nativeEvent.layout.y);
|
||||
}, []);
|
||||
|
||||
const scrollHeaderToTop = useCallback(() => {
|
||||
if (scrollViewRef?.current) {
|
||||
scrollViewRef.current?.scrollToPosition(0, headerPosition);
|
||||
}
|
||||
}, [headerPosition]);
|
||||
|
||||
const onKeyboardDidShow = useCallback((frames: KeyboardEvent) => {
|
||||
setKeyBoardVisible(true);
|
||||
if (Platform.OS === 'android') {
|
||||
setKeyboardHeight(frames.endCoordinates.height);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onKeyboardDidHide = useCallback(() => {
|
||||
setKeyBoardVisible(false);
|
||||
setKeyboardHeight(0);
|
||||
}, []);
|
||||
|
||||
const onScroll = useCallback((e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
const pos = e.nativeEvent.contentOffset.y;
|
||||
if (updateScrollTimeout.current) {
|
||||
|
|
@ -183,6 +183,35 @@ export default function ChannelInfoForm({
|
|||
}, 200);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (keyboardVisible && !keyboardHeight) {
|
||||
setKeyBoardVisible(false);
|
||||
}
|
||||
if (!keyboardVisible && keyboardHeight) {
|
||||
setKeyBoardVisible(true);
|
||||
}
|
||||
}, [keyboardHeight]);
|
||||
|
||||
const onLayoutError = useCallback((e: LayoutChangeEvent) => {
|
||||
setErrorHeight(e.nativeEvent.layout.height);
|
||||
}, []);
|
||||
const onLayoutMakePrivate = useCallback((e: LayoutChangeEvent) => {
|
||||
setMakePrivateHeight(e.nativeEvent.layout.height);
|
||||
}, []);
|
||||
const onLayoutDisplayName = useCallback((e: LayoutChangeEvent) => {
|
||||
setDisplayNameFieldHeight(e.nativeEvent.layout.height);
|
||||
}, []);
|
||||
const onLayoutPurpose = useCallback((e: LayoutChangeEvent) => {
|
||||
setPurposeFieldHeight(e.nativeEvent.layout.height);
|
||||
}, []);
|
||||
const onLayoutHeader = useCallback((e: LayoutChangeEvent) => {
|
||||
setHeaderFieldHeight(e.nativeEvent.layout.height);
|
||||
setHeaderPosition(e.nativeEvent.layout.y);
|
||||
}, []);
|
||||
const onLayoutWrapper = useCallback((e: LayoutChangeEvent) => {
|
||||
setWrapperHeight(e.nativeEvent.layout.height);
|
||||
}, []);
|
||||
|
||||
if (saving) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
|
|
@ -202,6 +231,7 @@ export default function ChannelInfoForm({
|
|||
<SafeAreaView
|
||||
edges={['bottom', 'left', 'right']}
|
||||
style={styles.errorContainer}
|
||||
onLayout={onLayoutError}
|
||||
>
|
||||
<View style={styles.errorWrapper}>
|
||||
<ErrorText
|
||||
|
|
@ -213,21 +243,42 @@ export default function ChannelInfoForm({
|
|||
);
|
||||
}
|
||||
|
||||
const platformHeaderHeight = headerHeight.defaultHeight + Platform.select({ios: 10, default: headerHeight.defaultHeight + 10});
|
||||
const postInputTop = (headerPosition + scrollPosition + platformHeaderHeight) - keyboardHeight;
|
||||
const bottomSpace = (dimensions.height - wrapperHeight - modalPosition);
|
||||
const otherElementsSize = LIST_PADDING + errorHeight +
|
||||
(showSelector ? makePrivateHeight + MAKE_PRIVATE_MARGIN_BOTTOM : 0) +
|
||||
(displayHeaderOnly ? 0 : purposeFieldHeight + FIELD_MARGIN_BOTTOM + displayNameFieldHeight + FIELD_MARGIN_BOTTOM);
|
||||
|
||||
const keyboardOverlap = Platform.select({
|
||||
ios: isTablet ?
|
||||
Math.max(0, keyboardHeight - bottomSpace) :
|
||||
keyboardHeight || insets.bottom,
|
||||
default: 0});
|
||||
const workingSpace = wrapperHeight - keyboardOverlap;
|
||||
const spaceOnTop = otherElementsSize - scrollPosition - AUTOCOMPLETE_ADJUST;
|
||||
const spaceOnBottom = (workingSpace + scrollPosition) - (otherElementsSize + headerFieldHeight + BOTTOM_AUTOCOMPLETE_SEPARATION);
|
||||
const insetsAdjust = keyboardHeight - keyboardHeight ? insets.bottom : 0;
|
||||
const keyboardAdjust = Platform.select({
|
||||
ios: isTablet ?
|
||||
keyboardOverlap :
|
||||
insetsAdjust,
|
||||
default: 0,
|
||||
});
|
||||
const autocompletePosition = spaceOnTop > spaceOnBottom ? (workingSpace + scrollPosition + AUTOCOMPLETE_ADJUST + keyboardAdjust) - otherElementsSize : (workingSpace + scrollPosition + keyboardAdjust) - (otherElementsSize + headerFieldHeight);
|
||||
const autocompleteAvailableSpace = spaceOnTop > spaceOnBottom ? spaceOnTop : spaceOnBottom;
|
||||
const growUp = spaceOnTop > spaceOnBottom;
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
edges={['bottom', 'left', 'right']}
|
||||
style={styles.container}
|
||||
testID='create_or_edit_channel.screen'
|
||||
onLayout={onLayoutWrapper}
|
||||
ref={mainView}
|
||||
>
|
||||
<KeyboardAwareScrollView
|
||||
testID={'create_or_edit_channel.scrollview'}
|
||||
ref={scrollViewRef}
|
||||
keyboardShouldPersistTaps={'always'}
|
||||
onKeyboardDidShow={onKeyboardDidShow}
|
||||
onKeyboardDidHide={onKeyboardDidHide}
|
||||
enableAutomaticScroll={!keyboardVisible}
|
||||
contentContainerStyle={styles.scrollView}
|
||||
onScroll={onScroll}
|
||||
|
|
@ -247,6 +298,7 @@ export default function ChannelInfoForm({
|
|||
selected={isPrivate}
|
||||
icon={'lock-outline'}
|
||||
containerStyle={styles.makePrivateContainer}
|
||||
onLayout={onLayoutMakePrivate}
|
||||
/>
|
||||
)}
|
||||
{!displayHeaderOnly && (
|
||||
|
|
@ -270,8 +322,12 @@ export default function ChannelInfoForm({
|
|||
ref={nameInput}
|
||||
containerStyle={styles.fieldContainer}
|
||||
theme={theme}
|
||||
onLayout={onLayoutDisplayName}
|
||||
/>
|
||||
<View style={styles.fieldContainer}>
|
||||
<View
|
||||
style={styles.fieldContainer}
|
||||
onLayout={onLayoutPurpose}
|
||||
>
|
||||
<FloatingTextInput
|
||||
autoCorrect={false}
|
||||
autoCapitalize={'none'}
|
||||
|
|
@ -301,7 +357,6 @@ export default function ChannelInfoForm({
|
|||
)}
|
||||
<View
|
||||
style={styles.fieldContainer}
|
||||
onLayout={onHeaderLayout}
|
||||
>
|
||||
<FloatingTextInput
|
||||
autoCorrect={false}
|
||||
|
|
@ -322,6 +377,7 @@ export default function ChannelInfoForm({
|
|||
ref={headerInput}
|
||||
theme={theme}
|
||||
onFocus={scrollHeaderToTop}
|
||||
onLayout={onLayoutHeader}
|
||||
/>
|
||||
<FormattedText
|
||||
style={styles.helpText}
|
||||
|
|
@ -336,13 +392,14 @@ export default function ChannelInfoForm({
|
|||
</KeyboardAwareScrollView>
|
||||
<View>
|
||||
<Autocomplete
|
||||
postInputTop={postInputTop}
|
||||
position={autocompletePosition}
|
||||
updateValue={onHeaderChange}
|
||||
cursorPosition={header.length}
|
||||
value={header}
|
||||
nestedScrollEnabled={true}
|
||||
maxHeightOverride={isTablet ? 200 : undefined}
|
||||
availableSpace={autocompleteAvailableSpace}
|
||||
inPost={false}
|
||||
growDown={!growUp}
|
||||
/>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@
|
|||
import React, {useCallback, useEffect, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Alert, Keyboard, KeyboardType, LayoutChangeEvent, Platform, SafeAreaView, useWindowDimensions, View} from 'react-native';
|
||||
import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import {deletePost, editPost} from '@actions/remote/post';
|
||||
import Autocomplete from '@components/autocomplete';
|
||||
import Loading from '@components/loading';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {useIsTablet, useKeyboardHeight, useModalPosition} from '@hooks/device';
|
||||
import useDidUpdate from '@hooks/did_update';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import PostError from '@screens/edit_post/post_error';
|
||||
|
|
@ -23,6 +23,8 @@ import EditPostInput, {EditPostInputRef} from './edit_post_input';
|
|||
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
|
||||
const AUTOCOMPLETE_SEPARATION = 8;
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
body: {
|
||||
|
|
@ -57,17 +59,20 @@ const EditPost = ({componentId, maxPostSize, post, closeButtonId, hasFilesAttach
|
|||
const [errorLine, setErrorLine] = useState<string | undefined>();
|
||||
const [errorExtra, setErrorExtra] = useState<string | undefined>();
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const layoutHeight = useSharedValue(0);
|
||||
const keyboardHeight = useSharedValue(0);
|
||||
const [containerHeight, setContainerHeight] = useState(0);
|
||||
|
||||
const mainView = useRef<View>(null);
|
||||
const modalPosition = useModalPosition(mainView);
|
||||
|
||||
const postInputRef = useRef<EditPostInputRef>(null);
|
||||
const theme = useTheme();
|
||||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
const isTablet = useIsTablet();
|
||||
const {width, height} = useWindowDimensions();
|
||||
const isLandscape = width > height;
|
||||
const styles = getStyleSheet(theme);
|
||||
const keyboardHeight = useKeyboardHeight();
|
||||
const insets = useSafeAreaInsets();
|
||||
const dimensions = useWindowDimensions();
|
||||
const isTablet = useIsTablet();
|
||||
|
||||
useEffect(() => {
|
||||
setButtons(componentId, {
|
||||
|
|
@ -80,31 +85,6 @@ const EditPost = ({componentId, maxPostSize, post, closeButtonId, hasFilesAttach
|
|||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const showListener = Keyboard.addListener('keyboardWillShow', (e) => {
|
||||
const {height: end} = e.endCoordinates;
|
||||
|
||||
// on iPad if we use the hardware keyboard multiply its height by 2
|
||||
// otherwise use the software keyboard height
|
||||
const minKeyboardHeight = end < 100 ? end * 2 : end;
|
||||
keyboardHeight.value = minKeyboardHeight;
|
||||
});
|
||||
const hideListener = Keyboard.addListener('keyboardWillHide', () => {
|
||||
if (isTablet) {
|
||||
const offset = isLandscape ? 60 : 0;
|
||||
keyboardHeight.value = ((height - (layoutHeight.value + offset)) / 2);
|
||||
return;
|
||||
}
|
||||
|
||||
keyboardHeight.value = 0;
|
||||
});
|
||||
|
||||
return () => {
|
||||
showListener.remove();
|
||||
hideListener.remove();
|
||||
};
|
||||
}, [isTablet, height]);
|
||||
|
||||
useEffect(() => {
|
||||
const t = setTimeout(() => {
|
||||
postInputRef.current?.focus();
|
||||
|
|
@ -127,10 +107,6 @@ const EditPost = ({componentId, maxPostSize, post, closeButtonId, hasFilesAttach
|
|||
dismissModal({componentId});
|
||||
}, []);
|
||||
|
||||
const onLayout = useCallback((e: LayoutChangeEvent) => {
|
||||
layoutHeight.value = e.nativeEvent.layout.height;
|
||||
}, [height]);
|
||||
|
||||
const onTextSelectionChange = useCallback((curPos: number = cursorPosition) => {
|
||||
if (Platform.OS === 'ios') {
|
||||
setKeyboardType(switchKeyboardForCodeBlocks(postMessage, curPos));
|
||||
|
|
@ -214,25 +190,9 @@ const EditPost = ({componentId, maxPostSize, post, closeButtonId, hasFilesAttach
|
|||
handleUIUpdates(res);
|
||||
}, [toggleSaveButton, serverUrl, post.id, postMessage, onClose]);
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => {
|
||||
if (Platform.OS === 'android') {
|
||||
return {bottom: 0};
|
||||
}
|
||||
|
||||
let bottom = 0;
|
||||
if (isTablet) {
|
||||
// 60 is the size of the navigation header
|
||||
const offset = isLandscape ? 60 : 0;
|
||||
|
||||
bottom = keyboardHeight.value - ((height - (layoutHeight.value + offset)) / 2);
|
||||
} else {
|
||||
bottom = keyboardHeight.value;
|
||||
}
|
||||
|
||||
return {
|
||||
bottom: withTiming(bottom, {duration: 250}),
|
||||
};
|
||||
});
|
||||
const onLayout = useCallback((e: LayoutChangeEvent) => {
|
||||
setContainerHeight(e.nativeEvent.layout.height);
|
||||
}, []);
|
||||
|
||||
useNavButtonPressed(RIGHT_BUTTON.id, componentId, onSavePostMessage, [postMessage]);
|
||||
useNavButtonPressed(closeButtonId, componentId, onClose, []);
|
||||
|
|
@ -245,6 +205,14 @@ const EditPost = ({componentId, maxPostSize, post, closeButtonId, hasFilesAttach
|
|||
);
|
||||
}
|
||||
|
||||
const bottomSpace = (dimensions.height - containerHeight - modalPosition);
|
||||
const autocompletePosition = Platform.select({
|
||||
ios: isTablet ?
|
||||
Math.max(0, keyboardHeight - bottomSpace) :
|
||||
keyboardHeight || insets.bottom,
|
||||
default: 0}) + AUTOCOMPLETE_SEPARATION;
|
||||
const autocompleteAvailableSpace = containerHeight - autocompletePosition;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SafeAreaView
|
||||
|
|
@ -252,7 +220,10 @@ const EditPost = ({componentId, maxPostSize, post, closeButtonId, hasFilesAttach
|
|||
style={styles.container}
|
||||
onLayout={onLayout}
|
||||
>
|
||||
<View style={styles.body}>
|
||||
<View
|
||||
style={styles.body}
|
||||
ref={mainView}
|
||||
>
|
||||
{Boolean((errorLine || errorExtra)) &&
|
||||
<PostError
|
||||
errorExtra={errorExtra}
|
||||
|
|
@ -269,22 +240,18 @@ const EditPost = ({componentId, maxPostSize, post, closeButtonId, hasFilesAttach
|
|||
/>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
<Animated.View style={animatedStyle}>
|
||||
<Autocomplete
|
||||
channelId={post.channelId}
|
||||
hasFilesAttached={hasFilesAttached}
|
||||
nestedScrollEnabled={true}
|
||||
rootId={post.rootId}
|
||||
updateValue={onChangeText}
|
||||
value={postMessage}
|
||||
cursorPosition={cursorPosition}
|
||||
postInputTop={1}
|
||||
fixedBottomPosition={true}
|
||||
maxHeightOverride={isTablet ? 200 : undefined}
|
||||
inPost={false}
|
||||
/>
|
||||
</Animated.View>
|
||||
|
||||
<Autocomplete
|
||||
channelId={post.channelId}
|
||||
hasFilesAttached={hasFilesAttached}
|
||||
nestedScrollEnabled={true}
|
||||
rootId={post.rootId}
|
||||
updateValue={onChangeText}
|
||||
value={postMessage}
|
||||
cursorPosition={cursorPosition}
|
||||
position={autocompletePosition}
|
||||
availableSpace={autocompleteAvailableSpace}
|
||||
inPost={false}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@
|
|||
import {useIsFocused, useNavigation} from '@react-navigation/native';
|
||||
import React, {useCallback, useMemo, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {FlatList, StyleSheet} from 'react-native';
|
||||
import {FlatList, LayoutChangeEvent, Platform, StyleSheet} from 'react-native';
|
||||
import Animated, {useAnimatedStyle, withTiming} from 'react-native-reanimated';
|
||||
import {Edge, SafeAreaView} from 'react-native-safe-area-context';
|
||||
import {Edge, SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import {addSearchToTeamSearchHistory} from '@actions/local/team';
|
||||
import {searchPosts, searchFiles} from '@actions/remote/search';
|
||||
|
|
@ -15,8 +15,10 @@ import FreezeScreen from '@components/freeze_screen';
|
|||
import Loading from '@components/loading';
|
||||
import NavigationHeader from '@components/navigation_header';
|
||||
import RoundedHeaderContext from '@components/rounded_header_context';
|
||||
import {BOTTOM_TAB_HEIGHT} from '@constants/view';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useKeyboardHeight} from '@hooks/device';
|
||||
import {useCollapsibleHeader} from '@hooks/header';
|
||||
import {FileFilter, FileFilters, filterFileExtensions} from '@utils/file';
|
||||
import {TabTypes, TabType} from '@utils/search';
|
||||
|
|
@ -34,7 +36,7 @@ const emptyChannelIds: string[] = [];
|
|||
|
||||
const dummyData = [1];
|
||||
|
||||
const AutocompletePaddingTop = -4;
|
||||
const AutocompletePaddingTop = 4;
|
||||
const AutocompleteZindex = 11;
|
||||
|
||||
type Props = {
|
||||
|
|
@ -68,6 +70,8 @@ const SearchScreen = ({teamId}: Props) => {
|
|||
const isFocused = useIsFocused();
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const insets = useSafeAreaInsets();
|
||||
const keyboardHeight = useKeyboardHeight();
|
||||
|
||||
const stateIndex = nav.getState().index;
|
||||
const serverUrl = useServerUrl();
|
||||
|
|
@ -79,6 +83,7 @@ const SearchScreen = ({teamId}: Props) => {
|
|||
const [selectedTab, setSelectedTab] = useState<TabType>(TabTypes.MESSAGES);
|
||||
const [filter, setFilter] = useState<FileFilter>(FileFilters.ALL);
|
||||
const [showResults, setShowResults] = useState(false);
|
||||
const [containerHeight, setContainerHeight] = useState(0);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [lastSearchedValue, setLastSearchedValue] = useState('');
|
||||
|
|
@ -218,7 +223,11 @@ const SearchScreen = ({teamId}: Props) => {
|
|||
top: headerHeight.value,
|
||||
zIndex: lastSearchedValue ? 10 : 0,
|
||||
};
|
||||
}, [headerHeight, lastSearchedValue]);
|
||||
}, [headerHeight.value, lastSearchedValue]);
|
||||
|
||||
const onLayout = useCallback((e: LayoutChangeEvent) => {
|
||||
setContainerHeight(e.nativeEvent.layout.height);
|
||||
}, []);
|
||||
|
||||
let header = null;
|
||||
if (lastSearchedValue && !loading) {
|
||||
|
|
@ -235,17 +244,25 @@ const SearchScreen = ({teamId}: Props) => {
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const autocompleteRemoveFromHeight = headerHeight.value + Platform.select({
|
||||
ios: keyboardHeight ? keyboardHeight - BOTTOM_TAB_HEIGHT : insets.bottom,
|
||||
default: 0,
|
||||
});
|
||||
const autocompleteMaxHeight = containerHeight - autocompleteRemoveFromHeight;
|
||||
const autocompletePosition = AutocompletePaddingTop;
|
||||
const autocomplete = useMemo(() => (
|
||||
<Autocomplete
|
||||
paddingTop={AutocompletePaddingTop}
|
||||
postInputTop={0}
|
||||
updateValue={handleTextChange}
|
||||
cursorPosition={cursorPosition}
|
||||
value={searchValue}
|
||||
isSearch={true}
|
||||
hasFilesAttached={false}
|
||||
availableSpace={autocompleteMaxHeight}
|
||||
position={autocompletePosition}
|
||||
growDown={true}
|
||||
/>
|
||||
), [cursorPosition, handleTextChange, searchValue]);
|
||||
), [cursorPosition, handleTextChange, searchValue, autocompleteMaxHeight, autocompletePosition]);
|
||||
|
||||
return (
|
||||
<FreezeScreen freeze={!isFocused}>
|
||||
|
|
@ -270,6 +287,7 @@ const SearchScreen = ({teamId}: Props) => {
|
|||
<SafeAreaView
|
||||
style={styles.flex}
|
||||
edges={EDGES}
|
||||
onLayout={onLayout}
|
||||
>
|
||||
<Animated.View style={animated}>
|
||||
<Animated.View style={top}>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect, useRef} from 'react';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react';
|
||||
import {DeviceEventEmitter, LayoutChangeEvent, StyleSheet, View} from 'react-native';
|
||||
import {KeyboardTrackingViewRef} from 'react-native-keyboard-tracking-view';
|
||||
import {Edge, SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import FreezeScreen from '@components/freeze_screen';
|
||||
import PostDraft from '@components/post_draft';
|
||||
import RoundedHeaderContext from '@components/rounded_header_context';
|
||||
import {Events} from '@constants';
|
||||
import {THREAD_ACCESSORIES_CONTAINER_NATIVE_ID} from '@constants/post_draft';
|
||||
import {useAppState} from '@hooks/device';
|
||||
import useDidUpdate from '@hooks/did_update';
|
||||
|
|
@ -33,6 +34,20 @@ const styles = StyleSheet.create({
|
|||
const Thread = ({componentId, rootPost}: ThreadProps) => {
|
||||
const appState = useAppState();
|
||||
const postDraftRef = useRef<KeyboardTrackingViewRef>(null);
|
||||
const [containerHeight, setContainerHeight] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = DeviceEventEmitter.addListener(Events.PAUSE_KEYBOARD_TRACKING_VIEW, (pause: boolean) => {
|
||||
if (pause) {
|
||||
postDraftRef.current?.pauseTracking(rootPost!.id);
|
||||
return;
|
||||
}
|
||||
|
||||
postDraftRef.current?.resumeTracking(rootPost!.id);
|
||||
});
|
||||
|
||||
return () => listener.remove();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
|
@ -46,6 +61,10 @@ const Thread = ({componentId, rootPost}: ThreadProps) => {
|
|||
}
|
||||
}, [componentId, rootPost]);
|
||||
|
||||
const onLayout = useCallback((e: LayoutChangeEvent) => {
|
||||
setContainerHeight(e.nativeEvent.layout.height);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<FreezeScreen>
|
||||
<SafeAreaView
|
||||
|
|
@ -53,6 +72,7 @@ const Thread = ({componentId, rootPost}: ThreadProps) => {
|
|||
mode='margin'
|
||||
edges={edges}
|
||||
testID='thread.screen'
|
||||
onLayout={onLayout}
|
||||
>
|
||||
<RoundedHeaderContext/>
|
||||
{Boolean(rootPost?.id) &&
|
||||
|
|
@ -71,6 +91,8 @@ const Thread = ({componentId, rootPost}: ThreadProps) => {
|
|||
rootId={rootPost!.id}
|
||||
keyboardTracker={postDraftRef}
|
||||
testID='thread.post_draft'
|
||||
containerHeight={containerHeight}
|
||||
isChannelScreen={false}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,13 @@ declare module 'react-native-keyboard-tracking-view' {
|
|||
resumeTracking: (id: string) => void;
|
||||
resetScrollView: (id: string) => void;
|
||||
setNativeProps(nativeProps: object): void;
|
||||
getNativeProps: () => Promise<KeyboardTrackingViewNativeProps>;
|
||||
}
|
||||
|
||||
type KeyboardTrackingViewNativeProps = {
|
||||
contentTopInset: number;
|
||||
keyboardHeight: number;
|
||||
trackingViewHeight: number;
|
||||
}
|
||||
|
||||
interface KeyboardTrackingViewProps extends ViewProps{
|
||||
|
|
|
|||
Loading…
Reference in a new issue