[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:
Jason Frerich
2022-07-14 07:56:08 -05:00
committed by GitHub
parent 97b5e75e5f
commit 17f6aee8a5
11 changed files with 430 additions and 104 deletions

View File

@@ -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};
}
}

View File

@@ -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;

View File

@@ -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>

View File

@@ -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();
};

View File

@@ -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>
</>
);
};

View 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);

View 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;

View 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;

View File

@@ -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;

View File

@@ -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>

View File

@@ -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: