Compare commits

...

6 Commits

Author SHA1 Message Date
Jason Frerich
8d21cd26f9 wip 2022-10-31 08:55:42 -05:00
Jason Frerich
8b9f378b70 nits 2022-10-28 13:55:40 -05:00
Jason Frerich
39e3d5a22b fix lint 2022-10-28 13:46:11 -05:00
Jason Frerich
ef76c6968e remove comments 2022-10-28 13:40:41 -05:00
Jason Frerich
e1dd87ec75 focus the input when any modifier is added to the search input 2022-10-28 12:02:49 -05:00
Jason Frerich
c1cc640546 Add search tab icon 2022-10-28 09:49:54 -05:00
6 changed files with 135 additions and 50 deletions

View File

@@ -1,7 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import React, {forwardRef, useImperativeHandle, useRef} from 'react';
import {NativeSyntheticEvent, TextInputSelectionChangeEventData} from 'react-native';
import Animated, {useAnimatedStyle, useDerivedValue} from 'react-native-reanimated';
import {SEARCH_INPUT_HEIGHT, SEARCH_INPUT_MARGIN} from '@constants/view';
@@ -14,7 +15,7 @@ import Header, {HeaderRightButton} from './header';
import NavigationHeaderLargeTitle from './large';
import NavigationSearch from './search';
import type {SearchProps} from '@components/search';
import type {SearchProps, SearchRef} from '@components/search';
type Props = SearchProps & {
hasSearch?: boolean;
@@ -30,6 +31,8 @@ type Props = SearchProps & {
subtitle?: string;
subtitleCompanion?: React.ReactElement;
title?: string;
cursorPosition?: number;
selection?: {start: number; end?: number | undefined } | undefined;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
@@ -41,24 +44,37 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
},
}));
const NavigationHeader = ({
hasSearch = false,
isLargeTitle = false,
leftComponent,
onBackPress,
onTitlePress,
rightButtons,
scrollValue,
lockValue,
showBackButton,
subtitle,
subtitleCompanion,
title = '',
hideHeader,
...searchProps
}: Props) => {
const NavigationHeader = forwardRef<SearchRef, Props>((props: Props, ref) => {
const {
hasSearch = false,
isLargeTitle = false,
leftComponent,
onBackPress,
onTitlePress,
rightButtons,
scrollValue,
lockValue,
showBackButton,
subtitle,
subtitleCompanion,
title = '',
hideHeader,
} = props;
const searchProps = props;
const theme = useTheme();
const styles = getStyleSheet(theme);
const searchRef = useRef<SearchRef>(null);
useImperativeHandle(ref, () => ({
focus: () => {
searchRef.current?.focus?.();
},
onSelectionChange: (event: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => {
// @ts-expect-error cancel is not part of TextInput does exist in SearchBar
searchRef.current?.onSelectionChange?.(event);
},
}), [searchRef]);
const {largeHeight, defaultHeight, headerOffset} = useHeaderHeight();
const containerHeight = useAnimatedStyle(() => {
@@ -125,12 +141,14 @@ const NavigationHeader = ({
hideHeader={hideHeader}
theme={theme}
topStyle={searchTopStyle}
ref={searchRef}
/>
}
</Animated.View>
</>
);
};
});
NavigationHeader.displayName = 'NavHeader';
export default NavigationHeader;

View File

@@ -1,11 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useEffect, useMemo} from 'react';
import {DeviceEventEmitter, Keyboard, NativeSyntheticEvent, Platform, TextInputFocusEventData, ViewStyle} from 'react-native';
import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef} from 'react';
import {DeviceEventEmitter, Keyboard, NativeSyntheticEvent, Platform, TextInputFocusEventData, TextInputSelectionChangeEventData, ViewStyle} from 'react-native';
import Animated, {AnimatedStyleProp} from 'react-native-reanimated';
import Search, {SearchProps} from '@components/search';
import Search, {SearchProps, SearchRef} from '@components/search';
import {Events} from '@constants';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
@@ -31,14 +31,21 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
},
}));
const NavigationSearch = ({
hideHeader,
theme,
topStyle,
...searchProps
}: Props) => {
const NavigationSearch = forwardRef<SearchRef, Props>((searchProps: Props, ref) => {
const {theme, hideHeader, topStyle} = searchProps;
const searchRef = useRef<SearchRef>(null);
const styles = getStyleSheet(theme);
useImperativeHandle(ref, () => ({
focus: () => {
searchRef.current?.focus?.();
},
onSelectionChange: (event: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => {
// @ts-expect-error cancel is not part of TextInput does exist in SearchBar
searchRef.current?.onSelectionChange(event.nativeEvent);
},
}), [searchRef]);
const cancelButtonProps: SearchProps['cancelButtonProps'] = useMemo(() => ({
buttonTextStyle: {
color: changeOpacity(theme.sidebarText, 0.72),
@@ -83,10 +90,12 @@ const NavigationSearch = ({
placeholderTextColor={changeOpacity(theme.sidebarText, Platform.select({android: 0.56, default: 0.72}))}
searchIconColor={theme.sidebarText}
selectionColor={theme.sidebarText}
ref={searchRef}
/>
</Animated.View>
);
};
});
NavigationSearch.displayName = 'NavSearch';
export default NavigationSearch;

View File

@@ -6,7 +6,7 @@
import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import {ActivityIndicatorProps, Keyboard, Platform, StyleProp, TextInput, TextInputProps, TextStyle, TouchableOpacityProps, ViewStyle} from 'react-native';
import {ActivityIndicatorProps, Keyboard, NativeSyntheticEvent, Platform, StyleProp, TextInput, TextInputProps, TextInputSelectionChangeEventData, TextStyle, TouchableOpacityProps, ViewStyle} from 'react-native';
import {SearchBar} from 'react-native-elements';
import CompassIcon from '@components/compass_icon';
@@ -16,6 +16,8 @@ import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
export type SearchProps = TextInputProps & {
cursorPosition: number;
selection?: {start: number; end?: number | undefined } | undefined;
cancelIcon?: React.ReactElement;
cancelButtonProps?: Partial<TouchableOpacityProps> & {
buttonStyle?: StyleProp<ViewStyle>;
@@ -42,11 +44,12 @@ export type SearchProps = TextInputProps & {
showLoading?: boolean;
};
type SearchRef = {
blur: () => void;
cancel: () => void;
clear: () => void;
focus: () => void;
export type SearchRef = {
blur?: () => void;
cancel?: () => void;
clear?: () => void;
focus?: () => void;
onSelectionChange: (event: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => void;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
@@ -80,6 +83,32 @@ const Search = forwardRef<SearchRef, SearchProps>((props: SearchProps, ref) => {
const searchClearButtonTestID = `${props.testID}.search.clear.button`;
const searchCancelButtonTestID = `${props.testID}.search.cancel.button`;
const searchInputTestID = `${props.testID}.search.input`;
const [localCursorPosition, setLocalCursorPosition] = useState(props.cursorPosition);
useEffect(() => {
if (localCursorPosition !== props.cursorPosition) {
setLocalCursorPosition(props.cursorPosition);
}
// setLocalSelection({start: props.cursorPosition});
}, [props.cursorPosition]);
const onChangeText = useCallback((text: string) => {
setValue(text);
props.onChangeText?.(text);
}, [props.onChangeText, value, props.selection]);
const onSelectionChange = useCallback((event: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => {
// const onSelectionChange = useCallback(({nativeEvent: {selection, text}}) => {
console.log('<><> onSelectionChange - selection', event.nativeEvent.selection);
setLocalCursorPosition(event.nativeEvent.selection.start);
// setLocalSelection(selection);
}, [props.selection]);
// console.log('props.selection', props.selection, 'localSelection', localSelection);
// console.log('props.selection', props.selection);
const onCancel = useCallback(() => {
Keyboard.dismiss();
@@ -92,11 +121,6 @@ const Search = forwardRef<SearchRef, SearchProps>((props: SearchProps, ref) => {
props.onClear?.();
}, [props.onClear]);
const onChangeText = useCallback((text: string) => {
setValue(text);
props.onChangeText?.(text);
}, [props.onChangeText]);
const cancelButtonProps = useMemo(() => ({
buttonTextStyle: {
color: changeOpacity(theme.centerChannelColor, 0.72),
@@ -151,7 +175,13 @@ const Search = forwardRef<SearchRef, SearchProps>((props: SearchProps, ref) => {
focus: () => {
searchRef.current?.focus();
},
onSelectionChange: (event: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => {
// console.log('. IN HERE!');
console.log('event?.nativeEvent.selection', event);
// @ts-expect-error cancel is not part of TextInput does exist in SearchBar
searchRef.current?.onSelectionChange?.(event);
},
}), [searchRef]);
return (
@@ -172,6 +202,8 @@ const Search = forwardRef<SearchRef, SearchProps>((props: SearchProps, ref) => {
// @ts-expect-error onChangeText type definition is wrong in elements
onChangeText={onChangeText}
selection={{start: localCursorPosition}}
onSelectionChange={onSelectionChange}
placeholder={props.placeholder || intl.formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
placeholderTextColor={props.placeholderTextColor || changeOpacity(theme.centerChannelColor, Platform.select({android: 0.56, default: 0.72}))}
platform={Platform.select({android: 'android', default: 'ios'})}

View File

@@ -20,10 +20,9 @@ import Account from './account';
import ChannelList from './channel_list';
import RecentMentions from './recent_mentions';
import SavedMessages from './saved_messages';
import Search from './search';
import TabBar from './tab_bar';
// import Search from './search';
import type {LaunchProps} from '@typings/launch';
if (Platform.OS === 'ios') {
@@ -125,11 +124,11 @@ export default function HomeScreen(props: HomeProps) {
>
{() => <ChannelList {...props}/>}
</Tab.Screen>
{/* <Tab.Screen
<Tab.Screen
name={Screens.SEARCH}
component={Search}
options={{unmountOnBlur: false, lazy: true, tabBarTestID: 'tab_bar.search.tab', freezeOnBlur: true}}
/> */}
/>
<Tab.Screen
name={Screens.MENTIONS}
component={RecentMentions}

View File

@@ -21,7 +21,7 @@ export type ModifierItem = {
type Props = {
item: ModifierItem;
setSearchValue: (value: string) => void;
setSearchValue: (value: string, cursorOffset?: number) => void;
searchValue?: string;
}
@@ -40,7 +40,8 @@ const Modifier = ({item, searchValue, setSearchValue}: Props) => {
newValue = `${searchValue} ${modifierTerm}`;
}
setSearchValue(newValue);
const cursorPosition = item.testID === 'search.phrases_section' ? -1 : undefined;
setSearchValue(newValue, cursorPosition);
});
return (

View File

@@ -4,7 +4,7 @@
import {useIsFocused, useNavigation} from '@react-navigation/native';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import {FlatList, LayoutChangeEvent, Platform, StyleSheet, ViewProps} from 'react-native';
import {FlatList, LayoutChangeEvent, NativeSyntheticEvent, Platform, StyleSheet, TextInputSelectionChangeEventData, ViewProps} from 'react-native';
import Animated, {useAnimatedStyle, useDerivedValue, withTiming} from 'react-native-reanimated';
import {Edge, SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context';
@@ -17,6 +17,7 @@ import FreezeScreen from '@components/freeze_screen';
import Loading from '@components/loading';
import NavigationHeader from '@components/navigation_header';
import RoundedHeaderContext from '@components/rounded_header_context';
import {SearchRef} from '@components/search';
import {BOTTOM_TAB_HEIGHT} from '@constants/view';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
@@ -84,6 +85,8 @@ const SearchScreen = ({teamId}: Props) => {
const clearRef = useRef<boolean>(false);
const cancelRef = useRef<boolean>(false);
const searchRef = useRef<SearchRef>(null);
const [cursorPosition, setCursorPosition] = useState(searchTerm?.length || 0);
const [searchValue, setSearchValue] = useState<string>(searchTerm || '');
const [searchTeamId, setSearchTeamId] = useState<string>(teamId);
@@ -139,10 +142,28 @@ const SearchScreen = ({teamId}: Props) => {
onSnap(0);
}, [resetToInitial]);
const handleTextChange = useCallback((newValue: string) => {
const handleTextChange = useCallback((newValue: string, cursorOffset?: undefined) => {
searchRef.current?.focus?.();
const newCursorPos = (newValue.length + (cursorOffset || 0));
// console.log(
// 'newValue', newValue,
// 'newVal.len', newValue.length,
// 'offset', cursorOffset,
// 'newCursorPos', newCursorPos,
// );
setCursorPosition(newCursorPos);
setSearchValue(newValue);
setCursorPosition(newValue.length);
}, []);
}, [cursorPosition, searchRef]);
const onSelectionChange = (e: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => {
console.log('e.nativeEvent', e.nativeEvent);
console.log('. IN HERE!');
};
searchRef.current?.onSelectionChange?.(onSelectionChange);
// console.log('searchValue', searchValue, 'cursorPosition', cursorPosition);
const handleLoading = useCallback((show: boolean) => {
(showResults ? setResultsLoading : setLoading)(show);
@@ -313,11 +334,16 @@ const SearchScreen = ({teamId}: Props) => {
hideHeader={hideHeader}
onChangeText={handleTextChange}
onSubmitEditing={onSubmit}
onSelectionChange={onSelectionChange}
blurOnSubmit={true}
placeholder={intl.formatMessage({id: 'screen.search.placeholder', defaultMessage: 'Search messages & files'})}
onClear={handleClearSearch}
onCancel={handleCancelSearch}
defaultValue={searchValue}
//selection={selection}
cursorPosition={cursorPosition}
ref={searchRef}
/>
<SafeAreaView
style={styles.flex}