forked from Ivasoft/mattermost-mobile
[Gekidou MM-46229] Add hideAndLock, showAndUnlock callbacks for Search (#6677)
Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
@@ -4,9 +4,11 @@
|
||||
import React, {useMemo} from 'react';
|
||||
import {Platform, Text, View} from 'react-native';
|
||||
import Animated, {useAnimatedStyle, withTiming} from 'react-native-reanimated';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import ViewConstants from '@constants/view';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
@@ -24,18 +26,18 @@ type Props = {
|
||||
defaultHeight: number;
|
||||
hasSearch: boolean;
|
||||
isLargeTitle: boolean;
|
||||
largeHeight: number;
|
||||
heightOffset: number;
|
||||
leftComponent?: React.ReactElement;
|
||||
onBackPress?: () => void;
|
||||
onTitlePress?: () => void;
|
||||
rightButtons?: HeaderRightButton[];
|
||||
scrollValue?: Animated.SharedValue<number>;
|
||||
lockValue?: Animated.SharedValue<number | null>;
|
||||
showBackButton?: boolean;
|
||||
subtitle?: string;
|
||||
subtitleCompanion?: React.ReactElement;
|
||||
theme: Theme;
|
||||
title?: string;
|
||||
top: number;
|
||||
}
|
||||
|
||||
const hitSlop = {top: 20, bottom: 20, left: 20, right: 20};
|
||||
@@ -127,20 +129,21 @@ const Header = ({
|
||||
defaultHeight,
|
||||
hasSearch,
|
||||
isLargeTitle,
|
||||
largeHeight,
|
||||
heightOffset,
|
||||
leftComponent,
|
||||
onBackPress,
|
||||
onTitlePress,
|
||||
rightButtons,
|
||||
scrollValue,
|
||||
lockValue,
|
||||
showBackButton = true,
|
||||
subtitle,
|
||||
subtitleCompanion,
|
||||
theme,
|
||||
title,
|
||||
top,
|
||||
}: Props) => {
|
||||
const styles = getStyleSheet(theme);
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const opacity = useAnimatedStyle(() => {
|
||||
if (!isLargeTitle) {
|
||||
@@ -151,8 +154,7 @@ const Header = ({
|
||||
return {opacity: 0};
|
||||
}
|
||||
|
||||
const largeTitleLabelHeight = 60;
|
||||
const barHeight = (largeHeight - defaultHeight) - largeTitleLabelHeight;
|
||||
const barHeight = heightOffset - ViewConstants.LARGE_HEADER_TITLE_HEIGHT;
|
||||
const val = (scrollValue?.value ?? 0);
|
||||
const showDuration = 200;
|
||||
const hideDuration = 50;
|
||||
@@ -161,11 +163,15 @@ const Header = ({
|
||||
return {
|
||||
opacity: withTiming(opacityValue, {duration}),
|
||||
};
|
||||
}, [defaultHeight, largeHeight, isLargeTitle, hasSearch]);
|
||||
}, [heightOffset, isLargeTitle, hasSearch]);
|
||||
|
||||
const containerStyle = useMemo(() => {
|
||||
return [styles.container, {height: defaultHeight + top, paddingTop: top}];
|
||||
}, [defaultHeight, theme]);
|
||||
const containerAnimatedStyle = useAnimatedStyle(() => ({
|
||||
height: defaultHeight,
|
||||
paddingTop: insets.top,
|
||||
}), [defaultHeight, lockValue]);
|
||||
|
||||
const containerStyle = useMemo(() => (
|
||||
[styles.container, containerAnimatedStyle]), [styles, containerAnimatedStyle]);
|
||||
|
||||
const additionalTitleStyle = useMemo(() => ({
|
||||
marginLeft: Platform.select({android: showBackButton && !leftComponent ? 20 : 0}),
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import Animated, {useAnimatedStyle} from 'react-native-reanimated';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
import Animated, {useAnimatedStyle, useDerivedValue} from 'react-native-reanimated';
|
||||
|
||||
import {SEARCH_INPUT_HEIGHT, SEARCH_INPUT_MARGIN} from '@constants/view';
|
||||
import {useTheme} from '@context/theme';
|
||||
import useHeaderHeight, {MAX_OVERSCROLL} from '@hooks/header';
|
||||
import {clamp} from '@utils/gallery';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import Header, {HeaderRightButton} from './header';
|
||||
@@ -23,6 +24,7 @@ type Props = SearchProps & {
|
||||
onTitlePress?: () => void;
|
||||
rightButtons?: HeaderRightButton[];
|
||||
scrollValue?: Animated.SharedValue<number>;
|
||||
lockValue?: Animated.SharedValue<number | null>;
|
||||
hideHeader?: () => void;
|
||||
showBackButton?: boolean;
|
||||
subtitle?: string;
|
||||
@@ -47,6 +49,7 @@ const NavigationHeader = ({
|
||||
onTitlePress,
|
||||
rightButtons,
|
||||
scrollValue,
|
||||
lockValue,
|
||||
showBackButton,
|
||||
subtitle,
|
||||
subtitleCompanion,
|
||||
@@ -55,21 +58,37 @@ const NavigationHeader = ({
|
||||
...searchProps
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
const insets = useSafeAreaInsets();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const {largeHeight, defaultHeight} = useHeaderHeight();
|
||||
const {largeHeight, defaultHeight, headerOffset} = useHeaderHeight();
|
||||
const containerHeight = useAnimatedStyle(() => {
|
||||
const minHeight = defaultHeight + insets.top;
|
||||
const minHeight = defaultHeight;
|
||||
const value = -(scrollValue?.value || 0);
|
||||
const height = ((isLargeTitle ? largeHeight : defaultHeight)) + value + insets.top;
|
||||
const calculatedHeight = (isLargeTitle ? largeHeight : defaultHeight) + value;
|
||||
const height = lockValue?.value ? lockValue.value : calculatedHeight;
|
||||
return {
|
||||
height: Math.max(height, minHeight),
|
||||
minHeight,
|
||||
maxHeight: largeHeight + insets.top + MAX_OVERSCROLL,
|
||||
maxHeight: largeHeight + MAX_OVERSCROLL,
|
||||
};
|
||||
});
|
||||
|
||||
const minScrollValue = useDerivedValue(() => scrollValue?.value || 0, [scrollValue]);
|
||||
|
||||
const translateY = useDerivedValue(() => (
|
||||
lockValue?.value ? -lockValue.value : Math.min(-minScrollValue.value, headerOffset)
|
||||
), [lockValue, minScrollValue, headerOffset]);
|
||||
|
||||
const searchTopStyle = useAnimatedStyle(() => {
|
||||
const margin = clamp(-minScrollValue.value, -headerOffset, headerOffset);
|
||||
const marginTop = (lockValue?.value ? -lockValue?.value : margin) - SEARCH_INPUT_HEIGHT - SEARCH_INPUT_MARGIN;
|
||||
return {marginTop};
|
||||
}, [lockValue, headerOffset, scrollValue]);
|
||||
|
||||
const heightOffset = useDerivedValue(() => (
|
||||
lockValue?.value ? lockValue.value : headerOffset
|
||||
), [lockValue, headerOffset]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Animated.View style={[styles.container, containerHeight]}>
|
||||
@@ -77,40 +96,36 @@ const NavigationHeader = ({
|
||||
defaultHeight={defaultHeight}
|
||||
hasSearch={hasSearch}
|
||||
isLargeTitle={isLargeTitle}
|
||||
largeHeight={largeHeight}
|
||||
heightOffset={heightOffset.value}
|
||||
leftComponent={leftComponent}
|
||||
onBackPress={onBackPress}
|
||||
onTitlePress={onTitlePress}
|
||||
rightButtons={rightButtons}
|
||||
lockValue={lockValue}
|
||||
scrollValue={scrollValue}
|
||||
showBackButton={showBackButton}
|
||||
subtitle={subtitle}
|
||||
subtitleCompanion={subtitleCompanion}
|
||||
theme={theme}
|
||||
title={title}
|
||||
top={insets.top}
|
||||
/>
|
||||
{isLargeTitle &&
|
||||
<NavigationHeaderLargeTitle
|
||||
defaultHeight={defaultHeight}
|
||||
heightOffset={heightOffset.value}
|
||||
hasSearch={hasSearch}
|
||||
largeHeight={largeHeight}
|
||||
scrollValue={scrollValue}
|
||||
subtitle={subtitle}
|
||||
theme={theme}
|
||||
title={title}
|
||||
translateY={translateY}
|
||||
/>
|
||||
}
|
||||
{hasSearch &&
|
||||
<NavigationSearch
|
||||
{...searchProps}
|
||||
defaultHeight={defaultHeight}
|
||||
largeHeight={largeHeight}
|
||||
scrollValue={scrollValue}
|
||||
hideHeader={hideHeader}
|
||||
theme={theme}
|
||||
top={0}
|
||||
/>
|
||||
<NavigationSearch
|
||||
{...searchProps}
|
||||
hideHeader={hideHeader}
|
||||
theme={theme}
|
||||
topStyle={searchTopStyle}
|
||||
/>
|
||||
}
|
||||
</Animated.View>
|
||||
</>
|
||||
|
||||
@@ -9,13 +9,12 @@ import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
type Props = {
|
||||
defaultHeight: number;
|
||||
heightOffset: number;
|
||||
hasSearch: boolean;
|
||||
largeHeight: number;
|
||||
scrollValue?: Animated.SharedValue<number>;
|
||||
subtitle?: string;
|
||||
theme: Theme;
|
||||
title: string;
|
||||
translateY: Animated.DerivedValue<number>;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
@@ -34,33 +33,29 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
}));
|
||||
|
||||
const NavigationHeaderLargeTitle = ({
|
||||
defaultHeight,
|
||||
largeHeight,
|
||||
heightOffset,
|
||||
hasSearch,
|
||||
scrollValue,
|
||||
subtitle,
|
||||
theme,
|
||||
title,
|
||||
translateY,
|
||||
}: Props) => {
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const transform = useAnimatedStyle(() => {
|
||||
const value = scrollValue?.value || 0;
|
||||
return {
|
||||
transform: [{translateY: Math.min(-value, largeHeight - defaultHeight)}],
|
||||
};
|
||||
});
|
||||
const transform = useAnimatedStyle(() => (
|
||||
{transform: [{translateY: translateY.value}]}
|
||||
), [translateY?.value]);
|
||||
|
||||
const containerStyle = useMemo(() => {
|
||||
return [{height: largeHeight - defaultHeight}, styles.container];
|
||||
}, [defaultHeight, largeHeight, theme]);
|
||||
return [{height: heightOffset}, styles.container];
|
||||
}, [heightOffset, theme]);
|
||||
|
||||
return (
|
||||
<Animated.View style={[containerStyle, transform]}>
|
||||
<Text
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
style={styles.heading}
|
||||
style={[styles.heading]}
|
||||
testID='navigation.large_header.title'
|
||||
>
|
||||
{title}
|
||||
|
||||
@@ -2,35 +2,26 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useMemo} from 'react';
|
||||
import {DeviceEventEmitter, Keyboard, NativeSyntheticEvent, Platform, TextInputFocusEventData} from 'react-native';
|
||||
import Animated, {useAnimatedStyle} from 'react-native-reanimated';
|
||||
import {DeviceEventEmitter, Keyboard, NativeSyntheticEvent, Platform, TextInputFocusEventData, ViewStyle} from 'react-native';
|
||||
import Animated, {AnimatedStyleProp} from 'react-native-reanimated';
|
||||
|
||||
import Search, {SearchProps} from '@components/search';
|
||||
import {Events} from '@constants';
|
||||
import {HEADER_SEARCH_HEIGHT} from '@constants/view';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
type Props = SearchProps & {
|
||||
defaultHeight: number;
|
||||
largeHeight: number;
|
||||
scrollValue?: Animated.SharedValue<number>;
|
||||
topStyle: AnimatedStyleProp<ViewStyle>;
|
||||
hideHeader?: () => void;
|
||||
theme: Theme;
|
||||
top: number;
|
||||
}
|
||||
|
||||
const INITIAL_TOP = -45;
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
backgroundColor: theme.sidebarBg,
|
||||
height: HEADER_SEARCH_HEIGHT,
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 20,
|
||||
width: '100%',
|
||||
zIndex: 10,
|
||||
top: INITIAL_TOP,
|
||||
},
|
||||
inputContainerStyle: {
|
||||
backgroundColor: changeOpacity(theme.sidebarText, 0.12),
|
||||
@@ -41,11 +32,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
}));
|
||||
|
||||
const NavigationSearch = ({
|
||||
defaultHeight,
|
||||
largeHeight,
|
||||
scrollValue,
|
||||
hideHeader,
|
||||
theme,
|
||||
topStyle,
|
||||
...searchProps
|
||||
}: Props) => {
|
||||
const styles = getStyleSheet(theme);
|
||||
@@ -58,12 +47,6 @@ const NavigationSearch = ({
|
||||
color: theme.sidebarText,
|
||||
}), [theme]);
|
||||
|
||||
const searchTop = useAnimatedStyle(() => {
|
||||
const value = scrollValue?.value || 0;
|
||||
const min = (largeHeight - defaultHeight);
|
||||
return {marginTop: Math.min(-Math.min((value), min), min)};
|
||||
}, [largeHeight, defaultHeight]);
|
||||
|
||||
const onFocus = useCallback((e: NativeSyntheticEvent<TextInputFocusEventData>) => {
|
||||
hideHeader?.();
|
||||
searchProps.onFocus?.(e);
|
||||
@@ -89,7 +72,7 @@ const NavigationSearch = ({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Animated.View style={[styles.container, searchTop]}>
|
||||
<Animated.View style={[styles.container, topStyle]}>
|
||||
<Search
|
||||
{...searchProps}
|
||||
cancelButtonProps={cancelButtonProps}
|
||||
|
||||
@@ -75,7 +75,7 @@ function PostDraft({
|
||||
ios: (keyboardHeight ? keyboardHeight - keyboardAdjustment : (postInputTop + insetsAdjustment)),
|
||||
default: postInputTop + insetsAdjustment,
|
||||
});
|
||||
const autocompleteAvailableSpace = containerHeight - autocompletePosition - (isChannelScreen ? headerHeight + insets.top : 0);
|
||||
const autocompleteAvailableSpace = containerHeight - autocompletePosition - (isChannelScreen ? headerHeight : 0);
|
||||
|
||||
const [animatedAutocompletePosition, animatedAutocompleteAvailableSpace] = useAutocompleteDefaultAnimatedValues(autocompletePosition, autocompleteAvailableSpace);
|
||||
|
||||
|
||||
@@ -7,21 +7,19 @@ export const BOTTOM_TAB_HEIGHT = 52;
|
||||
export const BOTTOM_TAB_ICON_SIZE = 31.2;
|
||||
export const PROFILE_PICTURE_SIZE = 32;
|
||||
export const PROFILE_PICTURE_EMOJI_SIZE = 28;
|
||||
export const SEARCH_INPUT_HEIGHT = Platform.select({android: 40, default: 36});
|
||||
|
||||
export const TEAM_SIDEBAR_WIDTH = 72;
|
||||
export const TABLET_HEADER_HEIGHT = 44;
|
||||
export const TABLET_SIDEBAR_WIDTH = 320;
|
||||
|
||||
export const IOS_STATUS_BAR_HEIGHT = 20;
|
||||
export const IOS_DEFAULT_HEADER_HEIGHT = 44;
|
||||
export const ANDROID_DEFAULT_HEADER_HEIGHT = 56;
|
||||
export const LARGE_HEADER_TITLE = 60;
|
||||
export const HEADER_WITH_SEARCH_HEIGHT = -16;
|
||||
export const HEADER_WITH_SUBTITLE = 24;
|
||||
export const DEFAULT_HEADER_HEIGHT = Platform.select({android: 56, default: 44});
|
||||
export const LARGE_HEADER_TITLE_HEIGHT = 60;
|
||||
export const SUBTITLE_HEIGHT = 24;
|
||||
export const KEYBOARD_TRACKING_OFFSET = 72;
|
||||
export const HEADER_SEARCH_HEIGHT = SEARCH_INPUT_HEIGHT + 5;
|
||||
export const HEADER_SEARCH_BOTTOM_MARGIN = 10;
|
||||
|
||||
export const SEARCH_INPUT_HEIGHT = Platform.select({android: 40, default: 36});
|
||||
export const SEARCH_INPUT_MARGIN = 5;
|
||||
|
||||
export const JOIN_CALL_BAR_HEIGHT = 38;
|
||||
export const CURRENT_CALL_BAR_HEIGHT = 74;
|
||||
@@ -41,14 +39,10 @@ export default {
|
||||
TEAM_SIDEBAR_WIDTH,
|
||||
TABLET_HEADER_HEIGHT,
|
||||
IOS_STATUS_BAR_HEIGHT,
|
||||
IOS_DEFAULT_HEADER_HEIGHT,
|
||||
ANDROID_DEFAULT_HEADER_HEIGHT,
|
||||
LARGE_HEADER_TITLE,
|
||||
HEADER_WITH_SEARCH_HEIGHT,
|
||||
HEADER_WITH_SUBTITLE,
|
||||
DEFAULT_HEADER_HEIGHT,
|
||||
LARGE_HEADER_TITLE_HEIGHT,
|
||||
SUBTITLE_HEIGHT,
|
||||
KEYBOARD_TRACKING_OFFSET,
|
||||
HEADER_SEARCH_HEIGHT,
|
||||
HEADER_SEARCH_BOTTOM_MARGIN,
|
||||
QUICK_OPTIONS_HEIGHT,
|
||||
};
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import {NativeScrollEvent, Platform} from 'react-native';
|
||||
import Animated, {runOnJS, scrollTo, useAnimatedRef, useAnimatedScrollHandler, useDerivedValue, useSharedValue} from 'react-native-reanimated';
|
||||
import {NativeScrollEvent} from 'react-native';
|
||||
import Animated, {runOnJS, scrollTo, useAnimatedRef, useAnimatedScrollHandler, useDerivedValue, useSharedValue, withTiming} from 'react-native-reanimated';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import ViewConstants from '@constants/view';
|
||||
@@ -18,52 +18,52 @@ type HeaderScrollContext = {
|
||||
export const MAX_OVERSCROLL = 80;
|
||||
|
||||
export const useDefaultHeaderHeight = () => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const isTablet = useIsTablet();
|
||||
|
||||
let headerHeight = ViewConstants.DEFAULT_HEADER_HEIGHT;
|
||||
if (isTablet) {
|
||||
return ViewConstants.TABLET_HEADER_HEIGHT;
|
||||
headerHeight = ViewConstants.TABLET_HEADER_HEIGHT;
|
||||
}
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
return ViewConstants.IOS_DEFAULT_HEADER_HEIGHT;
|
||||
}
|
||||
|
||||
return ViewConstants.ANDROID_DEFAULT_HEADER_HEIGHT;
|
||||
return headerHeight + insets.top;
|
||||
};
|
||||
|
||||
export const useLargeHeaderHeight = () => {
|
||||
const defaultHeight = useDefaultHeaderHeight();
|
||||
return defaultHeight + ViewConstants.LARGE_HEADER_TITLE + ViewConstants.HEADER_WITH_SUBTITLE;
|
||||
let largeHeight = useDefaultHeaderHeight();
|
||||
largeHeight += ViewConstants.LARGE_HEADER_TITLE_HEIGHT;
|
||||
largeHeight += ViewConstants.SUBTITLE_HEIGHT;
|
||||
return largeHeight;
|
||||
};
|
||||
|
||||
export const useHeaderHeight = () => {
|
||||
const defaultHeight = useDefaultHeaderHeight();
|
||||
const largeHeight = useLargeHeaderHeight();
|
||||
return useMemo(() => {
|
||||
return {
|
||||
defaultHeight,
|
||||
largeHeight,
|
||||
};
|
||||
}, [defaultHeight, largeHeight]);
|
||||
const headerOffset = largeHeight - defaultHeight;
|
||||
return useMemo(() => ({
|
||||
defaultHeight,
|
||||
largeHeight,
|
||||
headerOffset,
|
||||
}), [defaultHeight, largeHeight]);
|
||||
};
|
||||
|
||||
export const useCollapsibleHeader = <T>(isLargeTitle: boolean, onSnap?: (offset: number) => void) => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const animatedRef = useAnimatedRef<Animated.ScrollView>();
|
||||
const {largeHeight, defaultHeight} = useHeaderHeight();
|
||||
const {largeHeight, defaultHeight, headerOffset} = useHeaderHeight();
|
||||
const scrollValue = useSharedValue(0);
|
||||
const lockValue = useSharedValue<number | null>(null);
|
||||
const autoScroll = useSharedValue(false);
|
||||
const snapping = useSharedValue(false);
|
||||
const scrollEnabled = useSharedValue(true);
|
||||
|
||||
const headerHeight = useDerivedValue(() => {
|
||||
const minHeight = defaultHeight + insets.top;
|
||||
const value = -(scrollValue?.value || 0);
|
||||
const header = (isLargeTitle ? largeHeight : defaultHeight);
|
||||
const height = header + value + insets.top;
|
||||
if (height > header + (insets.top * 2)) {
|
||||
return Math.min(height, largeHeight + insets.top + MAX_OVERSCROLL);
|
||||
const heightWithScroll = (isLargeTitle ? largeHeight : defaultHeight) + value;
|
||||
let height = Math.max(heightWithScroll, defaultHeight);
|
||||
if (value > insets.top) {
|
||||
height = Math.min(heightWithScroll, largeHeight + MAX_OVERSCROLL);
|
||||
}
|
||||
return Math.max(height, minHeight);
|
||||
return height;
|
||||
});
|
||||
|
||||
function snapIfNeeded(dir: string, offset: number) {
|
||||
@@ -72,27 +72,35 @@ export const useCollapsibleHeader = <T>(isLargeTitle: boolean, onSnap?: (offset:
|
||||
snapping.value = true;
|
||||
if (dir === 'down' && offset < largeHeight) {
|
||||
runOnJS(onSnap)(0);
|
||||
} else if (dir === 'up' && offset < (defaultHeight + insets.top)) {
|
||||
runOnJS(onSnap)((largeHeight - defaultHeight));
|
||||
} else if (dir === 'up' && offset < (defaultHeight)) {
|
||||
runOnJS(onSnap)(headerOffset);
|
||||
}
|
||||
snapping.value = false;
|
||||
snapping.value = Boolean(withTiming(0, {duration: 100}));
|
||||
}
|
||||
}
|
||||
|
||||
const setAutoScroll = (enabled: boolean) => {
|
||||
autoScroll.value = enabled;
|
||||
};
|
||||
|
||||
const onScroll = useAnimatedScrollHandler({
|
||||
onBeginDrag: (e: NativeScrollEvent, ctx: HeaderScrollContext) => {
|
||||
ctx.start = e.contentOffset.y;
|
||||
ctx.dragging = true;
|
||||
},
|
||||
onScroll: (e, ctx) => {
|
||||
if (ctx.dragging || autoScroll.value) {
|
||||
if (!scrollEnabled.value) {
|
||||
scrollTo(animatedRef, 0, headerOffset, false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctx.dragging || autoScroll.value || snapping.value) {
|
||||
scrollValue.value = e.contentOffset.y;
|
||||
} else {
|
||||
// here we want to ensure that the scroll position
|
||||
// always start at 0 if the user has not dragged
|
||||
// the scrollview manually
|
||||
scrollValue.value = 0;
|
||||
scrollTo(animatedRef, 0, 0, false);
|
||||
scrollTo(animatedRef, 0, scrollValue.value, false);
|
||||
}
|
||||
},
|
||||
onEndDrag: (e, ctx) => {
|
||||
@@ -119,8 +127,12 @@ export const useCollapsibleHeader = <T>(isLargeTitle: boolean, onSnap?: (offset:
|
||||
},
|
||||
}, [insets, defaultHeight, largeHeight, animatedRef]);
|
||||
|
||||
const hideHeader = useCallback(() => {
|
||||
const offset = largeHeight - defaultHeight;
|
||||
const hideHeader = useCallback((lock = false) => {
|
||||
if (lock) {
|
||||
lockValue.value = defaultHeight;
|
||||
}
|
||||
|
||||
const offset = headerOffset;
|
||||
if (animatedRef?.current && Math.abs((scrollValue?.value || 0)) <= insets.top) {
|
||||
autoScroll.value = true;
|
||||
if ('scrollTo' in animatedRef.current) {
|
||||
@@ -136,15 +148,24 @@ export const useCollapsibleHeader = <T>(isLargeTitle: boolean, onSnap?: (offset:
|
||||
}
|
||||
}, [largeHeight, defaultHeight]);
|
||||
|
||||
const unlock = useCallback(() => {
|
||||
lockValue.value = null;
|
||||
}, []);
|
||||
|
||||
return {
|
||||
defaultHeight,
|
||||
largeHeight,
|
||||
scrollPaddingTop: (isLargeTitle ? largeHeight : defaultHeight) + insets.top,
|
||||
scrollPaddingTop: (isLargeTitle ? largeHeight : defaultHeight),
|
||||
scrollRef: animatedRef as unknown as React.RefObject<T>,
|
||||
scrollValue,
|
||||
onScroll,
|
||||
hideHeader,
|
||||
lockValue,
|
||||
unlock,
|
||||
headerHeight,
|
||||
headerOffset,
|
||||
scrollEnabled,
|
||||
setAutoScroll,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -5,12 +5,9 @@ import React from 'react';
|
||||
import {View, Platform, StyleSheet} from 'react-native';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import {ANDROID_DEFAULT_HEADER_HEIGHT, IOS_DEFAULT_HEADER_HEIGHT} from '@constants/view';
|
||||
import {DEFAULT_HEADER_HEIGHT} from '@constants/view';
|
||||
|
||||
let topBarHeight = ANDROID_DEFAULT_HEADER_HEIGHT;
|
||||
if (Platform.OS === 'ios') {
|
||||
topBarHeight = IOS_DEFAULT_HEADER_HEIGHT;
|
||||
}
|
||||
const topBarHeight = DEFAULT_HEADER_HEIGHT;
|
||||
|
||||
const style = StyleSheet.create({
|
||||
wrapper: {
|
||||
|
||||
@@ -92,7 +92,7 @@ const Channel = ({
|
||||
return () => back?.remove();
|
||||
}, [componentId, isTablet]);
|
||||
|
||||
const marginTop = defaultHeight + (isTablet ? insets.top : 0);
|
||||
const marginTop = defaultHeight + (isTablet ? 0 : -insets.top);
|
||||
useEffect(() => {
|
||||
// This is done so that the header renders
|
||||
// and the screen does not look totally blank
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Keyboard, Platform, Text, View} from 'react-native';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import CustomStatusEmoji from '@components/custom_status/custom_status_emoji';
|
||||
@@ -75,13 +74,12 @@ const ChannelHeader = ({
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const defaultHeight = useDefaultHeaderHeight();
|
||||
const insets = useSafeAreaInsets();
|
||||
const callsAvailable = callsEnabledInChannel && !callsFeatureRestricted;
|
||||
|
||||
const isDMorGM = isTypeDMorGM(channelType);
|
||||
const contextStyle = useMemo(() => ({
|
||||
top: defaultHeight + insets.top,
|
||||
}), [defaultHeight, insets.top]);
|
||||
top: defaultHeight,
|
||||
}), [defaultHeight]);
|
||||
|
||||
const leftComponent = useMemo(() => {
|
||||
if (isTablet || !channelId || !teamId) {
|
||||
|
||||
@@ -43,8 +43,9 @@ const edges: Edge[] = ['left', 'right'];
|
||||
const AnimatedSafeAreaView = Animated.createAnimatedComponent(SafeAreaView);
|
||||
|
||||
const Header = ({index, onClose, style, total}: Props) => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const {width} = useWindowDimensions();
|
||||
const height = useDefaultHeaderHeight();
|
||||
const height = useDefaultHeaderHeight() - insets.top;
|
||||
const {top} = useSafeAreaInsets();
|
||||
const topContainerStyle = useMemo(() => [{height: top, backgroundColor: '#000'}], [top]);
|
||||
const containerStyle = useMemo(() => [styles.container, {height}], [height]);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import React, {useCallback, useMemo, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Keyboard, StyleSheet, View} from 'react-native';
|
||||
import {Edge, SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
import {Edge, SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import NavigationHeader from '@components/navigation_header';
|
||||
import RoundedHeaderContext from '@components/rounded_header_context';
|
||||
@@ -30,7 +30,6 @@ const styles = StyleSheet.create({
|
||||
const GlobalThreads = ({componentId}: Props) => {
|
||||
const appState = useAppState();
|
||||
const intl = useIntl();
|
||||
const insets = useSafeAreaInsets();
|
||||
const switchingTeam = useTeamSwitch();
|
||||
const isTablet = useIsTablet();
|
||||
|
||||
@@ -39,13 +38,13 @@ const GlobalThreads = ({componentId}: Props) => {
|
||||
const [tab, setTab] = useState<GlobalThreadsTab>('all');
|
||||
|
||||
const containerStyle = useMemo(() => {
|
||||
const marginTop = defaultHeight + insets.top;
|
||||
const marginTop = defaultHeight;
|
||||
return {flex: 1, marginTop};
|
||||
}, [defaultHeight, insets.top]);
|
||||
}, [defaultHeight]);
|
||||
|
||||
const contextStyle = useMemo(() => ({
|
||||
top: defaultHeight + insets.top,
|
||||
}), [defaultHeight, insets.top]);
|
||||
top: defaultHeight,
|
||||
}), [defaultHeight]);
|
||||
|
||||
const onBackPress = useCallback(() => {
|
||||
Keyboard.dismiss();
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
import {useIsFocused, useRoute} from '@react-navigation/native';
|
||||
import React, {useCallback, useState, useEffect, useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {ActivityIndicator, DeviceEventEmitter, FlatList, ListRenderItemInfo, Platform, StyleSheet, View} from 'react-native';
|
||||
import {ActivityIndicator, DeviceEventEmitter, FlatList, ListRenderItemInfo, StyleSheet, View} from 'react-native';
|
||||
import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
|
||||
import {SafeAreaView, Edge, useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
import {SafeAreaView, Edge} from 'react-native-safe-area-context';
|
||||
|
||||
import {fetchRecentMentions} from '@actions/remote/search';
|
||||
import NavigationHeader from '@components/navigation_header';
|
||||
@@ -14,7 +14,6 @@ import DateSeparator from '@components/post_list/date_separator';
|
||||
import PostWithChannelInfo from '@components/post_with_channel_info';
|
||||
import RoundedHeaderContext from '@components/rounded_header_context';
|
||||
import {Events, Screens} from '@constants';
|
||||
import {BOTTOM_TAB_HEIGHT} from '@constants/view';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useCollapsibleHeader} from '@hooks/header';
|
||||
@@ -40,7 +39,6 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
marginBottom: Platform.select({ios: BOTTOM_TAB_HEIGHT}),
|
||||
},
|
||||
empty: {
|
||||
alignItems: 'center',
|
||||
@@ -53,7 +51,6 @@ const RecentMentionsScreen = ({mentions, currentTimezone, isTimezoneEnabled}: Pr
|
||||
const theme = useTheme();
|
||||
const route = useRoute();
|
||||
const isFocused = useIsFocused();
|
||||
const insets = useSafeAreaInsets();
|
||||
const {formatMessage} = useIntl();
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -87,8 +84,7 @@ const RecentMentionsScreen = ({mentions, currentTimezone, isTimezoneEnabled}: Pr
|
||||
}, [serverUrl, isFocused]);
|
||||
|
||||
const {scrollPaddingTop, scrollRef, scrollValue, onScroll, headerHeight} = useCollapsibleHeader<FlatList<string>>(true, onSnap);
|
||||
const paddingTop = useMemo(() => ({paddingTop: scrollPaddingTop - insets.top, flexGrow: 1}), [scrollPaddingTop, insets.top]);
|
||||
const scrollViewStyle = useMemo(() => ({top: insets.top}), [insets.top]);
|
||||
const paddingTop = useMemo(() => ({paddingTop: scrollPaddingTop, flexGrow: 1}), [scrollPaddingTop]);
|
||||
const posts = useMemo(() => selectOrderedPosts(mentions, 0, false, '', '', false, isTimezoneEnabled, currentTimezone, false).reverse(), [mentions]);
|
||||
|
||||
const animated = useAnimatedStyle(() => {
|
||||
@@ -195,7 +191,6 @@ const RecentMentionsScreen = ({mentions, currentTimezone, isTimezoneEnabled}: Pr
|
||||
renderItem={renderItem}
|
||||
removeClippedSubviews={true}
|
||||
onViewableItemsChanged={onViewableItemsChanged}
|
||||
style={scrollViewStyle}
|
||||
testID='recent_mentions.post_list.flat_list'
|
||||
/>
|
||||
</Animated.View>
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
import {useIsFocused, useRoute} from '@react-navigation/native';
|
||||
import React, {useCallback, useEffect, useMemo, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {DeviceEventEmitter, FlatList, ListRenderItemInfo, Platform, StyleSheet, View} from 'react-native';
|
||||
import {DeviceEventEmitter, FlatList, ListRenderItemInfo, StyleSheet, View} from 'react-native';
|
||||
import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
|
||||
import {Edge, SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
import {Edge, SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import {fetchSavedPosts} from '@actions/remote/post';
|
||||
import Loading from '@components/loading';
|
||||
@@ -15,7 +15,6 @@ import DateSeparator from '@components/post_list/date_separator';
|
||||
import PostWithChannelInfo from '@components/post_with_channel_info';
|
||||
import RoundedHeaderContext from '@components/rounded_header_context';
|
||||
import {Events, Screens} from '@constants';
|
||||
import {BOTTOM_TAB_HEIGHT} from '@constants/view';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useCollapsibleHeader} from '@hooks/header';
|
||||
@@ -41,7 +40,6 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
marginBottom: Platform.select({ios: BOTTOM_TAB_HEIGHT}),
|
||||
},
|
||||
empty: {
|
||||
alignItems: 'center',
|
||||
@@ -58,7 +56,6 @@ function SavedMessages({posts, currentTimezone, isTimezoneEnabled}: Props) {
|
||||
const serverUrl = useServerUrl();
|
||||
const route = useRoute();
|
||||
const isFocused = useIsFocused();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const params = route.params as {direction: string};
|
||||
const toLeft = params.direction === 'left';
|
||||
@@ -88,8 +85,7 @@ function SavedMessages({posts, currentTimezone, isTimezoneEnabled}: Props) {
|
||||
}, [serverUrl, isFocused]);
|
||||
|
||||
const {scrollPaddingTop, scrollRef, scrollValue, onScroll, headerHeight} = useCollapsibleHeader<FlatList<string>>(true, onSnap);
|
||||
const paddingTop = useMemo(() => ({paddingTop: scrollPaddingTop - insets.top, flexGrow: 1}), [scrollPaddingTop, insets.top]);
|
||||
const scrollViewStyle = useMemo(() => ({top: insets.top}), [insets.top]);
|
||||
const paddingTop = useMemo(() => ({paddingTop: scrollPaddingTop, flexGrow: 1}), [scrollPaddingTop]);
|
||||
const data = useMemo(() => selectOrderedPosts(posts, 0, false, '', '', false, isTimezoneEnabled, currentTimezone, false).reverse(), [posts]);
|
||||
|
||||
const animated = useAnimatedStyle(() => {
|
||||
@@ -196,7 +192,6 @@ function SavedMessages({posts, currentTimezone, isTimezoneEnabled}: Props) {
|
||||
onScroll={onScroll}
|
||||
removeClippedSubviews={true}
|
||||
onViewableItemsChanged={onViewableItemsChanged}
|
||||
style={scrollViewStyle}
|
||||
testID='saved_messages.post_list.flat_list'
|
||||
/>
|
||||
</Animated.View>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import Animated from 'react-native-reanimated';
|
||||
|
||||
import Modifiers from './modifiers';
|
||||
import RecentSearches from './recent_searches';
|
||||
@@ -10,6 +11,7 @@ import type TeamSearchHistoryModel from '@typings/database/models/servers/team_s
|
||||
|
||||
type Props = {
|
||||
recentSearches: TeamSearchHistoryModel[];
|
||||
scrollEnabled: Animated.SharedValue<boolean>;
|
||||
searchValue?: string;
|
||||
setRecentValue: (value: string) => void;
|
||||
setSearchValue: (value: string) => void;
|
||||
@@ -18,7 +20,7 @@ type Props = {
|
||||
teamName: string;
|
||||
}
|
||||
|
||||
const Initial = ({setRecentValue, recentSearches, searchValue, teamId, teamName, setTeamId, setSearchValue}: Props) => {
|
||||
const Initial = ({recentSearches, scrollEnabled, searchValue, setRecentValue, teamId, teamName, setTeamId, setSearchValue}: Props) => {
|
||||
return (
|
||||
<>
|
||||
<Modifiers
|
||||
@@ -26,6 +28,7 @@ const Initial = ({setRecentValue, recentSearches, searchValue, teamId, teamName,
|
||||
setSearchValue={setSearchValue}
|
||||
setTeamId={setTeamId}
|
||||
teamId={teamId}
|
||||
scrollEnabled={scrollEnabled}
|
||||
/>
|
||||
{Boolean(recentSearches.length) &&
|
||||
<RecentSearches
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useMemo, useState} from 'react';
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||
import {IntlShape, useIntl} from 'react-intl';
|
||||
import {View} from 'react-native';
|
||||
import Animated, {useSharedValue, useAnimatedStyle, withTiming} from 'react-native-reanimated';
|
||||
@@ -73,34 +73,51 @@ const getModifiersSectionsData = (intl: IntlShape): ModifierItem[] => {
|
||||
};
|
||||
|
||||
type Props = {
|
||||
scrollEnabled: Animated.SharedValue<boolean>;
|
||||
setSearchValue: (value: string) => void;
|
||||
searchValue?: string;
|
||||
setTeamId: (id: string) => void;
|
||||
teamId: string;
|
||||
}
|
||||
const Modifiers = ({searchValue, setSearchValue, setTeamId, teamId}: Props) => {
|
||||
const Modifiers = ({scrollEnabled, searchValue, setSearchValue, setTeamId, teamId}: Props) => {
|
||||
const theme = useTheme();
|
||||
const intl = useIntl();
|
||||
|
||||
const [showMore, setShowMore] = useState(false);
|
||||
const show = useSharedValue(3 * MODIFIER_LABEL_HEIGHT);
|
||||
const data = useMemo(() => getModifiersSectionsData(intl), [intl]);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | undefined>();
|
||||
|
||||
const styles = getStyleFromTheme(theme);
|
||||
const animatedStyle = useAnimatedStyle(() => (
|
||||
{
|
||||
width: '100%',
|
||||
height: withTiming(show.value, {duration: 300}),
|
||||
overflow: 'hidden',
|
||||
}
|
||||
));
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
width: '100%',
|
||||
height: withTiming(show.value, {duration: 300}),
|
||||
overflow: 'hidden',
|
||||
}), []);
|
||||
|
||||
const handleShowMore = useCallback(() => {
|
||||
const nextShowMore = !showMore;
|
||||
setShowMore(nextShowMore);
|
||||
scrollEnabled.value = false;
|
||||
show.value = (nextShowMore ? data.length : 3) * MODIFIER_LABEL_HEIGHT;
|
||||
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
setTimeout(() => {
|
||||
scrollEnabled.value = true;
|
||||
}, 350);
|
||||
}, [showMore]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (timeoutRef.current) {
|
||||
scrollEnabled.value = true;
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const renderModifier = (item: ModifierItem) => {
|
||||
return (
|
||||
<Modifier
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useIsFocused, useNavigation} from '@react-navigation/native';
|
||||
import React, {useCallback, useMemo, useState} from 'react';
|
||||
import React, {useCallback, useMemo, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {FlatList, LayoutChangeEvent, Platform, StyleSheet, ViewProps} from 'react-native';
|
||||
import Animated, {useAnimatedStyle, useDerivedValue, withTiming} from 'react-native-reanimated';
|
||||
@@ -11,6 +11,7 @@ import {Edge, SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-cont
|
||||
import {getPosts} from '@actions/local/post';
|
||||
import {addSearchToTeamSearchHistory} from '@actions/local/team';
|
||||
import {searchPosts, searchFiles} from '@actions/remote/search';
|
||||
import useDidUpdate from '@app/hooks/did_update';
|
||||
import Autocomplete from '@components/autocomplete';
|
||||
import FreezeScreen from '@components/freeze_screen';
|
||||
import Loading from '@components/loading';
|
||||
@@ -81,8 +82,10 @@ const SearchScreen = ({teamId}: Props) => {
|
||||
const serverUrl = useServerUrl();
|
||||
const searchTerm = (nav.getState().routes[stateIndex].params as any)?.searchTerm;
|
||||
|
||||
const [cursorPosition, setCursorPosition] = useState(searchTerm?.length);
|
||||
const [searchValue, setSearchValue] = useState<string>(searchTerm);
|
||||
const clearRef = useRef<boolean>(false);
|
||||
const cancelRef = useRef<boolean>(false);
|
||||
const [cursorPosition, setCursorPosition] = useState(searchTerm?.length || 0);
|
||||
const [searchValue, setSearchValue] = useState<string>(searchTerm || '');
|
||||
const [searchTeamId, setSearchTeamId] = useState<string>(teamId);
|
||||
const [selectedTab, setSelectedTab] = useState<TabType>(TabTypes.MESSAGES);
|
||||
const [filter, setFilter] = useState<FileFilter>(FileFilters.ALL);
|
||||
@@ -96,19 +99,42 @@ const SearchScreen = ({teamId}: Props) => {
|
||||
const [fileInfos, setFileInfos] = useState<FileInfo[]>(emptyFileResults);
|
||||
const [fileChannelIds, setFileChannelIds] = useState<string[]>([]);
|
||||
|
||||
const onSnap = (offset: number) => {
|
||||
scrollRef.current?.scrollToOffset({offset, animated: true});
|
||||
const onSnap = (offset: number, animated = true) => {
|
||||
scrollRef.current?.scrollToOffset({offset, animated});
|
||||
};
|
||||
|
||||
const {scrollPaddingTop, scrollRef, scrollValue, onScroll, headerHeight, hideHeader} = useCollapsibleHeader<FlatList>(true, onSnap);
|
||||
const {
|
||||
headerHeight,
|
||||
headerOffset,
|
||||
hideHeader,
|
||||
lockValue,
|
||||
onScroll,
|
||||
scrollEnabled,
|
||||
scrollPaddingTop,
|
||||
scrollRef,
|
||||
scrollValue,
|
||||
setAutoScroll,
|
||||
unlock,
|
||||
} = useCollapsibleHeader<FlatList>(true, onSnap);
|
||||
|
||||
const handleCancelAndClearSearch = useCallback(() => {
|
||||
const resetToInitial = useCallback(() => {
|
||||
setShowResults(false);
|
||||
setSearchValue('');
|
||||
setLastSearchedValue('');
|
||||
setFilter(FileFilters.ALL);
|
||||
setShowResults(false);
|
||||
}, []);
|
||||
|
||||
const handleClearSearch = useCallback(() => {
|
||||
clearRef.current = true;
|
||||
resetToInitial();
|
||||
}, [resetToInitial]);
|
||||
|
||||
const handleCancelSearch = useCallback(() => {
|
||||
cancelRef.current = true;
|
||||
resetToInitial();
|
||||
onSnap(0);
|
||||
}, [resetToInitial]);
|
||||
|
||||
const handleTextChange = useCallback((newValue: string) => {
|
||||
setSearchValue(newValue);
|
||||
setCursorPosition(newValue.length);
|
||||
@@ -121,9 +147,10 @@ const SearchScreen = ({teamId}: Props) => {
|
||||
const handleSearch = useCallback(async (newSearchTeamId: string, term: string) => {
|
||||
const searchParams = getSearchParams(term);
|
||||
if (!searchParams.terms) {
|
||||
handleCancelAndClearSearch();
|
||||
handleClearSearch();
|
||||
return;
|
||||
}
|
||||
hideHeader(true);
|
||||
handleLoading(true);
|
||||
setFilter(FileFilters.ALL);
|
||||
setLastSearchedValue(term);
|
||||
@@ -141,7 +168,7 @@ const SearchScreen = ({teamId}: Props) => {
|
||||
setFileChannelIds(channels?.length ? channels : emptyChannelIds);
|
||||
handleLoading(false);
|
||||
setShowResults(true);
|
||||
}, [handleCancelAndClearSearch, handleLoading, showResults]);
|
||||
}, [handleClearSearch, handleLoading]);
|
||||
|
||||
const onSubmit = useCallback(() => {
|
||||
handleSearch(searchTeamId, searchValue);
|
||||
@@ -167,38 +194,33 @@ const SearchScreen = ({teamId}: Props) => {
|
||||
handleSearch(newTeamId, lastSearchedValue);
|
||||
}, [lastSearchedValue, handleSearch]);
|
||||
|
||||
const containerStyle = useMemo(() => {
|
||||
const justifyContent = (resultsLoading || loading) ? 'center' : 'flex-start';
|
||||
return {paddingTop: scrollPaddingTop, flexGrow: 1, justifyContent} as ViewProps;
|
||||
const initialContainerStyle = useMemo(() => {
|
||||
return {
|
||||
paddingTop: scrollPaddingTop,
|
||||
flexGrow: 1,
|
||||
justifyContent: (resultsLoading || loading) ? 'center' : 'flex-start',
|
||||
} as ViewProps;
|
||||
}, [loading, resultsLoading, scrollPaddingTop]);
|
||||
|
||||
const loadingComponent = useMemo(() => (
|
||||
<Loading
|
||||
containerStyle={[styles.loading, {paddingTop: scrollPaddingTop}]}
|
||||
color={theme.buttonBg}
|
||||
size='large'
|
||||
/>
|
||||
), [theme, scrollPaddingTop]);
|
||||
|
||||
const initialComponent = useMemo(() => (
|
||||
<Initial
|
||||
searchValue={searchValue}
|
||||
setRecentValue={handleRecentSearch}
|
||||
setSearchValue={handleTextChange}
|
||||
setTeamId={setSearchTeamId}
|
||||
teamId={searchTeamId}
|
||||
/>
|
||||
), [searchValue, searchTeamId, handleRecentSearch, handleTextChange]);
|
||||
|
||||
const renderItem = useCallback(() => {
|
||||
if (loading) {
|
||||
return loadingComponent;
|
||||
}
|
||||
return initialComponent;
|
||||
}, [
|
||||
loading && loadingComponent,
|
||||
initialComponent,
|
||||
]);
|
||||
const renderInitialOrLoadingItem = useCallback(() => {
|
||||
return loading ? (
|
||||
<Loading
|
||||
containerStyle={[styles.loading, {paddingTop: scrollPaddingTop}]}
|
||||
color={theme.buttonBg}
|
||||
size='large'
|
||||
/>
|
||||
) : (
|
||||
<Initial
|
||||
scrollEnabled={scrollEnabled}
|
||||
searchValue={searchValue}
|
||||
setRecentValue={handleRecentSearch}
|
||||
setSearchValue={handleTextChange}
|
||||
setTeamId={setSearchTeamId}
|
||||
teamId={searchTeamId}
|
||||
/>
|
||||
);
|
||||
}, [handleRecentSearch, handleTextChange, loading,
|
||||
scrollPaddingTop, searchTeamId, searchValue, theme]);
|
||||
|
||||
const animated = useAnimatedStyle(() => {
|
||||
if (isFocused) {
|
||||
@@ -211,37 +233,20 @@ const SearchScreen = ({teamId}: Props) => {
|
||||
|
||||
return {
|
||||
opacity: withTiming(0, {duration: 150}),
|
||||
flex: 1,
|
||||
transform: [{translateX: withTiming(stateIndex < searchScreenIndex ? 25 : -25, {duration: 150})}],
|
||||
};
|
||||
}, [isFocused, stateIndex]);
|
||||
|
||||
const top = useAnimatedStyle(() => {
|
||||
return {
|
||||
top: headerHeight.value,
|
||||
zIndex: lastSearchedValue ? 10 : 0,
|
||||
};
|
||||
}, [headerHeight.value, lastSearchedValue]);
|
||||
const headerTopStyle = useAnimatedStyle(() => ({
|
||||
top: lockValue.value ? lockValue.value : headerHeight.value,
|
||||
zIndex: lastSearchedValue ? 10 : 0,
|
||||
}), [headerHeight, lastSearchedValue, lockValue]);
|
||||
|
||||
const onLayout = useCallback((e: LayoutChangeEvent) => {
|
||||
setContainerHeight(e.nativeEvent.layout.height);
|
||||
}, []);
|
||||
|
||||
let header = null;
|
||||
if (lastSearchedValue && !loading) {
|
||||
header = (
|
||||
<Header
|
||||
teamId={searchTeamId}
|
||||
setTeamId={handleResultsTeamChange}
|
||||
onTabSelect={setSelectedTab}
|
||||
onFilterChanged={handleFilterChange}
|
||||
numberMessages={posts.length}
|
||||
selectedTab={selectedTab}
|
||||
numberFiles={fileInfos.length}
|
||||
selectedFilter={filter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const autocompleteMaxHeight = useDerivedValue(() => {
|
||||
const iosAdjust = keyboardHeight ? keyboardHeight - BOTTOM_TAB_HEIGHT : insets.bottom;
|
||||
const autocompleteRemoveFromHeight = headerHeight.value + (Platform.OS === 'ios' ? iosAdjust : 0);
|
||||
@@ -250,7 +255,7 @@ const SearchScreen = ({teamId}: Props) => {
|
||||
|
||||
const autocompletePosition = useDerivedValue(() => {
|
||||
return headerHeight.value - AutocompletePaddingTop;
|
||||
}, [containerHeight]);
|
||||
}, [headerHeight]);
|
||||
|
||||
const autocomplete = useMemo(() => (
|
||||
<Autocomplete
|
||||
@@ -267,6 +272,31 @@ const SearchScreen = ({teamId}: Props) => {
|
||||
/>
|
||||
), [cursorPosition, handleTextChange, searchValue, autocompleteMaxHeight, autocompletePosition, searchTeamId]);
|
||||
|
||||
// when clearing the input from the search results, scroll the initial view
|
||||
// back to the top so the header is in the collapsed state
|
||||
const onFlatLayout = useCallback(() => {
|
||||
if (clearRef.current || cancelRef.current) {
|
||||
unlock();
|
||||
}
|
||||
if (clearRef.current) {
|
||||
onSnap(headerOffset, false);
|
||||
clearRef.current = false;
|
||||
} else if (cancelRef.current) {
|
||||
onSnap(0);
|
||||
cancelRef.current = false;
|
||||
}
|
||||
}, [headerOffset, scrollRef]);
|
||||
|
||||
useDidUpdate(() => {
|
||||
if (isFocused) {
|
||||
setTimeout(() => {
|
||||
setAutoScroll(true);
|
||||
}, 300);
|
||||
} else {
|
||||
setAutoScroll(false);
|
||||
}
|
||||
}, [isFocused]);
|
||||
|
||||
return (
|
||||
<FreezeScreen freeze={!isFocused}>
|
||||
<NavigationHeader
|
||||
@@ -275,13 +305,14 @@ const SearchScreen = ({teamId}: Props) => {
|
||||
title={intl.formatMessage({id: 'screen.search.title', defaultMessage: 'Search'})}
|
||||
hasSearch={true}
|
||||
scrollValue={scrollValue}
|
||||
lockValue={lockValue}
|
||||
hideHeader={hideHeader}
|
||||
onChangeText={handleTextChange}
|
||||
onSubmitEditing={onSubmit}
|
||||
blurOnSubmit={true}
|
||||
placeholder={intl.formatMessage({id: 'screen.search.placeholder', defaultMessage: 'Search messages & files'})}
|
||||
onClear={handleCancelAndClearSearch}
|
||||
onCancel={handleCancelAndClearSearch}
|
||||
onClear={handleClearSearch}
|
||||
onCancel={handleCancelSearch}
|
||||
defaultValue={searchValue}
|
||||
/>
|
||||
<SafeAreaView
|
||||
@@ -290,14 +321,26 @@ const SearchScreen = ({teamId}: Props) => {
|
||||
onLayout={onLayout}
|
||||
>
|
||||
<Animated.View style={animated}>
|
||||
<Animated.View style={top}>
|
||||
<Animated.View style={headerTopStyle}>
|
||||
<RoundedHeaderContext/>
|
||||
{header}
|
||||
{lastSearchedValue && !loading &&
|
||||
<Header
|
||||
teamId={searchTeamId}
|
||||
setTeamId={handleResultsTeamChange}
|
||||
onTabSelect={setSelectedTab}
|
||||
onFilterChanged={handleFilterChange}
|
||||
numberMessages={posts.length}
|
||||
selectedTab={selectedTab}
|
||||
numberFiles={fileInfos.length}
|
||||
selectedFilter={filter}
|
||||
/>
|
||||
}
|
||||
</Animated.View>
|
||||
{!showResults &&
|
||||
<AnimatedFlatList
|
||||
onLayout={onFlatLayout}
|
||||
data={dummyData}
|
||||
contentContainerStyle={containerStyle}
|
||||
contentContainerStyle={initialContainerStyle}
|
||||
keyboardShouldPersistTaps='handled'
|
||||
keyboardDismissMode={'interactive'}
|
||||
nestedScrollEnabled={true}
|
||||
@@ -308,7 +351,7 @@ const SearchScreen = ({teamId}: Props) => {
|
||||
scrollToOverflowEnabled={true}
|
||||
overScrollMode='always'
|
||||
ref={scrollRef}
|
||||
renderItem={renderItem}
|
||||
renderItem={renderInitialOrLoadingItem}
|
||||
/>
|
||||
}
|
||||
{showResults && !loading &&
|
||||
@@ -318,7 +361,7 @@ const SearchScreen = ({teamId}: Props) => {
|
||||
searchValue={lastSearchedValue}
|
||||
posts={posts}
|
||||
fileInfos={fileInfos}
|
||||
scrollPaddingTop={scrollPaddingTop}
|
||||
scrollPaddingTop={lockValue.value}
|
||||
fileChannelIds={fileChannelIds}
|
||||
/>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user