forked from Ivasoft/mattermost-mobile
Compare commits
6 Commits
main
...
MM-48050-q
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d21cd26f9 | ||
|
|
8b9f378b70 | ||
|
|
39e3d5a22b | ||
|
|
ef76c6968e | ||
|
|
e1dd87ec75 | ||
|
|
c1cc640546 |
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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'})}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user