From 340522a90cc7be47296808f455a46dea5777a894 Mon Sep 17 00:00:00 2001 From: Jason Frerich Date: Thu, 26 May 2022 11:50:43 -0500 Subject: [PATCH] [Gekidou - MM-44258] Search Screen - Results Empty State (#6279) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Daniel Espino García Co-authored-by: Elias Nahum Co-authored-by: Daniel Espino --- app/components/navigation_header/context.tsx | 8 +- app/components/navigation_header/header.tsx | 2 +- app/components/navigation_header/index.tsx | 8 +- app/components/navigation_header/large.tsx | 3 +- app/components/navigation_header/search.tsx | 33 +--- .../navigation_header/search_context.tsx | 10 +- app/components/no_results_with_term/index.tsx | 28 +++- .../search_files_illustration.tsx | 60 +++++++ app/constants/view.ts | 12 +- app/hooks/header.ts | 46 ++--- .../header/quick_actions/quick_actions.tsx | 4 +- .../home/recent_mentions/recent_mentions.tsx | 1 - app/screens/home/search/index.tsx | 157 ++++++++---------- app/screens/home/search/results/header.tsx | 76 +++++++++ .../home/search/results/header_button.tsx | 63 +++++++ app/screens/home/search/results/results.tsx | 73 ++++++++ app/screens/settings/display/display.tsx | 2 +- .../settings/notifications/notifications.tsx | 2 +- assets/base/i18n/en.json | 7 +- 19 files changed, 427 insertions(+), 168 deletions(-) create mode 100644 app/components/no_results_with_term/search_files_illustration.tsx create mode 100644 app/screens/home/search/results/header.tsx create mode 100644 app/screens/home/search/results/header_button.tsx create mode 100644 app/screens/home/search/results/results.tsx diff --git a/app/components/navigation_header/context.tsx b/app/components/navigation_header/context.tsx index d8e84ba6dc..41c856f774 100644 --- a/app/components/navigation_header/context.tsx +++ b/app/components/navigation_header/context.tsx @@ -5,6 +5,7 @@ import React from 'react'; import Animated, {useAnimatedStyle} from 'react-native-reanimated'; import RoundedHeaderContext from '@components/rounded_header_context'; +import {HEADER_SEARCH_BOTTOM_MARGIN, HEADER_SEARCH_HEIGHT} from '@constants/view'; type Props = { defaultHeight: number; @@ -25,12 +26,13 @@ const NavigationHeaderContext = ({ }: Props) => { const marginTop = useAnimatedStyle(() => { const normal = defaultHeight + top; - const calculated = -(top + (scrollValue?.value || 0)); - const searchHeight = hasSearch ? defaultHeight + 9 : 0; + const value = scrollValue?.value || 0; let margin: number; if (isLargeTitle) { - margin = Math.max((-(scrollValue?.value || 0) + largeHeight + searchHeight), normal); + const searchHeight = hasSearch ? HEADER_SEARCH_HEIGHT + HEADER_SEARCH_BOTTOM_MARGIN : 0; + margin = Math.max((-value + largeHeight + searchHeight), normal); } else { + const calculated = -(top + value); margin = Math.max((normal + calculated), normal); } diff --git a/app/components/navigation_header/header.tsx b/app/components/navigation_header/header.tsx index cea7359a99..4db06d6d3d 100644 --- a/app/components/navigation_header/header.tsx +++ b/app/components/navigation_header/header.tsx @@ -152,7 +152,7 @@ const Header = ({ } const barHeight = Platform.OS === 'ios' ? (largeHeight - defaultHeight - (top / 2)) : largeHeight - defaultHeight; - const val = (top + (scrollValue?.value ?? 0)); + const val = top + (scrollValue?.value ?? 0); return { opacity: val >= barHeight ? withTiming(1, {duration: 250}) : 0, }; diff --git a/app/components/navigation_header/index.tsx b/app/components/navigation_header/index.tsx index 301f3087e5..5b5500c626 100644 --- a/app/components/navigation_header/index.tsx +++ b/app/components/navigation_header/index.tsx @@ -2,7 +2,6 @@ // 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'; @@ -19,7 +18,6 @@ import NavigationHeaderSearchContext from './search_context'; import type {SearchProps} from '@components/search'; type Props = SearchProps & { - forwardedRef?: React.RefObject; hasSearch?: boolean; isLargeTitle?: boolean; leftComponent?: React.ReactElement; @@ -27,6 +25,7 @@ type Props = SearchProps & { onTitlePress?: () => void; rightButtons?: HeaderRightButton[]; scrollValue?: Animated.SharedValue; + hideHeader?: (visible: boolean) => void; showBackButton?: boolean; showHeaderInContext?: boolean; subtitle?: string; @@ -44,7 +43,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ })); const NavigationHeader = ({ - forwardedRef, hasSearch = false, isLargeTitle = false, leftComponent, @@ -57,6 +55,7 @@ const NavigationHeader = ({ subtitle, subtitleCompanion, title = '', + hideHeader, ...searchProps }: Props) => { const theme = useTheme(); @@ -107,10 +106,9 @@ const NavigationHeader = ({ <> diff --git a/app/components/navigation_header/large.tsx b/app/components/navigation_header/large.tsx index 42f4cbaa46..72cd2b7f00 100644 --- a/app/components/navigation_header/large.tsx +++ b/app/components/navigation_header/large.tsx @@ -47,8 +47,9 @@ const NavigationHeaderLargeTitle = ({ const styles = getStyleSheet(theme); const transform = useAnimatedStyle(() => { + const value = scrollValue?.value || 0; return { - transform: [{translateY: -(top + (scrollValue?.value || 0))}], + transform: [{translateY: -(top + value)}], }; }, [top]); diff --git a/app/components/navigation_header/search.tsx b/app/components/navigation_header/search.tsx index ab1f34ebbf..ad56b962d8 100644 --- a/app/components/navigation_header/search.tsx +++ b/app/components/navigation_header/search.tsx @@ -2,20 +2,18 @@ // See LICENSE.txt for license information. import React, {useCallback, useMemo} from 'react'; -import {FlatList, Platform, ScrollView, SectionList} from 'react-native'; +import {Platform} 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 {HEADER_SEARCH_HEIGHT} from '@constants/view'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; type Props = SearchProps & { - defaultHeight: number; - forwardedRef?: React.RefObject; largeHeight: number; scrollValue?: Animated.SharedValue; + hideHeader?: (visible: boolean) => void; theme: Theme; top: number; } @@ -23,7 +21,7 @@ type Props = SearchProps & { const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ container: { backgroundColor: theme.sidebarBg, - height: SEARCH_INPUT_HEIGHT + 5, + height: HEADER_SEARCH_HEIGHT, justifyContent: 'flex-start', paddingHorizontal: 20, position: 'absolute', @@ -39,15 +37,13 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ })); const NavigationSearch = ({ - defaultHeight, - forwardedRef, largeHeight, scrollValue, + hideHeader: setHeaderVisibility, theme, top, ...searchProps }: Props) => { - const isTablet = useIsTablet(); const styles = getStyleSheet(theme); const cancelButtonProps: SearchProps['cancelButtonProps'] = useMemo(() => ({ @@ -60,25 +56,12 @@ const NavigationSearch = ({ const searchTop = useAnimatedStyle(() => { return {marginTop: Math.max((-(scrollValue?.value || 0) + largeHeight), top)}; - }, [defaultHeight, largeHeight, top]); + }, [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 || 0)) <= top) { - if ('scrollTo' in forwardedRef.current) { - forwardedRef.current.scrollTo({y: offset, animated: true}); - } else if ('scrollToOffset' in forwardedRef.current) { - forwardedRef.current.scrollToOffset({ - offset, - animated: true, - }); - } else { - // No scroll for section lists? - } - } + setHeaderVisibility?.(false); searchProps.onFocus?.(e); - }, [largeHeight, top]); + }, [setHeaderVisibility, searchProps.onFocus]); return ( diff --git a/app/components/navigation_header/search_context.tsx b/app/components/navigation_header/search_context.tsx index 66063d210c..8d4b67eab8 100644 --- a/app/components/navigation_header/search_context.tsx +++ b/app/components/navigation_header/search_context.tsx @@ -4,7 +4,7 @@ import React from 'react'; import Animated, {useAnimatedStyle} from 'react-native-reanimated'; -import {ANDROID_HEADER_SEARCH_INSET} from '@constants/view'; +import {HEADER_SEARCH_BOTTOM_MARGIN, HEADER_SEARCH_HEIGHT} from '@constants/view'; import {makeStyleSheetFromTheme} from '@utils/theme'; type Props = { @@ -17,7 +17,7 @@ type Props = { const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ container: { backgroundColor: theme.sidebarBg, - height: 20, + height: HEADER_SEARCH_BOTTOM_MARGIN * 2, position: 'absolute', width: '100%', }, @@ -32,13 +32,11 @@ const NavigationHeaderSearchContext = ({ const styles = getStyleSheet(theme); const marginTop = useAnimatedStyle(() => { - return {marginTop: (-(scrollValue?.value || 0) + largeHeight + defaultHeight) - ANDROID_HEADER_SEARCH_INSET}; + return {marginTop: (largeHeight + HEADER_SEARCH_HEIGHT) - (scrollValue?.value || 0)}; }, [defaultHeight, largeHeight]); return ( - - - + ); }; diff --git a/app/components/no_results_with_term/index.tsx b/app/components/no_results_with_term/index.tsx index a53fb56e4e..ba4699be47 100644 --- a/app/components/no_results_with_term/index.tsx +++ b/app/components/no_results_with_term/index.tsx @@ -1,18 +1,21 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React from 'react'; +import React, {useState, useEffect} from 'react'; import {View} from 'react-native'; import FormattedText from '@components/formatted_text'; import {useTheme} from '@context/theme'; +import {t} from '@i18n'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; +import SearchFilesIllustration from './search_files_illustration'; import SearchIllustration from './search_illustration'; type Props = { term: string; + type?: 'default' | 'messages' | 'files'; }; const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => { @@ -34,18 +37,31 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => { }; }); -const NoResultsWithTerm = ({term}: Props) => { +const NoResultsWithTerm = ({term, type}: Props) => { const theme = useTheme(); const style = getStyleFromTheme(theme); + const [titleId, setTitleId] = useState(t('mobile.no_results_with_term')); + const [defaultMessage, setDefaultMessage] = useState('No results for “{term}”'); + + useEffect(() => { + if (type === 'files') { + setTitleId(t('mobile.no_results_with_term.files')); + setDefaultMessage('No files matching “{term}”'); + } else if (type === 'messages') { + setTitleId(t('mobile.no_results_with_term.messages')); + setDefaultMessage('No matches found for “{term}”'); + } + }, [type]); + return ( - + {type === 'files' ? : } + + + + + + + + + + + + ); +} + +export default SearchFilesIllustration; diff --git a/app/constants/view.ts b/app/constants/view.ts index 0ec236e2c5..22d4709f7a 100644 --- a/app/constants/view.ts +++ b/app/constants/view.ts @@ -7,7 +7,7 @@ export const BOTTOM_TAB_HEIGHT = 52; 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 SEARCH_INPUT_HEIGHT = Platform.select({android: 40, default: 36}); export const TEAM_SIDEBAR_WIDTH = 72; export const TABLET_HEADER_HEIGHT = 44; @@ -18,10 +18,9 @@ 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 const KEYBOARD_TRACKING_OFFSET = 72; +export const HEADER_SEARCH_HEIGHT = SEARCH_INPUT_HEIGHT + 5; +export const HEADER_SEARCH_BOTTOM_MARGIN = 10; export const INDICATOR_BAR_HEIGHT = 38; @@ -42,10 +41,9 @@ export default { LARGE_HEADER_TITLE, HEADER_WITH_SEARCH_HEIGHT, HEADER_WITH_SUBTITLE, - IOS_HEADER_SEARCH_INSET, - TABLET_HEADER_SEARCH_INSET, - ANDROID_HEADER_SEARCH_INSET, INDICATOR_BAR_HEIGHT, KEYBOARD_TRACKING_OFFSET, + HEADER_SEARCH_HEIGHT, + HEADER_SEARCH_BOTTOM_MARGIN, }; diff --git a/app/hooks/header.ts b/app/hooks/header.ts index 9bd37b92cf..1ff4a053b3 100644 --- a/app/hooks/header.ts +++ b/app/hooks/header.ts @@ -1,12 +1,12 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {useMemo} from 'react'; +import React, {useCallback, 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 ViewConstants, {HEADER_SEARCH_BOTTOM_MARGIN} from '@constants/view'; import {useIsTablet} from '@hooks/device'; type HeaderScrollContext = { @@ -52,7 +52,6 @@ export const useHeaderHeight = (hasLargeTitle: boolean, hasSubtitle: boolean, ha export const useCollapsibleHeader = (isLargeTitle: boolean, hasSubtitle: boolean, hasSearch: boolean) => { const insets = useSafeAreaInsets(); - const isTablet = useIsTablet(); const animatedRef = useAnimatedRef(); const {largeHeight, defaultHeight} = useHeaderHeight(true, hasSubtitle, hasSearch); const scrollValue = useSharedValue(0); @@ -63,20 +62,13 @@ export const useCollapsibleHeader = (isLargeTitle: boolean, hasSubtitle: bool 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)); + position = (diffHeight - (hasSearch ? -HEADER_SEARCH_BOTTOM_MARGIN : insets.top)); } else { - position = hasSearch ? largeHeight + ViewConstants.ANDROID_HEADER_SEARCH_INSET : diffHeight; + position = hasSearch ? largeHeight + HEADER_SEARCH_BOTTOM_MARGIN : diffHeight; } - scrollTo(animatedRef, 0, position!, true); + 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); - } + const inset = largeHeight + (hasSearch ? HEADER_SEARCH_BOTTOM_MARGIN : 0); if (offset < inset) { scrollTo(animatedRef, 0, -insets.top, true); } @@ -104,7 +96,6 @@ export const useCollapsibleHeader = (isLargeTitle: boolean, hasSubtitle: bool 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; @@ -114,27 +105,40 @@ export const useCollapsibleHeader = (isLargeTitle: boolean, hasSubtitle: bool return; } snapIfNeeded(dir, offset); - } else if (dir === 'down' && offset < (defaultHeight + (hasSearch ? searchInset : 0))) { + } else if (dir === 'down' && offset < (defaultHeight + (hasSearch ? HEADER_SEARCH_BOTTOM_MARGIN : 0))) { scrollTo(animatedRef, 0, -insets.top, true); } } }, }, [insets, defaultHeight, largeHeight]); + const hideHeader = useCallback(() => { + const offset = largeHeight + HEADER_SEARCH_BOTTOM_MARGIN; + if (animatedRef?.current && Math.abs((scrollValue?.value || 0)) <= insets.top) { + if ('scrollTo' in animatedRef.current) { + animatedRef.current.scrollTo({y: offset, animated: true}); + } else if ('scrollToOffset' in animatedRef.current) { + (animatedRef.current as any).scrollToOffset({ + offset, + animated: true, + }); + } else { + // No scroll for section lists? + } + } + }, [largeHeight, defaultHeight]); + let searchPadding = 0; if (hasSearch) { - searchPadding = ViewConstants.SEARCH_INPUT_HEIGHT + - ViewConstants.IOS_HEADER_SEARCH_INSET + - ViewConstants.ANDROID_HEADER_SEARCH_INSET; + searchPadding = ViewConstants.HEADER_SEARCH_HEIGHT + ViewConstants.HEADER_SEARCH_BOTTOM_MARGIN; } return { - defaultHeight, - largeHeight, scrollPaddingTop: (isLargeTitle ? largeHeight : defaultHeight) + searchPadding, scrollRef: animatedRef as unknown as React.RefObject, scrollValue, onScroll, + hideHeader, }; }; diff --git a/app/screens/channel/header/quick_actions/quick_actions.tsx b/app/screens/channel/header/quick_actions/quick_actions.tsx index d9caf10b11..c870465a2d 100644 --- a/app/screens/channel/header/quick_actions/quick_actions.tsx +++ b/app/screens/channel/header/quick_actions/quick_actions.tsx @@ -4,8 +4,8 @@ import React, {useCallback} from 'react'; import {StyleSheet, View} from 'react-native'; -import AddPeopleBox from '@app/components/channel_actions/add_people_box'; -import CopyChannelLinkBox from '@app/components/channel_actions/copy_channel_link_box'; +import AddPeopleBox from '@components/channel_actions/add_people_box'; +import CopyChannelLinkBox from '@components/channel_actions/copy_channel_link_box'; import FavoriteBox from '@components/channel_actions/favorite_box'; import InfoBox from '@components/channel_actions/info_box'; import LeaveChannelLabel from '@components/channel_actions/leave_channel_label'; diff --git a/app/screens/home/recent_mentions/recent_mentions.tsx b/app/screens/home/recent_mentions/recent_mentions.tsx index 80b2324720..3999d45f2a 100644 --- a/app/screens/home/recent_mentions/recent_mentions.tsx +++ b/app/screens/home/recent_mentions/recent_mentions.tsx @@ -154,7 +154,6 @@ const RecentMentionsScreen = ({mentions, currentTimezone, isTimezoneEnabled}: Pr title={title} hasSearch={false} scrollValue={scrollValue} - forwardedRef={scrollRef} /> { const nav = useNavigation(); const isFocused = useIsFocused(); - const theme = useTheme(); const intl = useIntl(); const searchScreenIndex = 1; const stateIndex = nav.getState().index; const {searchTerm} = nav.getState().routes[stateIndex].params; + const [searchValue, setSearchValue] = useState(searchTerm); + const [selectedTab, setSelectedTab] = useState('messages'); + + useEffect(() => { + setSearchValue(searchTerm); + }, [searchTerm]); + + const handleSearch = () => { + // 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?? + + // console.log('execute the search for : ', searchValue); + }; + + const isLargeTitle = true; + const hasSearch = true; + + const {scrollPaddingTop, scrollRef, scrollValue, onScroll, hideHeader} = useCollapsibleHeader(isLargeTitle, false, hasSearch); + + const onHeaderTabSelect = useCallback((tab: SelectTab) => { + setSelectedTab(tab); + }, [setSelectedTab]); + const animated = useAnimatedStyle(() => { if (isFocused) { return { opacity: withTiming(1, {duration: 150}), - transform: [{translateX: withTiming(0, {duration: 150})}], + flex: 1, + transform: [ + {translateX: withTiming(0, {duration: 150})}, + ], }; } return { opacity: withTiming(0, {duration: 150}), transform: [{translateX: withTiming(stateIndex < searchScreenIndex ? 25 : -25, {duration: 150})}], + }; - }, [isFocused, stateIndex]); + }, [isFocused, stateIndex, scrollPaddingTop]); - // 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 = ( - - - - ); - } - - 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>(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', - ]; - - const renderItem = ({item, index}: ListRenderItemInfo) => { - const height = index === data.length - 1 ? undefined : 400; - return ( - - {item} - - ); - }; + const paddingTop = useMemo(() => ({paddingTop: scrollPaddingTop + TOP_MARGIN, flexGrow: 1}), [scrollPaddingTop]); return ( { // eslint-disable-next-line no-console console.log('BACK'); }} - rightButtons={rightButtons} - showBackButton={showBackButton} - subtitle={subtitle} - title={title} + showBackButton={false} + title={intl.formatMessage({id: 'screen.search.title', defaultMessage: 'Search'})} 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'); - }} + hideHeader={hideHeader} + onChangeText={setSearchValue} + onSubmitEditing={handleSearch} blurOnSubmit={true} placeholder={intl.formatMessage({id: 'screen.search.placeholder', defaultMessage: 'Search messages & files'})} - defaultValue={searchTerm} + defaultValue={searchValue} /> - - + + > + {/* */} + {/* */} + + {/* */} + diff --git a/app/screens/home/search/results/header.tsx b/app/screens/home/search/results/header.tsx new file mode 100644 index 0000000000..f11ed8f93a --- /dev/null +++ b/app/screens/home/search/results/header.tsx @@ -0,0 +1,76 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import React, {useCallback, useState} from 'react'; +import {useIntl} from 'react-intl'; +import {View} from 'react-native'; + +import {useTheme} from '@context/theme'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; + +import SelectButton from './header_button'; + +export type SelectTab = 'files' | 'messages' + +type Props = { + onTabSelect: (tab: SelectTab) => void; + numberFiles: number; + numberMessages: number; +} + +const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { + return { + container: { + marginHorizontal: 12, + flexDirection: 'row', + marginBottom: 12, + flexGrow: 0, + }, + divider: { + backgroundColor: changeOpacity(theme.centerChannelColor, 0.08), + height: 1, + }, + }; +}); + +const Header = ({onTabSelect, numberFiles, numberMessages}: Props) => { + const theme = useTheme(); + const styles = getStyleFromTheme(theme); + const intl = useIntl(); + + const messagesText = intl.formatMessage({id: 'screen.search.header.messages', defaultMessage: 'Messages'}); + const filesText = intl.formatMessage({id: 'screen.search.header.files', defaultMessage: 'Files'}); + + const [tab, setTab] = useState(0); + + const handleMessagesPress = useCallback(() => { + onTabSelect('messages'); + setTab(0); + }, [onTabSelect]); + + const handleFilesPress = useCallback(() => { + onTabSelect('files'); + setTab(1); + }, [onTabSelect]); + + return ( + <> + + + + + + + + ); +}; + +export default Header; + diff --git a/app/screens/home/search/results/header_button.tsx b/app/screens/home/search/results/header_button.tsx new file mode 100644 index 0000000000..14785a5858 --- /dev/null +++ b/app/screens/home/search/results/header_button.tsx @@ -0,0 +1,63 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {Text} from 'react-native'; +import Button from 'react-native-button'; + +import {useTheme} from '@context/theme'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import {typography} from '@utils/typography'; + +const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { + return { + flex: { + flex: 1, + }, + button: { + alignItems: 'center', + borderRadius: 4, + height: 40, + }, + text: { + marginHorizontal: 16, + marginVertical: 8, + ...typography('Body', 200, 'SemiBold'), + }, + selectedButton: { + backgroundColor: changeOpacity(theme.buttonBg, 0.1), + }, + selectedText: { + color: theme.buttonBg, + }, + unselectedText: { + color: changeOpacity(theme.centerChannelColor, 0.56), + }, + }; +}); + +type ButtonProps = { + onPress: () => void; + selected: boolean; + text: string; +} + +const SelectButton = ({selected, onPress, text}: ButtonProps) => { + const theme = useTheme(); + const styles = getStyleFromTheme(theme); + + return ( + + ); +}; + +export default SelectButton; diff --git a/app/screens/home/search/results/results.tsx b/app/screens/home/search/results/results.tsx new file mode 100644 index 0000000000..dc65c80bbe --- /dev/null +++ b/app/screens/home/search/results/results.tsx @@ -0,0 +1,73 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useState} from 'react'; +import {Text, View} from 'react-native'; + +import NoResultsWithTerm from '@components/no_results_with_term'; + +import Header from './header'; + +type Props = { + searchValue: string; + selectedTab: string; + onHeaderTabSelect: (tab: string) => void; +} + +const emptyPostResults: Post[] = []; +const emptyFilesResults: FileInfo[] = []; + +const notImplementedComponent = ( + + {'Not Implemented'} + +); + +const SearchResults = ({ + searchValue, + selectedTab, + onHeaderTabSelect, +}: Props) => { + const [postResults] = useState(emptyPostResults); + const [fileResults] = useState(emptyFilesResults); + const [loading] = useState(false); + + let content; + if (loading) { + content = notImplementedComponent; + } else if (!searchValue) { + content = notImplementedComponent; + } else if ( + (selectedTab === 'messages' && postResults.length === 0) || + (selectedTab === 'files' && fileResults.length === 0) + ) { + content = (<> +
+ + + ); + } else { + content = notImplementedComponent; + } + + return (<> + + {content} + ); +}; + +export default SearchResults; diff --git a/app/screens/settings/display/display.tsx b/app/screens/settings/display/display.tsx index efd12c0eb5..58ab09ae1d 100644 --- a/app/screens/settings/display/display.tsx +++ b/app/screens/settings/display/display.tsx @@ -5,9 +5,9 @@ import React from 'react'; import {Alert, Platform, ScrollView, View} from 'react-native'; import {SafeAreaView} from 'react-native-safe-area-context'; -import {changeOpacity, makeStyleSheetFromTheme} from '@app/utils/theme'; import {useTheme} from '@context/theme'; import SettingOption from '@screens/settings/setting_option'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; const getStyleSheet = makeStyleSheetFromTheme((theme) => { return { diff --git a/app/screens/settings/notifications/notifications.tsx b/app/screens/settings/notifications/notifications.tsx index f0ebdff83b..9e8d68652a 100644 --- a/app/screens/settings/notifications/notifications.tsx +++ b/app/screens/settings/notifications/notifications.tsx @@ -6,12 +6,12 @@ import {useIntl} from 'react-intl'; import {Alert, Platform, ScrollView, View} from 'react-native'; import {Edge, SafeAreaView} from 'react-native-safe-area-context'; -import {changeOpacity, makeStyleSheetFromTheme} from '@app/utils/theme'; import {Screens} from '@constants'; import {useTheme} from '@context/theme'; import {t} from '@i18n'; import {goToScreen} from '@screens/navigation'; import SettingOption from '@screens/settings/setting_option'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; const getStyleSheet = makeStyleSheetFromTheme((theme) => { return { diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index 9e17f37434..3a5805d57b 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -403,7 +403,9 @@ "mobile.message_length.message": "Your current message is too long. Current character count: {count}/{max}", "mobile.message_length.message_split_left": "Message exceeds the character limit", "mobile.message_length.title": "Message Length", - "mobile.no_results_with_term": "No results for {term}", + "mobile.no_results_with_term": "No results for “{term}”", + "mobile.no_results_with_term.files": "No files matching “{term}”", + "mobile.no_results_with_term.messages": "No matches found for “{term}”", "mobile.no_results.spelling": "Check the spelling or try another search.", "mobile.notice_mobile_link": "mobile apps", "mobile.notice_platform_link": "server", @@ -573,7 +575,10 @@ "saved_posts.empty.title": "No saved messages yet", "screen.mentions.subtitle": "Messages you've been mentioned in", "screen.mentions.title": "Recent Mentions", + "screen.search.header.files": "Files", + "screen.search.header.messages": "Messages", "screen.search.placeholder": "Search messages & files", + "screen.search.title": "Search", "screens.channel_edit_header": "Edit Channel Header", "screens.channel_info": "Channel Info", "search_bar.search": "Search",