forked from Ivasoft/mattermost-mobile
[Gekidou MM-44927] Add Recent Searches Component (#6454)
* initial check in * add search value to memoized dependencies in modifier component * ignore the back press * UI adjustments from PR feedback * initial commit * recent search are getting rendered from WDB * search terms from the search bar are getting added * can delete recent searches from WDB from recent searches Options * will now add new ters to the table and recreate existing terms with new timestamp * push for scrollview * use flatlist instead of scrolview * s/deleteRecentTeamSearchById/removeSearchFromTeamSearchHistory/ * s/addRecentTeamSearch/addSearchToTeamSearchHistory/ * Fix search to use a flatlist and remove douplicate reference * fix eslint * Fix android autoscroll search field to the top * limit the number of saved searches to 20 for a team. return the results a team Search History sorted by createdAt * set display to term for now * clean up * clean up * extract as constant * move styles to the top * always update the created_at value in the database. * remove unused function * - remove useMemo of recent - set or remove props on AnimatedFlatlist * styling adjustments * styling changes * divider takes up 1ox so only need 15px margin to get the 16px total to the neighboring veritcal views * update compassIcon to match figma design * update divider opacity to match figma design * update styling from UX PR requests * increase close button to touchable area of 40x40 and adjust menuitem container * use logError instead of console.log and trowing an error * remove surrounding parenthesis * There is only one record, so no need to batch. Just call destroyPermanently. * call destroyPermanently directly * when not useing the onScroll callback you don't need to set the scrollEventThrottle * set the max searches saved to 15 * no need to memoize * shoud be a function call * batch the add/update with the delete of the oldest model * Minor improvements * Fix bug when hitting back on search screen * Fix long channel names in search results Co-authored-by: Elias Nahum <nahumhbl@gmail.com> Co-authored-by: Daniel Espino García <larkox@gmail.com>
This commit is contained in:
@@ -2,9 +2,11 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {prepareDeleteTeam, getMyTeamById, removeTeamFromTeamHistory} from '@queries/servers/team';
|
||||
import {prepareDeleteTeam, getMyTeamById, queryTeamSearchHistoryByTeamId, removeTeamFromTeamHistory, getTeamSearchHistoryById} from '@queries/servers/team';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
import type Model from '@nozbe/watermelondb/Model';
|
||||
|
||||
export async function removeUserFromTeam(serverUrl: string, teamId: string) {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
@@ -30,3 +32,56 @@ export async function removeUserFromTeam(serverUrl: string, teamId: string) {
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function addSearchToTeamSearchHistory(serverUrl: string, teamId: string, terms: string) {
|
||||
const MAX_TEAM_SEARCHES = 15;
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const newSearch: TeamSearchHistory = {
|
||||
created_at: Date.now(),
|
||||
display_term: terms,
|
||||
term: terms,
|
||||
team_id: teamId,
|
||||
};
|
||||
|
||||
const models: Model[] = [];
|
||||
const searchModels = await operator.handleTeamSearchHistory({teamSearchHistories: [newSearch], prepareRecordsOnly: true});
|
||||
const searchModel = searchModels[0];
|
||||
|
||||
models.push(searchModel);
|
||||
|
||||
// determine if need to delete the oldest entry
|
||||
if (searchModel._raw._changed !== 'created_at') {
|
||||
const teamSearchHistory = await queryTeamSearchHistoryByTeamId(database, teamId).fetch();
|
||||
if (teamSearchHistory.length > MAX_TEAM_SEARCHES) {
|
||||
const lastSearches = teamSearchHistory.slice(MAX_TEAM_SEARCHES);
|
||||
for (const lastSearch of lastSearches) {
|
||||
models.push(lastSearch.prepareDestroyPermanently());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await operator.batchRecords(models);
|
||||
return {searchModel};
|
||||
} catch (error) {
|
||||
logError('Failed addSearchToTeamSearchHistory', error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeSearchFromTeamSearchHistory(serverUrl: string, id: string) {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const teamSearch = await getTeamSearchHistoryById(database, id);
|
||||
if (teamSearch) {
|
||||
await database.write(async () => {
|
||||
await teamSearch.destroyPermanently();
|
||||
});
|
||||
}
|
||||
return {teamSearch};
|
||||
} catch (error) {
|
||||
logError('Failed removeSearchFromTeamSearchHistory', error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {ActivityIndicator, View, ViewStyle} from 'react-native';
|
||||
import {ActivityIndicator, StyleProp, View, ViewStyle} from 'react-native';
|
||||
|
||||
import {useTheme} from '@context/theme';
|
||||
|
||||
type LoadingProps = {
|
||||
containerStyle?: ViewStyle;
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
size?: number | 'small' | 'large';
|
||||
color?: string;
|
||||
themeColor?: keyof Theme;
|
||||
|
||||
@@ -22,10 +22,12 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
...typography('Body', 75, 'SemiBold'),
|
||||
color: theme.centerChannelColor,
|
||||
marginRight: 5,
|
||||
flexShrink: 1,
|
||||
},
|
||||
teamContainer: {
|
||||
borderColor: theme.centerChannelColor,
|
||||
borderLeftWidth: StyleSheet.hairlineWidth,
|
||||
flexShrink: 1,
|
||||
},
|
||||
team: {
|
||||
...typography('Body', 75, 'Light'),
|
||||
@@ -52,6 +54,7 @@ function ChannelInfo({channelName, teamName, testID}: Props) {
|
||||
<Text
|
||||
style={styles.channel}
|
||||
testID='channel_display_name'
|
||||
numberOfLines={1}
|
||||
>
|
||||
{channelName}
|
||||
</Text>
|
||||
@@ -60,6 +63,7 @@ function ChannelInfo({channelName, teamName, testID}: Props) {
|
||||
<Text
|
||||
style={styles.team}
|
||||
testID='team_display_name'
|
||||
numberOfLines={1}
|
||||
>
|
||||
{teamName}
|
||||
</Text>
|
||||
|
||||
@@ -22,12 +22,14 @@ import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import type MyTeamModel from '@typings/database/models/servers/my_team';
|
||||
import type TeamModel from '@typings/database/models/servers/team';
|
||||
import type TeamChannelHistoryModel from '@typings/database/models/servers/team_channel_history';
|
||||
import type TeamSearchHistoryModel from '@typings/database/models/servers/team_search_history';
|
||||
|
||||
const {
|
||||
MY_CHANNEL,
|
||||
MY_TEAM,
|
||||
TEAM,
|
||||
TEAM_CHANNEL_HISTORY,
|
||||
TEAM_SEARCH_HISTORY,
|
||||
} = DatabaseConstants.MM_TABLES.SERVER;
|
||||
|
||||
export const getCurrentTeam = async (database: Database) => {
|
||||
@@ -322,6 +324,15 @@ export const getTeamById = async (database: Database, teamId: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getTeamSearchHistoryById = async (database: Database, id: string) => {
|
||||
try {
|
||||
const teamSearchHistory = await database.get<TeamSearchHistoryModel>(TEAM_SEARCH_HISTORY).find(id);
|
||||
return teamSearchHistory;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const observeTeam = (database: Database, teamId: string) => {
|
||||
return database.get<TeamModel>(TEAM).query(Q.where('id', teamId), Q.take(1)).observe().pipe(
|
||||
switchMap((result) => (result.length ? result[0].observe() : of$(undefined))),
|
||||
@@ -356,6 +367,12 @@ export const getTeamByName = async (database: Database, teamName: string) => {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const queryTeamSearchHistoryByTeamId = (database: Database, teamId: string) => {
|
||||
return database.get<TeamSearchHistoryModel>(TEAM_SEARCH_HISTORY).query(
|
||||
Q.where('team_id', teamId),
|
||||
Q.sortBy('created_at', Q.desc));
|
||||
};
|
||||
|
||||
export const queryMyTeams = (database: Database) => {
|
||||
return database.get<MyTeamModel>(MY_TEAM).query();
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
import React, {useCallback, useMemo, useState} from 'react';
|
||||
import {IntlShape, useIntl} from 'react-intl';
|
||||
import {View} from 'react-native';
|
||||
import Animated, {useSharedValue, useAnimatedStyle, withTiming} from 'react-native-reanimated';
|
||||
|
||||
import FormattedText from '@components/formatted_text';
|
||||
@@ -97,14 +96,12 @@ const getModifiersSectionsData = (intl: IntlShape): ModifierItem[] => {
|
||||
};
|
||||
|
||||
type Props = {
|
||||
scrollPaddingTop: number;
|
||||
setSearchValue: (value: string) => void;
|
||||
searchValue?: string;
|
||||
}
|
||||
const SearchModifiers = ({scrollPaddingTop, searchValue, setSearchValue}: Props) => {
|
||||
const SearchModifiers = ({searchValue, setSearchValue}: Props) => {
|
||||
const theme = useTheme();
|
||||
const intl = useIntl();
|
||||
const paddingTop = useMemo(() => ({paddingTop: scrollPaddingTop, flexGrow: 1}), [scrollPaddingTop]);
|
||||
|
||||
const [showMore, setShowMore] = useState(false);
|
||||
const show = useSharedValue(3 * MODIFIER_LABEL_HEIGHT);
|
||||
@@ -137,7 +134,7 @@ const SearchModifiers = ({scrollPaddingTop, searchValue, setSearchValue}: Props)
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={paddingTop}>
|
||||
<>
|
||||
<FormattedText
|
||||
style={styles.title}
|
||||
id={'screen.search.modifier.header'}
|
||||
@@ -150,7 +147,7 @@ const SearchModifiers = ({scrollPaddingTop, searchValue, setSearchValue}: Props)
|
||||
onPress={handleShowMore}
|
||||
showMore={showMore}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
27
app/screens/home/search/recent_searches/index.tsx
Normal file
27
app/screens/home/search/recent_searches/index.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import compose from 'lodash/fp/compose';
|
||||
|
||||
import {queryTeamSearchHistoryByTeamId} from '@queries/servers/team';
|
||||
|
||||
import RecentSearches from './recent_searches';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
type EnhanceProps = WithDatabaseArgs & {
|
||||
teamId: string;
|
||||
}
|
||||
|
||||
const enhance = withObservables(['teamId'], ({database, teamId}: EnhanceProps) => {
|
||||
return {
|
||||
recentSearches: queryTeamSearchHistoryByTeamId(database, teamId).observe(),
|
||||
};
|
||||
});
|
||||
|
||||
export default compose(
|
||||
withDatabase,
|
||||
enhance,
|
||||
)(RecentSearches);
|
||||
97
app/screens/home/search/recent_searches/recent_item.tsx
Normal file
97
app/screens/home/search/recent_searches/recent_item.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {Text, TouchableOpacity, View} from 'react-native';
|
||||
|
||||
import {removeSearchFromTeamSearchHistory} from '@actions/local/team';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import MenuItem from '@components/menu_item';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import type TeamSearchHistoryModel from '@typings/database/models/servers/team_search_history';
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
marginVertical: -16,
|
||||
paddingLeft: 20,
|
||||
paddingRight: 6,
|
||||
alignItems: 'center',
|
||||
height: 48,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
remove: {
|
||||
height: 40,
|
||||
width: 40,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
term: {
|
||||
flex: 1,
|
||||
marginLeft: 16,
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Body', 200, 'Regular'),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export type RecentItemType = {
|
||||
terms: string;
|
||||
isOrSearch: boolean;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
setRecentValue: (value: string) => void;
|
||||
item: TeamSearchHistoryModel;
|
||||
}
|
||||
|
||||
const RecentItem = ({item, setRecentValue}: Props) => {
|
||||
const theme = useTheme();
|
||||
const style = getStyleFromTheme(theme);
|
||||
const testID = 'search.recent_item';
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
setRecentValue(item.term);
|
||||
}, [item, setRecentValue]);
|
||||
|
||||
const handleRemove = useCallback(async () => {
|
||||
await removeSearchFromTeamSearchHistory(serverUrl, item.id);
|
||||
}, [item.id]);
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
testID={testID}
|
||||
onPress={handlePress}
|
||||
labelComponent={
|
||||
<View style={style.container}>
|
||||
<CompassIcon
|
||||
name='clock-outline'
|
||||
size={24}
|
||||
color={changeOpacity(theme.centerChannelColor, 0.56)}
|
||||
/>
|
||||
<Text style={style.term}>{item.term}</Text>
|
||||
<TouchableOpacity
|
||||
onPress={handleRemove}
|
||||
style={style.remove}
|
||||
testID={`${testID}.remove.button`}
|
||||
>
|
||||
<CompassIcon
|
||||
name='close'
|
||||
size={18}
|
||||
color={changeOpacity(theme.centerChannelColor, 0.64)}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
}
|
||||
separator={false}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecentItem;
|
||||
80
app/screens/home/search/recent_searches/recent_searches.tsx
Normal file
80
app/screens/home/search/recent_searches/recent_searches.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {FlatList, View} from 'react-native';
|
||||
import Animated from 'react-native-reanimated';
|
||||
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import RecentItem from './recent_item';
|
||||
|
||||
import type TeamSearchHistoryModel from '@typings/database/models/servers/team_search_history';
|
||||
|
||||
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList);
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
divider: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
|
||||
height: 1,
|
||||
marginVertical: 15,
|
||||
marginHorizontal: 20,
|
||||
},
|
||||
title: {
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 12,
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Heading', 300, 'SemiBold'),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
type Props = {
|
||||
setRecentValue: (value: string) => void;
|
||||
recentSearches: TeamSearchHistoryModel[];
|
||||
}
|
||||
|
||||
const RecentSearches = ({setRecentValue, recentSearches}: Props) => {
|
||||
const theme = useTheme();
|
||||
const {formatMessage} = useIntl();
|
||||
const styles = getStyleFromTheme(theme);
|
||||
|
||||
const renderRecentItem = useCallback(({item}) => {
|
||||
return (
|
||||
<RecentItem
|
||||
item={item}
|
||||
setRecentValue={setRecentValue}
|
||||
/>
|
||||
);
|
||||
}, [setRecentValue]);
|
||||
|
||||
const header = (
|
||||
<>
|
||||
<View style={styles.divider}/>
|
||||
<FormattedText
|
||||
style={styles.title}
|
||||
id={'screen.search.recent.header'}
|
||||
defaultMessage={formatMessage({id: 'mobile.search.recent_title', defaultMessage: 'Recent searches'})}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<AnimatedFlatList
|
||||
data={recentSearches}
|
||||
keyboardShouldPersistTaps='always'
|
||||
keyboardDismissMode='interactive'
|
||||
ListHeaderComponent={header}
|
||||
renderItem={renderRecentItem}
|
||||
testID='search.recents_list'
|
||||
removeClippedSubviews={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecentSearches;
|
||||
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import {StyleSheet, FlatList, ListRenderItemInfo, NativeScrollEvent, NativeSyntheticEvent, StyleProp, View, ViewStyle} from 'react-native';
|
||||
import {StyleSheet, FlatList, ListRenderItemInfo, StyleProp, View, ViewStyle} from 'react-native';
|
||||
import Animated from 'react-native-reanimated';
|
||||
|
||||
import File from '@components/files/file';
|
||||
@@ -20,8 +20,6 @@ import {getDateForDateLine, isDateLine, selectOrderedPosts} from '@utils/post_li
|
||||
import {TabTypes, TabType} from '@utils/search';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
|
||||
import Loader from './loader';
|
||||
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
|
||||
@@ -40,31 +38,24 @@ type Props = {
|
||||
fileChannels: ChannelModel[];
|
||||
fileInfos: FileInfo[];
|
||||
isTimezoneEnabled: boolean;
|
||||
loading: boolean;
|
||||
onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
|
||||
posts: PostModel[];
|
||||
publicLinkEnabled: boolean;
|
||||
scrollPaddingTop: number;
|
||||
scrollRef: React.RefObject<FlatList>;
|
||||
searchValue: string;
|
||||
selectedTab: TabType;
|
||||
}
|
||||
|
||||
const emptyList: FileInfo[] | Array<string | PostModel> = [];
|
||||
const galleryIdentifier = 'search-files-location';
|
||||
|
||||
const Results = ({
|
||||
const SearchResults = ({
|
||||
canDownloadFiles,
|
||||
currentTimezone,
|
||||
fileChannels,
|
||||
fileInfos,
|
||||
isTimezoneEnabled,
|
||||
loading,
|
||||
onScroll,
|
||||
posts,
|
||||
publicLinkEnabled,
|
||||
scrollPaddingTop,
|
||||
scrollRef,
|
||||
searchValue,
|
||||
selectedTab,
|
||||
}: Props) => {
|
||||
@@ -188,21 +179,16 @@ const Results = ({
|
||||
]);
|
||||
|
||||
const noResults = useMemo(() => {
|
||||
if (loading) {
|
||||
return (<Loader/>);
|
||||
}
|
||||
return (
|
||||
<NoResultsWithTerm
|
||||
term={searchValue}
|
||||
type={selectedTab}
|
||||
/>
|
||||
);
|
||||
}, [searchValue, loading, selectedTab]);
|
||||
}, [searchValue, selectedTab]);
|
||||
|
||||
let data;
|
||||
if (loading || !searchValue) {
|
||||
data = emptyList;
|
||||
} else if (selectedTab === TabTypes.MESSAGES) {
|
||||
if (selectedTab === TabTypes.MESSAGES) {
|
||||
data = orderedPosts;
|
||||
} else {
|
||||
data = orderedFilesForGallery;
|
||||
@@ -220,13 +206,11 @@ const Results = ({
|
||||
renderItem={renderItem}
|
||||
contentContainerStyle={paddingTop}
|
||||
nestedScrollEnabled={true}
|
||||
onScroll={onScroll}
|
||||
removeClippedSubviews={true}
|
||||
ref={scrollRef}
|
||||
style={containerStyle}
|
||||
testID='search_results.post_list.flat_list'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Results;
|
||||
export default SearchResults;
|
||||
|
||||
@@ -2,32 +2,38 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useIsFocused, useNavigation} from '@react-navigation/native';
|
||||
import {debounce} from 'lodash';
|
||||
import React, {useCallback, useState, useEffect} from 'react';
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {FlatList, StyleSheet} from 'react-native';
|
||||
import Animated, {useAnimatedStyle, withTiming} from 'react-native-reanimated';
|
||||
import {Edge, SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import {addSearchToTeamSearchHistory} from '@actions/local/team';
|
||||
import {searchPosts, searchFiles} from '@actions/remote/search';
|
||||
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 {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useCollapsibleHeader} from '@hooks/header';
|
||||
import {FileFilter, FileFilters, filterFileExtensions} from '@utils/file';
|
||||
import {TabTypes, TabType} from '@utils/search';
|
||||
|
||||
import Modifiers from './modifiers';
|
||||
import RecentSearches from './recent_searches';
|
||||
import Results from './results';
|
||||
import Header from './results/header';
|
||||
|
||||
const EDGES: Edge[] = ['bottom', 'left', 'right'];
|
||||
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList);
|
||||
|
||||
const emptyFileResults: FileInfo[] = [];
|
||||
const emptyPostResults: string[] = [];
|
||||
const emptyChannelIds: string[] = [];
|
||||
|
||||
const dummyData = [1];
|
||||
|
||||
type Props = {
|
||||
teamId: string;
|
||||
}
|
||||
@@ -36,16 +42,31 @@ const styles = StyleSheet.create({
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
loading: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
const getSearchParams = (terms: string, filterValue?: FileFilter) => {
|
||||
const fileExtensions = filterFileExtensions(filterValue);
|
||||
const extensionTerms = fileExtensions ? ' ' + fileExtensions : '';
|
||||
return {
|
||||
terms: terms + extensionTerms,
|
||||
is_or_search: true,
|
||||
};
|
||||
};
|
||||
|
||||
const SearchScreen = ({teamId}: Props) => {
|
||||
const nav = useNavigation();
|
||||
const isFocused = useIsFocused();
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const searchScreenIndex = 1;
|
||||
const stateIndex = nav.getState().index;
|
||||
const serverUrl = useServerUrl();
|
||||
const {searchTerm} = nav.getState().routes[stateIndex].params;
|
||||
const searchTerm = (nav.getState().routes[stateIndex].params as any)?.searchTerm;
|
||||
|
||||
const [searchValue, setSearchValue] = useState<string>(searchTerm);
|
||||
const [selectedTab, setSelectedTab] = useState<TabType>(TabTypes.MESSAGES);
|
||||
@@ -59,62 +80,117 @@ const SearchScreen = ({teamId}: Props) => {
|
||||
const [fileInfos, setFileInfos] = useState<FileInfo[]>(emptyFileResults);
|
||||
const [fileChannelIds, setFileChannelIds] = useState<string[]>([]);
|
||||
|
||||
const getSearchParams = useCallback((filterValue?: FileFilter) => {
|
||||
const terms = filterValue ? lastSearchedValue : searchValue;
|
||||
const fileExtensions = filterFileExtensions(filterValue || filter);
|
||||
const extensionTerms = fileExtensions ? ' ' + fileExtensions : '';
|
||||
return {
|
||||
terms: terms + extensionTerms,
|
||||
is_or_search: true,
|
||||
};
|
||||
}, [filter, lastSearchedValue, searchValue]);
|
||||
|
||||
const handleSearch = useCallback((debounce(async () => {
|
||||
// execute the search for the text in the navigation text box
|
||||
// handle recent searches
|
||||
// - add recent if doesn't exist
|
||||
// - updated recent createdAt if exists??
|
||||
|
||||
const searchParams = getSearchParams();
|
||||
if (!searchParams.terms) {
|
||||
handleClearSearch();
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setShowResults(true);
|
||||
setFilter(FileFilters.ALL);
|
||||
setLastSearchedValue(searchValue);
|
||||
const [postResults, {files, channels}] = await Promise.all([
|
||||
searchPosts(serverUrl, searchParams),
|
||||
searchFiles(serverUrl, teamId, searchParams),
|
||||
]);
|
||||
|
||||
setFileInfos(files?.length ? files : emptyFileResults);
|
||||
setPostIds(postResults?.order?.length ? postResults.order : emptyPostResults);
|
||||
setFileChannelIds(channels?.length ? channels : emptyChannelIds);
|
||||
setLoading(false);
|
||||
})), [searchValue]);
|
||||
const handleSearch = useRef<(term: string) => void>();
|
||||
|
||||
const onSnap = (offset: number) => {
|
||||
scrollRef.current?.scrollToOffset({offset, animated: true});
|
||||
};
|
||||
|
||||
const {scrollPaddingTop, scrollRef, scrollValue, onScroll, headerHeight, hideHeader} = useCollapsibleHeader<FlatList>(true, onSnap);
|
||||
|
||||
const onSubmit = useCallback(() => {
|
||||
handleSearch.current?.(searchValue);
|
||||
}, [searchValue]);
|
||||
|
||||
const handleClearSearch = useCallback(() => {
|
||||
setSearchValue('');
|
||||
setLastSearchedValue('');
|
||||
setFilter(FileFilters.ALL);
|
||||
}, []);
|
||||
|
||||
const handleCancelSearch = useCallback(() => {
|
||||
handleClearSearch();
|
||||
setShowResults(false);
|
||||
}, [handleClearSearch, showResults]);
|
||||
|
||||
useEffect(() => {
|
||||
handleSearch.current = async (term: string) => {
|
||||
const searchParams = getSearchParams(term);
|
||||
if (!searchParams.terms) {
|
||||
handleClearSearch();
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setFilter(FileFilters.ALL);
|
||||
setLastSearchedValue(term);
|
||||
addSearchToTeamSearchHistory(serverUrl, teamId, term);
|
||||
const [postResults, {files, channels}] = await Promise.all([
|
||||
searchPosts(serverUrl, searchParams),
|
||||
searchFiles(serverUrl, teamId, searchParams),
|
||||
]);
|
||||
|
||||
setFileInfos(files?.length ? files : emptyFileResults);
|
||||
setPostIds(postResults?.order?.length ? postResults.order : emptyPostResults);
|
||||
setFileChannelIds(channels?.length ? channels : emptyChannelIds);
|
||||
|
||||
setShowResults(true);
|
||||
setLoading(false);
|
||||
};
|
||||
}, [teamId]);
|
||||
|
||||
const handleRecentSearch = useCallback((text: string) => {
|
||||
setSearchValue(text);
|
||||
handleSearch.current?.(text);
|
||||
}, []);
|
||||
|
||||
const handleFilterChange = useCallback(async (filterValue: FileFilter) => {
|
||||
setLoading(true);
|
||||
setFilter(filterValue);
|
||||
const searchParams = getSearchParams(filterValue);
|
||||
const searchParams = getSearchParams(lastSearchedValue, filterValue);
|
||||
const {files, channels} = await searchFiles(serverUrl, teamId, searchParams);
|
||||
setFileInfos(files?.length ? files : emptyFileResults);
|
||||
setFileChannelIds(channels?.length ? channels : emptyChannelIds);
|
||||
|
||||
setLoading(false);
|
||||
}, [lastSearchedValue]);
|
||||
}, [getSearchParams, lastSearchedValue, searchFiles]);
|
||||
|
||||
useEffect(() => {
|
||||
setSearchValue(searchTerm);
|
||||
}, [searchTerm]);
|
||||
const loadingComponent = useMemo(() => (
|
||||
<Loading
|
||||
containerStyle={[styles.loading, {paddingTop: scrollPaddingTop}]}
|
||||
color={theme.buttonBg}
|
||||
size='large'
|
||||
/>
|
||||
), [theme, scrollPaddingTop]);
|
||||
|
||||
const {scrollPaddingTop, scrollRef, scrollValue, onScroll, headerHeight, hideHeader} = useCollapsibleHeader<FlatList>(true, onSnap);
|
||||
const modifiersComponent = useMemo(() => (
|
||||
<>
|
||||
<Modifiers
|
||||
setSearchValue={setSearchValue}
|
||||
searchValue={searchValue}
|
||||
/>
|
||||
<RecentSearches
|
||||
setRecentValue={handleRecentSearch}
|
||||
teamId={teamId}
|
||||
/>
|
||||
</>
|
||||
), [searchValue, teamId, handleRecentSearch]);
|
||||
|
||||
const resultsComponent = useMemo(() => (
|
||||
<Results
|
||||
selectedTab={selectedTab}
|
||||
searchValue={lastSearchedValue}
|
||||
postIds={postIds}
|
||||
fileInfos={fileInfos}
|
||||
scrollPaddingTop={scrollPaddingTop}
|
||||
fileChannelIds={fileChannelIds}
|
||||
/>
|
||||
), [selectedTab, lastSearchedValue, postIds, fileInfos, scrollPaddingTop]);
|
||||
|
||||
const renderItem = useCallback(() => {
|
||||
if (loading) {
|
||||
return loadingComponent;
|
||||
}
|
||||
if (!showResults) {
|
||||
return modifiersComponent;
|
||||
}
|
||||
return resultsComponent;
|
||||
}, [
|
||||
loading && loadingComponent,
|
||||
!loading && !showResults && modifiersComponent,
|
||||
!loading && showResults && resultsComponent,
|
||||
]);
|
||||
|
||||
const paddingTop = useMemo(() => ({paddingTop: scrollPaddingTop, flexGrow: 1}), [scrollPaddingTop]);
|
||||
|
||||
const animated = useAnimatedStyle(() => {
|
||||
if (isFocused) {
|
||||
@@ -139,15 +215,8 @@ const SearchScreen = ({teamId}: Props) => {
|
||||
};
|
||||
}, [headerHeight, lastSearchedValue]);
|
||||
|
||||
const handleClearSearch = useCallback(() => {
|
||||
setSearchValue('');
|
||||
setLastSearchedValue('');
|
||||
setFilter(FileFilters.ALL);
|
||||
setShowResults(false);
|
||||
}, []);
|
||||
|
||||
let header = null;
|
||||
if (lastSearchedValue) {
|
||||
if (lastSearchedValue && !loading) {
|
||||
header = (
|
||||
<Header
|
||||
onTabSelect={setSelectedTab}
|
||||
@@ -170,10 +239,11 @@ const SearchScreen = ({teamId}: Props) => {
|
||||
scrollValue={scrollValue}
|
||||
hideHeader={hideHeader}
|
||||
onChangeText={setSearchValue}
|
||||
onSubmitEditing={handleSearch}
|
||||
onSubmitEditing={onSubmit}
|
||||
blurOnSubmit={true}
|
||||
placeholder={intl.formatMessage({id: 'screen.search.placeholder', defaultMessage: 'Search messages & files'})}
|
||||
onClear={handleClearSearch}
|
||||
onCancel={handleCancelSearch}
|
||||
defaultValue={searchValue}
|
||||
/>
|
||||
<SafeAreaView
|
||||
@@ -185,26 +255,21 @@ const SearchScreen = ({teamId}: Props) => {
|
||||
<RoundedHeaderContext/>
|
||||
{header}
|
||||
</Animated.View>
|
||||
{!showResults &&
|
||||
<Modifiers
|
||||
setSearchValue={setSearchValue}
|
||||
searchValue={searchValue}
|
||||
scrollPaddingTop={scrollPaddingTop}
|
||||
/>
|
||||
}
|
||||
{showResults &&
|
||||
<Results
|
||||
selectedTab={selectedTab}
|
||||
searchValue={lastSearchedValue}
|
||||
postIds={postIds}
|
||||
fileChannelIds={fileChannelIds}
|
||||
fileInfos={fileInfos}
|
||||
scrollRef={scrollRef}
|
||||
onScroll={onScroll}
|
||||
scrollPaddingTop={scrollPaddingTop}
|
||||
loading={loading}
|
||||
/>
|
||||
}
|
||||
<AnimatedFlatList
|
||||
data={dummyData}
|
||||
contentContainerStyle={paddingTop}
|
||||
keyboardShouldPersistTaps='handled'
|
||||
keyboardDismissMode={'interactive'}
|
||||
nestedScrollEnabled={true}
|
||||
indicatorStyle='black'
|
||||
onScroll={onScroll}
|
||||
scrollEventThrottle={16}
|
||||
removeClippedSubviews={false}
|
||||
scrollToOverflowEnabled={true}
|
||||
overScrollMode='always'
|
||||
ref={scrollRef}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
</Animated.View>
|
||||
</SafeAreaView>
|
||||
</FreezeScreen>
|
||||
|
||||
@@ -74,7 +74,7 @@ const SUPPORTED_VIDEO_FORMAT = Platform.select({
|
||||
const types: Record<string, string> = {};
|
||||
const extensions: Record<string, readonly string[]> = {};
|
||||
|
||||
export function filterFileExtensions(filter: FileFilter): string {
|
||||
export function filterFileExtensions(filter?: FileFilter): string {
|
||||
let searchTerms: string[] = [];
|
||||
switch (filter) {
|
||||
case FileFilters.ALL:
|
||||
|
||||
Reference in New Issue
Block a user