From 9d88099ac5cd2794d1ad2f0c50e0ce3fd2ec47e1 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Tue, 12 Apr 2022 09:24:46 -0400 Subject: [PATCH] [Gekidou] new UI for no results found (#6165) * new UI for no results found * feedback review --- app/actions/remote/user.ts | 10 ++- app/components/no_results_with_term/index.tsx | 59 +++++++++++++ .../search_illustration.tsx | 49 +++++++++++ app/hooks/device.ts | 23 ++++- .../browse_channels/browse_channels.tsx | 2 +- app/screens/browse_channels/channel_list.tsx | 57 ++++++------ .../create_direct_message.tsx | 2 +- .../create_direct_message/user_list.tsx | 88 +++++++------------ assets/base/i18n/en.json | 4 +- 9 files changed, 203 insertions(+), 91 deletions(-) create mode 100644 app/components/no_results_with_term/index.tsx create mode 100644 app/components/no_results_with_term/search_illustration.tsx diff --git a/app/actions/remote/user.ts b/app/actions/remote/user.ts index 2f44542a9e..f7269f61ab 100644 --- a/app/actions/remote/user.ts +++ b/app/actions/remote/user.ts @@ -421,10 +421,12 @@ export const searchProfiles = async (serverUrl: string, term: string, options: a if (!fetchOnly) { const toStore = removeUserFromList(currentUserId, users); - await operator.handleUsers({ - users: toStore, - prepareRecordsOnly: false, - }); + if (toStore.length) { + await operator.handleUsers({ + users: toStore, + prepareRecordsOnly: false, + }); + } } return {data: users}; diff --git a/app/components/no_results_with_term/index.tsx b/app/components/no_results_with_term/index.tsx new file mode 100644 index 0000000000..a53fb56e4e --- /dev/null +++ b/app/components/no_results_with_term/index.tsx @@ -0,0 +1,59 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {View} from 'react-native'; + +import FormattedText from '@components/formatted_text'; +import {useTheme} from '@context/theme'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import {typography} from '@utils/typography'; + +import SearchIllustration from './search_illustration'; + +type Props = { + term: string; +}; + +const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => { + return { + container: { + flexGrow: 1, + alignItems: 'center' as const, + justifyContent: 'center' as const, + }, + result: { + color: theme.centerChannelColor, + ...typography('Heading', 400, 'SemiBold'), + }, + spelling: { + color: changeOpacity(theme.centerChannelColor, 0.72), + marginTop: 8, + ...typography('Body', 200), + }, + }; +}); + +const NoResultsWithTerm = ({term}: Props) => { + const theme = useTheme(); + const style = getStyleFromTheme(theme); + + return ( + + + + + + ); +}; + +export default NoResultsWithTerm; diff --git a/app/components/no_results_with_term/search_illustration.tsx b/app/components/no_results_with_term/search_illustration.tsx new file mode 100644 index 0000000000..2c94437652 --- /dev/null +++ b/app/components/no_results_with_term/search_illustration.tsx @@ -0,0 +1,49 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import Svg, {Ellipse, Path} from 'react-native-svg'; + +function SearchIllustration() { + return ( + + + + + + + + + ); +} + +export default SearchIllustration; diff --git a/app/hooks/device.ts b/app/hooks/device.ts index f0186fca4b..700719ddd5 100644 --- a/app/hooks/device.ts +++ b/app/hooks/device.ts @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import {useEffect, useState} from 'react'; -import {AppState, NativeModules, useWindowDimensions} from 'react-native'; +import {AppState, Keyboard, NativeModules, useWindowDimensions} from 'react-native'; import {Device} from '@constants'; @@ -42,3 +42,24 @@ export function useIsTablet() { const isSplitView = useSplitView(); return Device.IS_TABLET && !isSplitView; } + +export function useKeyboardHeight() { + const [keyboardHeight, setKeyboardHeight] = useState(0); + + useEffect(() => { + const show = Keyboard.addListener('keyboardWillShow', (event) => { + setKeyboardHeight(event.endCoordinates.height); + }); + + const hide = Keyboard.addListener('keyboardWillHide', () => { + setKeyboardHeight(0); + }); + + return () => { + show.remove(); + hide.remove(); + }; + }, []); + + return keyboardHeight; +} diff --git a/app/screens/browse_channels/browse_channels.tsx b/app/screens/browse_channels/browse_channels.tsx index bf2176cd36..20ad872cf0 100644 --- a/app/screens/browse_channels/browse_channels.tsx +++ b/app/screens/browse_channels/browse_channels.tsx @@ -236,9 +236,9 @@ export default function BrowseChannels(props: Props) { ); diff --git a/app/screens/browse_channels/channel_list.tsx b/app/screens/browse_channels/channel_list.tsx index 7b1e0ec6d0..d679b3a27f 100644 --- a/app/screens/browse_channels/channel_list.tsx +++ b/app/screens/browse_channels/channel_list.tsx @@ -1,25 +1,24 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {useCallback} from 'react'; +import React, {useCallback, useMemo} from 'react'; import {View, FlatList} from 'react-native'; import FormattedText from '@components/formatted_text'; import Loading from '@components/loading'; +import NoResultsWithTerm from '@components/no_results_with_term'; import {useTheme} from '@context/theme'; -import { - changeOpacity, - makeStyleSheetFromTheme, -} from '@utils/theme'; +import {useKeyboardHeight} from '@hooks/device'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import ChannelListRow from './channel_list_row'; type Props = { onEndReached: () => void; loading: boolean; - isSearch: boolean; channels: Channel[]; onSelectChannel: (channel: Channel) => void; + term?: string; } const channelKeyExtractor = (channel: Channel) => { @@ -28,15 +27,6 @@ const channelKeyExtractor = (channel: Channel) => { const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => { return { - noResultContainer: { - flexGrow: 1, - alignItems: 'center' as const, - justifyContent: 'center' as const, - }, - noResultText: { - fontSize: 26, - color: changeOpacity(theme.centerChannelColor, 0.5), - }, loadingContainer: { flex: 1, justifyContent: 'center' as const, @@ -51,6 +41,11 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => { paddingHorizontal: 20, flexGrow: 1, }, + noResultContainer: { + flexGrow: 1, + alignItems: 'center' as const, + justifyContent: 'center' as const, + }, separator: { height: 1, backgroundColor: changeOpacity(theme.centerChannelColor, 0.08), @@ -63,11 +58,17 @@ export default function ChannelList({ onEndReached, onSelectChannel, loading, - isSearch, + term, channels, }: Props) { const theme = useTheme(); + const style = getStyleFromTheme(theme); + const keyboardHeight = useKeyboardHeight(); + const noResutsStyle = useMemo(() => [ + style.noResultContainer, + {paddingBottom: keyboardHeight}, + ], [style, keyboardHeight]); const renderItem = useCallback(({item}: {item: Channel}) => { return ( @@ -80,6 +81,10 @@ export default function ChannelList({ }, [onSelectChannel]); const renderLoading = useCallback(() => { + if (!loading) { + return null; + } + return ( { - if (isSearch) { + if (term) { return ( - - + + ); } return ( - + ); - }, [style, isSearch]); + }, [style, term, noResutsStyle]); const renderSeparator = useCallback(() => ( diff --git a/app/screens/create_direct_message/user_list.tsx b/app/screens/create_direct_message/user_list.tsx index 7f2909129e..e3ccf25800 100644 --- a/app/screens/create_direct_message/user_list.tsx +++ b/app/screens/create_direct_message/user_list.tsx @@ -4,10 +4,12 @@ import React, {useCallback, useMemo} from 'react'; import {FlatList, Keyboard, ListRenderItemInfo, Platform, SectionList, SectionListData, Text, View} from 'react-native'; -import FormattedText from '@components/formatted_text'; +import Loading from '@components/loading'; +import NoResultsWithTerm from '@components/no_results_with_term'; import UserListRow from '@components/user_list_row'; import {General} from '@constants'; import {useTheme} from '@context/theme'; +import {useKeyboardHeight} from '@hooks/device'; import { changeOpacity, makeStyleSheetFromTheme, @@ -73,31 +75,20 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { container: { flexGrow: 1, }, - separator: { - height: 1, - flex: 1, - backgroundColor: changeOpacity(theme.centerChannelColor, 0.1), - }, - listView: { - flex: 1, - backgroundColor: theme.centerChannelBg, - ...Platform.select({ - android: { - marginBottom: 20, - }, - }), - }, loadingContainer: { - marginHorizontal: 20, + flex: 1, + justifyContent: 'center' as const, + alignItems: 'center' as const, }, - loadingText: { - color: changeOpacity(theme.centerChannelColor, 0.6), + loading: { + height: 32, + width: 32, + justifyContent: 'center' as const, }, - searching: { - backgroundColor: theme.centerChannelBg, - height: '100%', - position: 'absolute', - width: '100%', + noResultContainer: { + flexGrow: 1, + alignItems: 'center' as const, + justifyContent: 'center' as const, }, sectionContainer: { backgroundColor: changeOpacity(theme.centerChannelColor, 0.08), @@ -112,16 +103,6 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { color: theme.centerChannelColor, ...typography('Body', 300, 'SemiBold'), }, - noResultContainer: { - flex: 1, - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - }, - noResultText: { - fontSize: 26, - color: changeOpacity(theme.centerChannelColor, 0.5), - }, }; }); @@ -135,7 +116,7 @@ type Props = { showNoResults: boolean; selectedIds: {[id: string]: UserProfile}; testID?: string; - isSearch: boolean; + term?: string; } export default function UserList({ @@ -147,11 +128,16 @@ export default function UserList({ fetchMore, loading, showNoResults, - isSearch, + term, testID, }: Props) { const theme = useTheme(); const style = getStyleFromTheme(theme); + const keyboardHeight = useKeyboardHeight(); + const noResutsStyle = useMemo(() => [ + style.noResultContainer, + {paddingBottom: keyboardHeight}, + ], [style, keyboardHeight]); const renderItem = useCallback(({item}: ListRenderItemInfo) => { // The list will re-render when the selection changes because it's passed into the list as extraData @@ -180,31 +166,25 @@ export default function UserList({ } return ( - - - + ); - }, [loading && style]); + }, [loading, theme]); const renderNoResults = useCallback(() => { - if (!showNoResults) { + if (!showNoResults || !term) { return null; } return ( - - + + ); - }, [showNoResults && style]); + }, [showNoResults && style, term, noResutsStyle]); const renderSectionHeader = useCallback(({section}: {section: SectionListData}) => { return ( @@ -264,13 +244,13 @@ export default function UserList({ }; const data = useMemo(() => { - if (isSearch) { + if (term) { return profiles; } return createProfilesSections(profiles); - }, [isSearch, profiles]); + }, [term, profiles]); - if (isSearch) { + if (term) { return renderFlatList(data as UserProfile[]); } return renderSectionList(data as Array>); diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index 795d3933be..c3095d49ae 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -258,7 +258,6 @@ "mobile.create_direct_message.start": "Start", "mobile.create_direct_message.you": "@{username} - you", "mobile.create_post.read_only": "This channel is read-only.", - "mobile.custom_list.no_results": "No Results", "mobile.custom_status.choose_emoji": "Choose an emoji", "mobile.custom_status.clear_after": "Clear After", "mobile.custom_status.clear_after.title": "Clear Custom Status After", @@ -290,7 +289,6 @@ "mobile.join_channel.error": "We couldn't join the channel {displayName}. Please check your connection and try again.", "mobile.link.error.text": "Unable to open the link.", "mobile.link.error.title": "Error", - "mobile.loading_members": "Loading Members...", "mobile.login_options.cant_heading": "Can't Log In", "mobile.login_options.enter_credentials": "Enter your login details below.", "mobile.login_options.gitlab": "GitLab", @@ -319,6 +317,8 @@ "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.spelling": "Check the spelling or try another search.", "mobile.notice_mobile_link": "mobile apps", "mobile.notice_platform_link": "server", "mobile.notice_text": "Mattermost is made possible by the open source software used in our {platform} and {mobile}.",