forked from Ivasoft/mattermost-mobile
[Gekidou] - Custom navigation header (#5820)
* Custom navigation header * Add Search props to header * Add Search props to header * Simplify example * Set large subtitle opacity to 80% * Add extra spacing on large title
This commit is contained in:
@@ -8,7 +8,7 @@ import {useTheme} from '@context/theme';
|
||||
|
||||
type Props = {
|
||||
backgroundColor?: string;
|
||||
borderColor: string;
|
||||
borderColor?: string;
|
||||
color?: string;
|
||||
style?: Animated.WithAnimatedValue<StyleProp<TextStyle>>;
|
||||
type?: 'Normal' | 'Small';
|
||||
|
||||
64
app/components/navigation_header/context.tsx
Normal file
64
app/components/navigation_header/context.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import Animated, {useAnimatedStyle} from 'react-native-reanimated';
|
||||
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
defaultHeight: number;
|
||||
hasSearch: boolean;
|
||||
isLargeTitle: boolean;
|
||||
largeHeight: number;
|
||||
scrollValue: Animated.SharedValue<number>;
|
||||
theme: Theme;
|
||||
top: number;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
backgroundColor: theme.sidebarBg,
|
||||
height: 16,
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
},
|
||||
content: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderTopLeftRadius: 12,
|
||||
borderTopRightRadius: 12,
|
||||
flex: 1,
|
||||
},
|
||||
}));
|
||||
|
||||
const NavigationHeaderContext = ({
|
||||
defaultHeight,
|
||||
hasSearch,
|
||||
isLargeTitle,
|
||||
largeHeight,
|
||||
scrollValue,
|
||||
theme,
|
||||
top,
|
||||
}: Props) => {
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const marginTop = useAnimatedStyle(() => {
|
||||
const normal = defaultHeight + top;
|
||||
const calculated = -(top + scrollValue.value);
|
||||
const searchHeight = hasSearch ? defaultHeight + 9 : 0;
|
||||
if (!isLargeTitle) {
|
||||
return {marginTop: Math.max((normal + calculated), normal)};
|
||||
}
|
||||
|
||||
return {marginTop: Math.max((-scrollValue.value + largeHeight + searchHeight), normal)};
|
||||
}, [defaultHeight, largeHeight, isLargeTitle, hasSearch, top]);
|
||||
|
||||
return (
|
||||
<Animated.View style={[styles.container, marginTop]}>
|
||||
<Animated.View style={styles.content}/>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavigationHeaderContext;
|
||||
|
||||
194
app/components/navigation_header/header.tsx
Normal file
194
app/components/navigation_header/header.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useMemo} from 'react';
|
||||
import {Platform, Text} from 'react-native';
|
||||
import Animated, {useAnimatedStyle, withTiming} from 'react-native-reanimated';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
export type HeaderRightButton = {
|
||||
borderless?: boolean;
|
||||
buttonType?: 'native' | 'opacity' | 'highlight';
|
||||
color?: string;
|
||||
iconName: string;
|
||||
onPress: () => void;
|
||||
rippleRadius?: number;
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
defaultHeight: number;
|
||||
hasSearch: boolean;
|
||||
isLargeTitle: boolean;
|
||||
largeHeight: number;
|
||||
leftComponent?: React.ReactElement;
|
||||
onBackPress?: () => void;
|
||||
rightButtons?: HeaderRightButton[];
|
||||
scrollValue: Animated.SharedValue<number>;
|
||||
showBackButton?: boolean;
|
||||
subtitle?: string;
|
||||
theme: Theme;
|
||||
title?: string;
|
||||
top: number;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.sidebarBg,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-start',
|
||||
paddingHorizontal: 16,
|
||||
zIndex: 10,
|
||||
},
|
||||
subtitle: {
|
||||
color: changeOpacity(theme.sidebarHeaderTextColor, 0.72),
|
||||
fontFamily: 'OpenSans',
|
||||
fontSize: 12,
|
||||
lineHeight: 12,
|
||||
marginBottom: 8,
|
||||
marginTop: 2,
|
||||
},
|
||||
titleContainer: {
|
||||
alignItems: Platform.select({android: 'flex-start', ios: 'center'}),
|
||||
justifyContent: 'center',
|
||||
flex: 3,
|
||||
height: '100%',
|
||||
paddingHorizontal: 8,
|
||||
},
|
||||
leftContainer: {
|
||||
alignItems: 'center',
|
||||
flex: Platform.select({ios: 1}),
|
||||
flexDirection: 'row',
|
||||
height: '100%',
|
||||
},
|
||||
rightContainer: {
|
||||
alignItems: 'center',
|
||||
flex: Platform.select({ios: 1}),
|
||||
flexDirection: 'row',
|
||||
height: '100%',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
rightIcon: {
|
||||
marginLeft: 20,
|
||||
},
|
||||
title: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
...typography('Heading', 300),
|
||||
},
|
||||
}));
|
||||
|
||||
const Header = ({
|
||||
defaultHeight,
|
||||
hasSearch,
|
||||
isLargeTitle,
|
||||
largeHeight,
|
||||
leftComponent,
|
||||
onBackPress,
|
||||
rightButtons,
|
||||
scrollValue,
|
||||
showBackButton = true,
|
||||
subtitle,
|
||||
theme,
|
||||
title,
|
||||
top,
|
||||
}: Props) => {
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const opacity = useAnimatedStyle(() => {
|
||||
if (!isLargeTitle) {
|
||||
return {opacity: 1};
|
||||
}
|
||||
|
||||
if (hasSearch) {
|
||||
return {opacity: 0};
|
||||
}
|
||||
|
||||
const barHeight = Platform.OS === 'ios' ? (largeHeight - defaultHeight - (top / 2)) : largeHeight - defaultHeight;
|
||||
const val = (top + scrollValue.value);
|
||||
return {
|
||||
opacity: val >= barHeight ? withTiming(1, {duration: 250}) : 0,
|
||||
};
|
||||
}, [defaultHeight, largeHeight, top, isLargeTitle, hasSearch]);
|
||||
|
||||
const containerStyle = useMemo(() => {
|
||||
return [styles.container, {height: defaultHeight + top, paddingTop: top}];
|
||||
}, [top, defaultHeight, theme]);
|
||||
|
||||
const additionalTitleStyle = useMemo(() => ({
|
||||
marginLeft: Platform.select({android: showBackButton && !leftComponent ? 20 : 0}),
|
||||
}), [leftComponent, showBackButton, theme]);
|
||||
|
||||
return (
|
||||
<Animated.View style={containerStyle}>
|
||||
<Animated.View style={styles.leftContainer}>
|
||||
{showBackButton &&
|
||||
<TouchableWithFeedback
|
||||
borderlessRipple={true}
|
||||
onPress={onBackPress}
|
||||
rippleRadius={20}
|
||||
type={Platform.select({android: 'native', default: 'opacity'})}
|
||||
testID='navigation.header.back'
|
||||
>
|
||||
<CompassIcon
|
||||
size={24}
|
||||
name={Platform.select({android: 'arrow-left', ios: 'arrow-back-ios'})!}
|
||||
color={theme.sidebarHeaderTextColor}
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
}
|
||||
{leftComponent}
|
||||
</Animated.View>
|
||||
<Animated.View style={[styles.titleContainer, additionalTitleStyle]}>
|
||||
{!hasSearch &&
|
||||
<Animated.Text
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
style={[styles.title, opacity]}
|
||||
testID='navigation.header.title'
|
||||
>
|
||||
{title}
|
||||
</Animated.Text>
|
||||
}
|
||||
{!isLargeTitle &&
|
||||
<Text
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
style={styles.subtitle}
|
||||
testID='navigation.header.subtitle'
|
||||
>
|
||||
{subtitle}
|
||||
</Text>
|
||||
}
|
||||
</Animated.View>
|
||||
<Animated.View style={styles.rightContainer}>
|
||||
{Boolean(rightButtons?.length) &&
|
||||
rightButtons?.map((r, i) => (
|
||||
<TouchableWithFeedback
|
||||
key={r.iconName}
|
||||
borderlessRipple={r.borderless === undefined ? true : r.borderless}
|
||||
onPress={r.onPress}
|
||||
rippleRadius={r.rippleRadius || 20}
|
||||
type={r.buttonType || Platform.select({android: 'native', default: 'opacity'})}
|
||||
style={i > 0 ? styles.rightIcon : undefined}
|
||||
testID={r.testID}
|
||||
>
|
||||
<CompassIcon
|
||||
size={24}
|
||||
name={r.iconName}
|
||||
color={r.color || theme.sidebarHeaderTextColor}
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
))
|
||||
}
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
|
||||
135
app/components/navigation_header/index.tsx
Normal file
135
app/components/navigation_header/index.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FlatList, ScrollView, SectionList} from 'react-native';
|
||||
import Animated, {useAnimatedStyle} from 'react-native-reanimated';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import {useTheme} from '@context/theme';
|
||||
import useHeaderHeight from '@hooks/header';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import NavigationHeaderContext from './context';
|
||||
import Header, {HeaderRightButton} from './header';
|
||||
import NavigationHeaderLargeTitle from './large';
|
||||
import NavigationSearch from './search';
|
||||
import NavigationHeaderSearchContext from './search_context';
|
||||
|
||||
import type {SearchProps} from '@components/search';
|
||||
|
||||
type Props = SearchProps & {
|
||||
forwardedRef?: React.RefObject<ScrollView | FlatList | SectionList>;
|
||||
hasSearch?: boolean;
|
||||
isLargeTitle?: boolean;
|
||||
leftComponent?: React.ReactElement;
|
||||
onBackPress?: () => void;
|
||||
rightButtons?: HeaderRightButton[];
|
||||
scrollValue: Animated.SharedValue<number>;
|
||||
showBackButton?: boolean;
|
||||
showHeaderInContext?: boolean;
|
||||
subtitle?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
backgroundColor: theme.sidebarBg,
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
zIndex: 10,
|
||||
},
|
||||
}));
|
||||
|
||||
const NavigationHeader = ({
|
||||
forwardedRef,
|
||||
hasSearch = false,
|
||||
isLargeTitle = false,
|
||||
leftComponent,
|
||||
onBackPress,
|
||||
rightButtons,
|
||||
scrollValue,
|
||||
showBackButton,
|
||||
showHeaderInContext = true,
|
||||
subtitle,
|
||||
title = '',
|
||||
...searchProps
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
const insets = useSafeAreaInsets();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const {largeHeight, defaultHeight} = useHeaderHeight(isLargeTitle, Boolean(subtitle), hasSearch);
|
||||
const containerHeight = useAnimatedStyle(() => {
|
||||
const normal = defaultHeight + insets.top;
|
||||
const calculated = -(insets.top + scrollValue.value);
|
||||
return {height: Math.max((normal + calculated), normal)};
|
||||
}, [defaultHeight, insets.top]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Animated.View style={[styles.container, containerHeight]}>
|
||||
<Header
|
||||
defaultHeight={defaultHeight}
|
||||
hasSearch={hasSearch}
|
||||
isLargeTitle={isLargeTitle}
|
||||
largeHeight={largeHeight}
|
||||
leftComponent={leftComponent}
|
||||
onBackPress={onBackPress}
|
||||
rightButtons={rightButtons}
|
||||
scrollValue={scrollValue}
|
||||
showBackButton={showBackButton}
|
||||
subtitle={subtitle}
|
||||
theme={theme}
|
||||
title={title}
|
||||
top={insets.top}
|
||||
/>
|
||||
{isLargeTitle &&
|
||||
<NavigationHeaderLargeTitle
|
||||
defaultHeight={defaultHeight}
|
||||
hasSearch={hasSearch}
|
||||
largeHeight={largeHeight}
|
||||
scrollValue={scrollValue}
|
||||
subtitle={subtitle}
|
||||
theme={theme}
|
||||
title={title}
|
||||
top={insets.top}
|
||||
/>
|
||||
}
|
||||
</Animated.View>
|
||||
{hasSearch &&
|
||||
<>
|
||||
<NavigationSearch
|
||||
{...searchProps}
|
||||
defaultHeight={defaultHeight}
|
||||
forwardedRef={forwardedRef}
|
||||
largeHeight={largeHeight}
|
||||
scrollValue={scrollValue}
|
||||
theme={theme}
|
||||
top={insets.top}
|
||||
/>
|
||||
<NavigationHeaderSearchContext
|
||||
defaultHeight={defaultHeight}
|
||||
largeHeight={largeHeight}
|
||||
scrollValue={scrollValue}
|
||||
theme={theme}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
{showHeaderInContext &&
|
||||
<NavigationHeaderContext
|
||||
defaultHeight={defaultHeight}
|
||||
hasSearch={hasSearch}
|
||||
isLargeTitle={isLargeTitle}
|
||||
largeHeight={largeHeight}
|
||||
scrollValue={scrollValue}
|
||||
theme={theme}
|
||||
top={insets.top}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavigationHeader;
|
||||
|
||||
84
app/components/navigation_header/large.tsx
Normal file
84
app/components/navigation_header/large.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useMemo} from 'react';
|
||||
import {Text} from 'react-native';
|
||||
import Animated, {useAnimatedStyle} from 'react-native-reanimated';
|
||||
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
type Props = {
|
||||
defaultHeight: number;
|
||||
hasSearch: boolean;
|
||||
largeHeight: number;
|
||||
scrollValue: Animated.SharedValue<number>;
|
||||
subtitle?: string;
|
||||
theme: Theme;
|
||||
title: string;
|
||||
top: number;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
backgroundColor: theme.sidebarBg,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
heading: {
|
||||
...typography('Heading', 800),
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
},
|
||||
subHeading: {
|
||||
...typography('Heading', 200, 'Regular'),
|
||||
color: changeOpacity(theme.sidebarHeaderTextColor, 0.8),
|
||||
},
|
||||
}));
|
||||
|
||||
const NavigationHeaderLargeTitle = ({
|
||||
defaultHeight,
|
||||
largeHeight,
|
||||
hasSearch,
|
||||
scrollValue,
|
||||
subtitle,
|
||||
theme,
|
||||
title,
|
||||
top,
|
||||
}: Props) => {
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const transform = useAnimatedStyle(() => {
|
||||
return {
|
||||
transform: [{translateY: -(top + scrollValue.value)}],
|
||||
};
|
||||
}, [top]);
|
||||
|
||||
const containerStyle = useMemo(() => {
|
||||
return [{height: largeHeight - defaultHeight}, styles.container];
|
||||
}, [defaultHeight, largeHeight, theme]);
|
||||
|
||||
return (
|
||||
<Animated.View style={[containerStyle, transform]}>
|
||||
<Text
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
style={styles.heading}
|
||||
testID='navigation.large_header.title'
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
{!hasSearch && Boolean(subtitle) &&
|
||||
<Text
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
style={styles.subHeading}
|
||||
testID='navigation.large_header.subtitle'
|
||||
>
|
||||
{subtitle}
|
||||
</Text>
|
||||
}
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavigationHeaderLargeTitle;
|
||||
|
||||
77
app/components/navigation_header/search.tsx
Normal file
77
app/components/navigation_header/search.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {FlatList, Platform, ScrollView, SectionList, VirtualizedList} from 'react-native';
|
||||
import Animated, {useAnimatedStyle} from 'react-native-reanimated';
|
||||
|
||||
import Search, {SearchProps} from '@components/search';
|
||||
import {ANDROID_HEADER_SEARCH_INSET, IOS_HEADER_SEARCH_INSET, SEARCH_INPUT_HEIGHT, TABLET_HEADER_SEARCH_INSET} from '@constants/view';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type Props = SearchProps & {
|
||||
defaultHeight: number;
|
||||
forwardedRef?: React.RefObject<ScrollView | FlatList | SectionList>;
|
||||
largeHeight: number;
|
||||
scrollValue: Animated.SharedValue<number>;
|
||||
theme: Theme;
|
||||
top: number;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
backgroundColor: theme.sidebarBg,
|
||||
height: SEARCH_INPUT_HEIGHT + 5,
|
||||
justifyContent: 'flex-start',
|
||||
paddingHorizontal: 20,
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
zIndex: 10,
|
||||
},
|
||||
}));
|
||||
|
||||
const NavigationSearch = ({
|
||||
defaultHeight,
|
||||
forwardedRef,
|
||||
largeHeight,
|
||||
scrollValue,
|
||||
theme,
|
||||
top,
|
||||
...searchProps
|
||||
}: Props) => {
|
||||
const isTablet = useIsTablet();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const searchTop = useAnimatedStyle(() => {
|
||||
return {marginTop: Math.max((-scrollValue.value + largeHeight), top)};
|
||||
}, [defaultHeight, largeHeight, top]);
|
||||
|
||||
const onFocus = useCallback((e) => {
|
||||
const searchInset = isTablet ? TABLET_HEADER_SEARCH_INSET : IOS_HEADER_SEARCH_INSET;
|
||||
const offset = Platform.select({android: largeHeight + ANDROID_HEADER_SEARCH_INSET, default: defaultHeight + searchInset});
|
||||
if (forwardedRef?.current && Math.abs(scrollValue.value) <= top) {
|
||||
if ((forwardedRef.current as ScrollView).scrollTo) {
|
||||
(forwardedRef.current as ScrollView).scrollTo({y: offset, animated: true});
|
||||
} else {
|
||||
(forwardedRef.current as VirtualizedList<any>).scrollToOffset({
|
||||
offset,
|
||||
animated: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
searchProps.onFocus?.(e);
|
||||
}, [largeHeight, top]);
|
||||
|
||||
return (
|
||||
<Animated.View style={[styles.container, searchTop]}>
|
||||
<Search
|
||||
{...searchProps}
|
||||
onFocus={onFocus}
|
||||
/>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavigationSearch;
|
||||
|
||||
46
app/components/navigation_header/search_context.tsx
Normal file
46
app/components/navigation_header/search_context.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import Animated, {useAnimatedStyle} from 'react-native-reanimated';
|
||||
|
||||
import {ANDROID_HEADER_SEARCH_INSET} from '@constants/view';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
defaultHeight: number;
|
||||
largeHeight: number;
|
||||
scrollValue: Animated.SharedValue<number>;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
backgroundColor: theme.sidebarBg,
|
||||
height: 20,
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
},
|
||||
}));
|
||||
|
||||
const NavigationHeaderSearchContext = ({
|
||||
defaultHeight,
|
||||
largeHeight,
|
||||
scrollValue,
|
||||
theme,
|
||||
}: Props) => {
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const marginTop = useAnimatedStyle(() => {
|
||||
return {marginTop: (-scrollValue.value + largeHeight + defaultHeight) - ANDROID_HEADER_SEARCH_INSET};
|
||||
}, [defaultHeight, largeHeight]);
|
||||
|
||||
return (
|
||||
<Animated.View style={[styles.container, marginTop]}>
|
||||
<Animated.View style={styles.content}/>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavigationHeaderSearchContext;
|
||||
|
||||
183
app/components/search/index.tsx
Normal file
183
app/components/search/index.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {ActivityIndicatorProps, Platform, StyleProp, TextInput, TextInputProps, TextStyle, TouchableOpacityProps, ViewStyle} from 'react-native';
|
||||
import {SearchBar} from 'react-native-elements';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {SEARCH_INPUT_HEIGHT} from '@constants/view';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
export type SearchProps = TextInputProps & {
|
||||
cancelIcon?: React.ReactElement;
|
||||
cancelButtonProps?: Partial<TouchableOpacityProps> & {
|
||||
buttonStyle?: StyleProp<ViewStyle>;
|
||||
buttonTextStyle?: StyleProp<TextStyle>;
|
||||
color?: string;
|
||||
buttonDisabledStyle?: StyleProp<ViewStyle>;
|
||||
buttonDisabledTextStyle?: StyleProp<ViewStyle>;
|
||||
};
|
||||
cancelButtonTitle?: string;
|
||||
clearIcon?: React.ReactElement;
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
inputContainerStyle?: StyleProp<ViewStyle>;
|
||||
inputStyle?: StyleProp<TextStyle>;
|
||||
loadingProps?: ActivityIndicatorProps;
|
||||
leftIconContainerStyle?: StyleProp<ViewStyle>;
|
||||
onCancel?(): void;
|
||||
onClear?(): void;
|
||||
rightIconContainerStyle?: StyleProp<ViewStyle>;
|
||||
searchIcon?: React.ReactElement;
|
||||
showCancel?: boolean;
|
||||
showLoading?: boolean;
|
||||
};
|
||||
|
||||
type SearchRef = {
|
||||
blur: () => void;
|
||||
cancel: () => void;
|
||||
clear: () => void;
|
||||
focus: () => void;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
containerStyle: {
|
||||
backgroundColor: undefined,
|
||||
height: undefined,
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
},
|
||||
inputContainerStyle: {
|
||||
backgroundColor: changeOpacity(theme.sidebarText, 0.12),
|
||||
borderRadius: 8,
|
||||
height: SEARCH_INPUT_HEIGHT,
|
||||
marginLeft: 0,
|
||||
},
|
||||
inputStyle: {
|
||||
color: theme.sidebarText,
|
||||
marginLeft: Platform.select({ios: 6, android: 14}),
|
||||
top: Platform.select({android: 1}),
|
||||
...typography('Body', 200, 'Regular'),
|
||||
lineHeight: undefined,
|
||||
},
|
||||
}));
|
||||
|
||||
const Search = forwardRef<SearchRef, SearchProps>((props: SearchProps, ref) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const searchRef = useRef<TextInput>(null);
|
||||
const [value, setValue] = useState(props.value || '');
|
||||
const searchClearButtonTestID = `${props.testID}.search.clear.button`;
|
||||
const searchCancelButtonTestID = `${props.testID}.search.cancel.button`;
|
||||
const searchInputTestID = `${props.testID}.search.input`;
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
setValue('');
|
||||
props.onCancel?.();
|
||||
}, []);
|
||||
|
||||
const onClear = useCallback(() => {
|
||||
setValue('');
|
||||
props.onClear?.();
|
||||
}, []);
|
||||
|
||||
const onChangeText = useCallback((text: string) => {
|
||||
setValue(text);
|
||||
props.onChangeText?.(text);
|
||||
}, []);
|
||||
|
||||
const cancelButtonProps = useMemo(() => ({
|
||||
buttonTextStyle: {
|
||||
color: changeOpacity(theme.sidebarText, 0.72),
|
||||
...typography('Body', 100, 'Regular'),
|
||||
},
|
||||
}), [theme]);
|
||||
|
||||
const clearIcon = useMemo(() => {
|
||||
return (
|
||||
<CompassIcon
|
||||
color={changeOpacity(theme.sidebarText, Platform.select({android: 0.56, default: 0.72}))}
|
||||
name={Platform.select({android: 'close', default: 'close-circle'})}
|
||||
onPress={searchRef.current?.clear}
|
||||
size={Platform.select({android: 24, default: 18})}
|
||||
testID={searchClearButtonTestID}
|
||||
/>
|
||||
);
|
||||
}, [searchRef.current, theme]);
|
||||
|
||||
const searchIcon = useMemo(() => (
|
||||
<CompassIcon
|
||||
color={changeOpacity(theme.sidebarText, Platform.select({android: 0.56, default: 0.72}))}
|
||||
name='magnify'
|
||||
size={24}
|
||||
/>
|
||||
), [theme]);
|
||||
|
||||
const cancelIcon = useMemo(() => (
|
||||
<CompassIcon
|
||||
color={changeOpacity(theme.sidebarText, Platform.select({android: 0.56, default: 0.72}))}
|
||||
name='arrow-left'
|
||||
|
||||
// @ts-expect-error cancel is not part of TextInput does exist in SearchBar
|
||||
onPress={searchRef.current?.cancel}
|
||||
size={24}
|
||||
testID={searchCancelButtonTestID}
|
||||
/>
|
||||
), [searchRef.current, theme]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
blur: () => {
|
||||
searchRef.current?.blur();
|
||||
},
|
||||
cancel: () => {
|
||||
// @ts-expect-error cancel is not part of TextInput does exist in SearchBar
|
||||
searchRef.current?.cancel();
|
||||
},
|
||||
clear: () => {
|
||||
searchRef.current?.clear();
|
||||
},
|
||||
focus: () => {
|
||||
searchRef.current?.focus();
|
||||
},
|
||||
|
||||
}), [searchRef]);
|
||||
|
||||
return (
|
||||
<SearchBar
|
||||
{...props}
|
||||
cancelButtonProps={props.cancelButtonProps || cancelButtonProps}
|
||||
cancelButtonTitle={props.cancelButtonTitle || intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
|
||||
cancelIcon={cancelIcon}
|
||||
|
||||
// @ts-expect-error clearIcon definition does not include a ReactElement
|
||||
clearIcon={clearIcon}
|
||||
containerStyle={[styles.containerStyle, props.containerStyle]}
|
||||
inputContainerStyle={[styles.inputContainerStyle, props.inputContainerStyle]}
|
||||
inputStyle={[styles.inputStyle, props.inputStyle]}
|
||||
returnKeyType='search'
|
||||
onCancel={onCancel}
|
||||
onClear={onClear}
|
||||
|
||||
// @ts-expect-error onChangeText type definition is wrong in elements
|
||||
onChangeText={onChangeText}
|
||||
placeholder={props.placeholder || intl.formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
|
||||
placeholderTextColor={props.placeholderTextColor || changeOpacity(theme.sidebarText, Platform.select({android: 0.56, default: 0.72}))}
|
||||
platform={Platform.select({android: 'android', default: 'ios'})}
|
||||
ref={searchRef}
|
||||
|
||||
// @ts-expect-error searchIcon definition does not include a ReactElement
|
||||
searchIcon={searchIcon}
|
||||
selectionColor={Platform.select({android: changeOpacity(theme.sidebarText, 0.24), default: theme.sidebarText})}
|
||||
testID={searchInputTestID}
|
||||
value={value}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
Search.displayName = 'SeachBar';
|
||||
|
||||
export default Search;
|
||||
@@ -8,14 +8,16 @@ import {Touchable, TouchableOpacity, TouchableWithoutFeedback, View, StyleProp,
|
||||
import {TouchableNativeFeedback} from 'react-native-gesture-handler';
|
||||
|
||||
type TouchableProps = Touchable & {
|
||||
testID: string;
|
||||
children: React.ReactNode | React.ReactNode[];
|
||||
underlayColor: string;
|
||||
type: 'native' | 'opacity' | 'none';
|
||||
borderlessRipple?: boolean;
|
||||
rippleRadius?: number;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
testID: string;
|
||||
type: 'native' | 'opacity' | 'none';
|
||||
underlayColor: string;
|
||||
}
|
||||
|
||||
const TouchableWithFeedbackAndroid = ({testID, children, underlayColor, type = 'native', ...props}: TouchableProps) => {
|
||||
const TouchableWithFeedbackAndroid = ({borderlessRipple = false, children, rippleRadius, testID, type = 'native', underlayColor, ...props}: TouchableProps) => {
|
||||
switch (type) {
|
||||
case 'native':
|
||||
return (
|
||||
@@ -23,7 +25,7 @@ const TouchableWithFeedbackAndroid = ({testID, children, underlayColor, type = '
|
||||
testID={testID}
|
||||
{...props}
|
||||
style={[props.style]}
|
||||
background={TouchableNativeFeedback.Ripple(underlayColor || '#fff', false)}
|
||||
background={TouchableNativeFeedback.Ripple(underlayColor || '#fff', borderlessRipple, rippleRadius)}
|
||||
>
|
||||
<View>
|
||||
{children}
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Platform} from 'react-native';
|
||||
|
||||
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, ios: 36})!;
|
||||
export const TABLET_SIDEBAR_WIDTH = 320;
|
||||
export const TEAM_SIDEBAR_WIDTH = 72;
|
||||
export const TABLET_HEADER_HEIGHT = 44;
|
||||
export const IOS_DEFAULT_HEADER_HEIGHT = 50;
|
||||
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 IOS_HEADER_SEARCH_INSET = 20;
|
||||
export const TABLET_HEADER_SEARCH_INSET = 28;
|
||||
export const ANDROID_HEADER_SEARCH_INSET = 11;
|
||||
|
||||
export default {
|
||||
BOTTOM_TAB_ICON_SIZE,
|
||||
@@ -13,6 +25,16 @@ export default {
|
||||
PROFILE_PICTURE_EMOJI_SIZE,
|
||||
DATA_SOURCE_USERS: 'users',
|
||||
DATA_SOURCE_CHANNELS: 'channels',
|
||||
SEARCH_INPUT_HEIGHT,
|
||||
TABLET_SIDEBAR_WIDTH,
|
||||
TEAM_SIDEBAR_WIDTH,
|
||||
TABLET_HEADER_HEIGHT,
|
||||
IOS_DEFAULT_HEADER_HEIGHT,
|
||||
ANDROID_DEFAULT_HEADER_HEIGHT,
|
||||
LARGE_HEADER_TITLE,
|
||||
HEADER_WITH_SEARCH_HEIGHT,
|
||||
HEADER_WITH_SUBTITLE,
|
||||
IOS_HEADER_SEARCH_INSET,
|
||||
TABLET_HEADER_SEARCH_INSET,
|
||||
ANDROID_HEADER_SEARCH_INSET,
|
||||
};
|
||||
|
||||
139
app/hooks/header.ts
Normal file
139
app/hooks/header.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useMemo} from 'react';
|
||||
import {NativeScrollEvent, Platform} from 'react-native';
|
||||
import Animated, {scrollTo, useAnimatedRef, useAnimatedScrollHandler, useSharedValue} from 'react-native-reanimated';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import ViewConstants from '@constants/view';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
|
||||
type HeaderScrollContext = {
|
||||
momentum?: number;
|
||||
start?: number;
|
||||
};
|
||||
|
||||
export const useDefaultHeaderHeight = () => {
|
||||
const isTablet = useIsTablet();
|
||||
|
||||
if (isTablet) {
|
||||
return ViewConstants.TABLET_HEADER_HEIGHT;
|
||||
}
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
return ViewConstants.IOS_DEFAULT_HEADER_HEIGHT;
|
||||
}
|
||||
|
||||
return ViewConstants.ANDROID_DEFAULT_HEADER_HEIGHT;
|
||||
};
|
||||
|
||||
export const useLargeHeaderHeight = (hasLargeTitle: boolean, hasSubtitle: boolean, hasSearch: boolean) => {
|
||||
const defaultHeight = useDefaultHeaderHeight();
|
||||
if (hasLargeTitle && hasSubtitle && !hasSearch) {
|
||||
return defaultHeight + ViewConstants.LARGE_HEADER_TITLE + ViewConstants.HEADER_WITH_SUBTITLE;
|
||||
} else if (hasLargeTitle && hasSearch) {
|
||||
return defaultHeight + ViewConstants.LARGE_HEADER_TITLE + ViewConstants.HEADER_WITH_SEARCH_HEIGHT;
|
||||
}
|
||||
|
||||
return defaultHeight + ViewConstants.LARGE_HEADER_TITLE;
|
||||
};
|
||||
|
||||
export const useHeaderHeight = (hasLargeTitle: boolean, hasSubtitle: boolean, hasSearch: boolean) => {
|
||||
const defaultHeight = useDefaultHeaderHeight();
|
||||
const largeHeight = useLargeHeaderHeight(hasLargeTitle, hasSubtitle, hasSearch);
|
||||
return useMemo(() => {
|
||||
return {
|
||||
defaultHeight,
|
||||
largeHeight,
|
||||
};
|
||||
}, [defaultHeight, hasSearch, largeHeight]);
|
||||
};
|
||||
|
||||
export const useCollapsibleHeader = <T>(isLargeTitle: boolean, hasSubtitle: boolean, hasSearch: boolean) => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const isTablet = useIsTablet();
|
||||
const animatedRef = useAnimatedRef<Animated.ScrollView>();
|
||||
const {largeHeight, defaultHeight} = useHeaderHeight(true, hasSubtitle, hasSearch);
|
||||
const scrollValue = useSharedValue(0);
|
||||
|
||||
function snapIfNeeded(dir: string, offset: number) {
|
||||
'worklet';
|
||||
if (dir === 'up' && offset < defaultHeight) {
|
||||
const diffHeight = largeHeight - defaultHeight;
|
||||
let position = 0;
|
||||
if (Platform.OS === 'ios') {
|
||||
const searchInset = isTablet ? ViewConstants.TABLET_HEADER_SEARCH_INSET : ViewConstants.IOS_HEADER_SEARCH_INSET;
|
||||
position = (diffHeight - (hasSearch ? -searchInset : insets.top));
|
||||
} else {
|
||||
position = hasSearch ? largeHeight + ViewConstants.ANDROID_HEADER_SEARCH_INSET : diffHeight;
|
||||
}
|
||||
scrollTo(animatedRef, 0, position!, true);
|
||||
} else if (dir === 'down') {
|
||||
let inset = 0;
|
||||
if (Platform.OS === 'ios') {
|
||||
const searchInset = isTablet ? ViewConstants.TABLET_HEADER_SEARCH_INSET : ViewConstants.IOS_HEADER_SEARCH_INSET;
|
||||
inset = defaultHeight + (hasSearch ? searchInset : 0);
|
||||
} else {
|
||||
inset = largeHeight + (hasSearch ? ViewConstants.ANDROID_HEADER_SEARCH_INSET : 0);
|
||||
}
|
||||
if (offset < inset) {
|
||||
scrollTo(animatedRef, 0, -insets.top, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onScroll = useAnimatedScrollHandler({
|
||||
onBeginDrag: (e: NativeScrollEvent, ctx: HeaderScrollContext) => {
|
||||
ctx.start = e.contentOffset.y;
|
||||
},
|
||||
onScroll: (e) => {
|
||||
scrollValue.value = e.contentOffset.y;
|
||||
},
|
||||
onEndDrag: (e, ctx) => {
|
||||
if (ctx.start !== undefined && Platform.OS === 'ios') {
|
||||
const dir = e.contentOffset.y < ctx.start ? 'down' : 'up';
|
||||
const offset = Math.abs(e.contentOffset.y);
|
||||
ctx.start = undefined;
|
||||
snapIfNeeded(dir, offset);
|
||||
}
|
||||
},
|
||||
onMomentumBegin: (e, ctx) => {
|
||||
ctx.momentum = Platform.OS === 'ios' ? e.contentOffset.y : ctx.start;
|
||||
},
|
||||
onMomentumEnd: (e, ctx) => {
|
||||
if (ctx.momentum !== undefined) {
|
||||
const offset = Math.abs(e.contentOffset.y);
|
||||
const searchInset = isTablet ? ViewConstants.TABLET_HEADER_SEARCH_INSET : ViewConstants.IOS_HEADER_SEARCH_INSET;
|
||||
const dir = e.contentOffset.y < ctx.momentum ? 'down' : 'up';
|
||||
ctx.momentum = undefined;
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
// This avoids snapping to the defaultHeight when already at the top and scrolling down
|
||||
if (dir === 'up' && offset === 0) {
|
||||
return;
|
||||
}
|
||||
snapIfNeeded(dir, offset);
|
||||
} else if (dir === 'down' && offset < (defaultHeight + (hasSearch ? searchInset : 0))) {
|
||||
scrollTo(animatedRef, 0, -insets.top, true);
|
||||
}
|
||||
}
|
||||
},
|
||||
}, [insets, defaultHeight, largeHeight]);
|
||||
|
||||
let searchPadding = 0;
|
||||
if (hasSearch) {
|
||||
searchPadding = ViewConstants.SEARCH_INPUT_HEIGHT +
|
||||
ViewConstants.IOS_HEADER_SEARCH_INSET +
|
||||
ViewConstants.ANDROID_HEADER_SEARCH_INSET;
|
||||
}
|
||||
|
||||
return {
|
||||
scrollPaddingTop: (isLargeTitle ? largeHeight : defaultHeight) + searchPadding,
|
||||
scrollRef: animatedRef as unknown as React.RefObject<T>,
|
||||
scrollValue,
|
||||
onScroll,
|
||||
};
|
||||
};
|
||||
|
||||
export default useHeaderHeight;
|
||||
@@ -2,14 +2,26 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useIsFocused, useNavigation} from '@react-navigation/native';
|
||||
import React from 'react';
|
||||
import {Text} from 'react-native';
|
||||
import React, {useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Text, FlatList, View, Platform} from 'react-native';
|
||||
import Animated, {useAnimatedStyle, withTiming} from 'react-native-reanimated';
|
||||
import {SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import Badge from '@components/badge';
|
||||
import NavigationHeader from '@components/navigation_header';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useCollapsibleHeader} from '@hooks/header';
|
||||
|
||||
import type {HeaderRightButton} from '@components/navigation_header/header';
|
||||
|
||||
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList);
|
||||
|
||||
const SearchScreen = () => {
|
||||
const nav = useNavigation();
|
||||
const isFocused = useIsFocused();
|
||||
const theme = useTheme();
|
||||
const intl = useIntl();
|
||||
const searchScreenIndex = 1;
|
||||
const stateIndex = nav.getState().index;
|
||||
|
||||
@@ -27,19 +39,109 @@ const SearchScreen = () => {
|
||||
};
|
||||
}, [isFocused, stateIndex]);
|
||||
|
||||
// Todo: Remove example
|
||||
const isLargeTitle = true;
|
||||
const subtitle = '';
|
||||
const title = 'Search';
|
||||
const hasSearch = true;
|
||||
const showBackButton = false;
|
||||
const addLeftComponent = false;
|
||||
const addRightButtons = false;
|
||||
let leftComponent;
|
||||
let rightButtons: HeaderRightButton[] | undefined;
|
||||
|
||||
if (addLeftComponent) {
|
||||
leftComponent = (
|
||||
<View>
|
||||
<Badge
|
||||
type='Small'
|
||||
visible={true}
|
||||
value={1}
|
||||
style={{top: 0, left: 2, position: 'relative'}}
|
||||
borderColor='transparent'
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (addRightButtons) {
|
||||
rightButtons = [{
|
||||
iconName: 'magnify',
|
||||
onPress: () => true,
|
||||
}, {
|
||||
iconName: Platform.select({android: 'dots-vertical', default: 'dots-horizontal'}),
|
||||
onPress: () => true,
|
||||
rippleRadius: 15,
|
||||
borderless: true,
|
||||
buttonType: 'opacity',
|
||||
}];
|
||||
}
|
||||
|
||||
const {scrollPaddingTop, scrollRef, scrollValue, onScroll} = useCollapsibleHeader<FlatList<string>>(isLargeTitle, Boolean(subtitle), hasSearch);
|
||||
const paddingTop = useMemo(() => ({paddingTop: scrollPaddingTop}), [scrollPaddingTop]);
|
||||
const data = [
|
||||
'Search Screen 1',
|
||||
'Search Screen 2',
|
||||
'Search Screen 3',
|
||||
'Search Screen 4',
|
||||
'Search Screen 5',
|
||||
];
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
style={{flex: 1, backgroundColor: 'blue'}}
|
||||
>
|
||||
<Animated.View
|
||||
style={[{flex: 1, justifyContent: 'center', alignItems: 'center'}, animated]}
|
||||
<>
|
||||
<NavigationHeader
|
||||
isLargeTitle={isLargeTitle}
|
||||
leftComponent={leftComponent}
|
||||
onBackPress={() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('BACK');
|
||||
}}
|
||||
rightButtons={rightButtons}
|
||||
showBackButton={showBackButton}
|
||||
subtitle={subtitle}
|
||||
title={title}
|
||||
hasSearch={hasSearch}
|
||||
scrollValue={scrollValue}
|
||||
forwardedRef={scrollRef}
|
||||
onChangeText={(text) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Search for value', text);
|
||||
}}
|
||||
onSubmitEditing={() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Execute search');
|
||||
}}
|
||||
blurOnSubmit={true}
|
||||
placeholder={intl.formatMessage({id: 'screen.search.placeholder', defaultMessage: 'Search messages & files'})}
|
||||
/>
|
||||
<SafeAreaView
|
||||
style={{flex: 1}}
|
||||
edges={['bottom', 'left', 'right']}
|
||||
>
|
||||
{isFocused &&
|
||||
<Text style={{fontSize: 20, color: '#fff'}}>{'Search Screen'}</Text>
|
||||
}
|
||||
</Animated.View>
|
||||
</SafeAreaView>
|
||||
<Animated.View style={[{flex: 1}, animated]}>
|
||||
<AnimatedFlatList
|
||||
contentContainerStyle={paddingTop}
|
||||
data={data}
|
||||
scrollToOverflowEnabled={true}
|
||||
showsVerticalScrollIndicator={false}
|
||||
indicatorStyle='black'
|
||||
onScroll={onScroll}
|
||||
scrollEventThrottle={16}
|
||||
ref={scrollRef}
|
||||
renderItem={({item, index}) => {
|
||||
const height = index === data.length - 1 ? undefined : 400;
|
||||
return (
|
||||
<View style={{flex: 1, alignItems: 'center'}}>
|
||||
<Text style={{fontSize: 20, color: theme.centerChannelColor, height}}>{item as string}</Text>
|
||||
</View>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Animated.View>
|
||||
</SafeAreaView>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchScreen;
|
||||
|
||||
|
||||
@@ -227,8 +227,10 @@
|
||||
"mobile.routes.sso": "Single Sign-On",
|
||||
"mobile.routes.table": "Table",
|
||||
"mobile.routes.user_profile": "Profile",
|
||||
"mobile.server_identifier.exists": "You are already connected to this server.",
|
||||
"mobile.server_link.unreachable_channel.error": "This link belongs to a deleted channel or to a channel to which you do not have access.",
|
||||
"mobile.server_link.unreachable_team.error": "This link belongs to a deleted team or to a team to which you do not have access.",
|
||||
"mobile.server_name.exists": "You are using this name for another server.",
|
||||
"mobile.server_ping_failed": "Cannot connect to the server.",
|
||||
"mobile.server_requires_client_certificate": "Server requires client certificate for authentication.",
|
||||
"mobile.server_upgrade.alert_description": "This server version is unsupported and users will be exposed to compatibility issues that cause crashes or severe bugs breaking core functionality of the app. Upgrading to server version {serverVersion} or later is required.",
|
||||
@@ -293,6 +295,7 @@
|
||||
"post_info.system": "System",
|
||||
"post_message_view.edited": "(edited)",
|
||||
"posts_view.newMsg": "New Messages",
|
||||
"screen.search.placeholder": "Search messages & files",
|
||||
"search_bar.search": "Search",
|
||||
"signup.email": "Email and Password",
|
||||
"signup.google": "Google Apps",
|
||||
|
||||
@@ -4,8 +4,6 @@ PODS:
|
||||
- BVLinearGradient (2.5.6):
|
||||
- React
|
||||
- DoubleConversion (1.1.6)
|
||||
- EXErrorRecovery (3.0.3):
|
||||
- ExpoModulesCore
|
||||
- EXFileSystem (13.0.3):
|
||||
- ExpoModulesCore
|
||||
- Expo (43.0.2):
|
||||
@@ -474,7 +472,6 @@ DEPENDENCIES:
|
||||
- boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`)
|
||||
- BVLinearGradient (from `../node_modules/react-native-linear-gradient`)
|
||||
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
|
||||
- EXErrorRecovery (from `../node_modules/expo-error-recovery/ios`)
|
||||
- EXFileSystem (from `../node_modules/expo-file-system/ios`)
|
||||
- Expo (from `../node_modules/expo/ios`)
|
||||
- ExpoModulesCore (from `../node_modules/expo-modules-core/ios`)
|
||||
@@ -583,8 +580,6 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native-linear-gradient"
|
||||
DoubleConversion:
|
||||
:podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec"
|
||||
EXErrorRecovery:
|
||||
:path: "../node_modules/expo-error-recovery/ios"
|
||||
EXFileSystem:
|
||||
:path: "../node_modules/expo-file-system/ios"
|
||||
Expo:
|
||||
@@ -751,7 +746,6 @@ SPEC CHECKSUMS:
|
||||
boost: a7c83b31436843459a1961bfd74b96033dc77234
|
||||
BVLinearGradient: e3aad03778a456d77928f594a649e96995f1c872
|
||||
DoubleConversion: 831926d9b8bf8166fd87886c4abab286c2422662
|
||||
EXErrorRecovery: ac2622400a32be84604591f70d0efff416e4b9a2
|
||||
EXFileSystem: 99aac7962c11c680681819dd9cbca24e20e5b1e7
|
||||
Expo: b66c9661cf2514b4adf67b8367178e8ed3eb9801
|
||||
ExpoModulesCore: c353a3bed60c3b83dbe63f134aaa7bf4733ffa16
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
diff --git a/node_modules/react-native-elements/dist/searchbar/SearchBar-android.js b/node_modules/react-native-elements/dist/searchbar/SearchBar-android.js
|
||||
index 1bfd2b4..820ccbc 100644
|
||||
index 1bfd2b4..436e870 100644
|
||||
--- a/node_modules/react-native-elements/dist/searchbar/SearchBar-android.js
|
||||
+++ b/node_modules/react-native-elements/dist/searchbar/SearchBar-android.js
|
||||
@@ -10,7 +10,7 @@ var __rest = (this && this.__rest) || function (s, e) {
|
||||
@@ -11,7 +11,14 @@ index 1bfd2b4..820ccbc 100644
|
||||
import { renderNode } from '../helpers';
|
||||
import Input from '../input/Input';
|
||||
import Icon from '../icons/Icon';
|
||||
@@ -74,18 +74,11 @@ class SearchBar extends Component {
|
||||
@@ -68,24 +68,17 @@ class SearchBar extends Component {
|
||||
};
|
||||
this.onBlur = (event) => {
|
||||
this.props.onBlur(event);
|
||||
- this.setState({ hasFocus: false });
|
||||
+ this.setState({ hasFocus: false, isEmpty: this.props.value === '' });
|
||||
};
|
||||
this.onChangeText = (text) => {
|
||||
this.props.onChangeText(text);
|
||||
this.setState({ isEmpty: text === '' });
|
||||
};
|
||||
@@ -31,7 +38,7 @@ index 1bfd2b4..820ccbc 100644
|
||||
render() {
|
||||
var _a;
|
||||
diff --git a/node_modules/react-native-elements/dist/searchbar/SearchBar-ios.js b/node_modules/react-native-elements/dist/searchbar/SearchBar-ios.js
|
||||
index 8fe90be..bb0e071 100644
|
||||
index 8fe90be..3daf517 100644
|
||||
--- a/node_modules/react-native-elements/dist/searchbar/SearchBar-ios.js
|
||||
+++ b/node_modules/react-native-elements/dist/searchbar/SearchBar-ios.js
|
||||
@@ -85,6 +85,11 @@ class SearchBar extends Component {
|
||||
@@ -46,20 +53,3 @@ index 8fe90be..bb0e071 100644
|
||||
render() {
|
||||
var _a, _b, _c, _d, _e, _f, _g;
|
||||
const _h = this.props, { theme, cancelButtonProps, cancelButtonTitle, clearIcon, containerStyle, leftIconContainerStyle, rightIconContainerStyle, inputContainerStyle, inputStyle, placeholderTextColor, showLoading, loadingProps, searchIcon, showCancel } = _h, attributes = __rest(_h, ["theme", "cancelButtonProps", "cancelButtonTitle", "clearIcon", "containerStyle", "leftIconContainerStyle", "rightIconContainerStyle", "inputContainerStyle", "inputStyle", "placeholderTextColor", "showLoading", "loadingProps", "searchIcon", "showCancel"]);
|
||||
@@ -167,7 +172,6 @@ const styles = StyleSheet.create({
|
||||
paddingBottom: 13,
|
||||
paddingTop: 13,
|
||||
flexDirection: 'row',
|
||||
- overflow: 'hidden',
|
||||
alignItems: 'center',
|
||||
},
|
||||
input: {
|
||||
@@ -177,7 +181,7 @@ const styles = StyleSheet.create({
|
||||
inputContainer: {
|
||||
borderBottomWidth: 0,
|
||||
borderRadius: 9,
|
||||
- minHeight: 36,
|
||||
+ minHeight: 30,
|
||||
marginLeft: 8,
|
||||
marginRight: 8,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user