[Gekidou] new UI for no results found (#6165)

* new UI for no results found

* feedback review
This commit is contained in:
Elias Nahum
2022-04-12 09:24:46 -04:00
committed by GitHub
parent 57e3334507
commit 9d88099ac5
9 changed files with 203 additions and 91 deletions

View File

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

View File

@@ -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 (
<View style={style.container}>
<SearchIllustration/>
<FormattedText
id='mobile.no_results_with_term'
defaultMessage='No results for {term}'
values={{term}}
style={style.result}
/>
<FormattedText
id='mobile.no_results.spelling'
defaultMessage='Check the spelling or try another search.'
style={style.spelling}
/>
</View>
);
};
export default NoResultsWithTerm;

View File

@@ -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 (
<Svg
width={140}
height={149}
viewBox='0 0 140 149'
fill='none'
>
<Ellipse
cx={70}
cy={122.5}
rx={45}
ry={3}
fill='#000'
fillOpacity={0.06}
/>
<Path
opacity={0.4}
d='M37.593 38.008c4.754-4.815 10.754-7.295 17.989-7.428 7.101.133 13.065 2.601 17.892 7.428 4.815 4.827 7.295 10.791 7.428 17.892-.133 7.235-2.601 13.223-7.428 17.99-4.827 4.754-10.791 7.27-17.892 7.512-7.235-.254-13.223-2.758-17.99-7.513-4.754-4.766-7.258-10.766-7.512-18 .254-7.102 2.758-13.066 7.513-17.881z'
fill='#fff'
/>
<Path
d='M78.887 51.382c-2.152-6.992-6.225-12.225-12.226-15.69-6.001-3.465-12.57-4.376-19.701-2.744-3.9.996-7.297 2.718-10.22 5.163 3.269-3.567 7.415-6.037 12.428-7.416 7.13-1.633 13.732-.703 19.787 2.793s10.16 8.748 12.313 15.74c1.322 5.037 1.256 9.862-.21 14.47-1.455 4.614-4.067 8.49-7.84 11.611 2.833-3.087 4.783-6.713 5.844-10.894 1.05-4.187.991-8.523-.175-13.033z'
fill='#000'
fillOpacity={0.4}
/>
<Path
d='M86.76 53.929c-.508-7.506-3.554-14.097-9.126-19.774-6.345-6.05-13.67-9.08-21.973-9.08-8.303 0-15.616 3.03-21.961 9.08-6.08 6.315-9.126 13.591-9.126 21.855 0 8.262 3.046 15.551 9.126 21.854 5.825 5.556 12.485 8.551 19.967 8.984 7.481.445 14.383-1.611 20.728-6.146l4.75 4.727 6.08-6.05-4.75-4.727c4.69-6.302 6.78-13.218 6.285-20.723zm-13.126 19.87c-4.823 4.726-10.782 7.228-17.876 7.468-7.228-.252-13.211-2.742-17.973-7.469-4.75-4.727-7.252-10.692-7.506-17.885.254-7.06 2.756-12.99 7.506-17.789 4.75-4.787 10.745-7.252 17.973-7.385 7.094.133 13.053 2.586 17.876 7.385 4.81 4.8 7.288 10.73 7.42 17.79-.132 7.192-2.598 13.157-7.42 17.884z'
fill='#BABEC9'
/>
<Path
d='M106.202 114.187c-1.568.448-2.728.291-3.482-.472L78.06 86.651c-.754-.763-1.065-1.743-.945-2.954s.873-2.567 2.261-4.093c1.508-1.393 2.848-2.192 4.044-2.386 1.197-.193 2.166.158 2.92 1.054l26.921 24.957c.754.763.874 1.901.371 3.427-.502 1.525-1.448 3.051-2.824 4.577-1.495 1.526-3.039 2.506-4.606 2.954z'
fill='#FFBC1F'
/>
<Path
d='M108.007 98.343l-10.08 10.164-12.155-13.34 8.915-9.106 13.32 12.281z'
fill='#7A5600'
/>
</Svg>
);
}
export default SearchIllustration;

View File

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

View File

@@ -236,9 +236,9 @@ export default function BrowseChannels(props: Props) {
<ChannelList
channels={channels}
onEndReached={onEndReached}
isSearch={Boolean(term)}
loading={loading}
onSelectChannel={onSelectChannel}
term={term}
/>
</>
);

View File

@@ -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 (
<Loading
containerStyle={style.loadingContainer}
@@ -89,23 +94,19 @@ export default function ChannelList({
);
//Style is covered by the theme
}, [theme]);
}, [loading, theme]);
const renderNoResults = useCallback(() => {
if (isSearch) {
if (term) {
return (
<View style={style.noResultContainer}>
<FormattedText
id='mobile.custom_list.no_results'
defaultMessage='No Results'
style={style.noResultText}
/>
<View style={noResutsStyle}>
<NoResultsWithTerm term={term}/>
</View>
);
}
return (
<View style={style.noResultContainer}>
<View style={noResutsStyle}>
<FormattedText
id='browse_channels.noMore'
defaultMessage='No more channels to join'
@@ -113,7 +114,7 @@ export default function ChannelList({
/>
</View>
);
}, [style, isSearch]);
}, [style, term, noResutsStyle]);
const renderSeparator = useCallback(() => (
<View
@@ -126,9 +127,9 @@ export default function ChannelList({
data={channels}
renderItem={renderItem}
testID='browse_channels.channel_list.flat_list'
ListEmptyComponent={loading ? renderLoading : renderNoResults}
ListEmptyComponent={renderNoResults}
ListFooterComponent={renderLoading}
onEndReached={onEndReached}
ListFooterComponent={loading && channels.length ? renderLoading : null}
contentContainerStyle={style.listContainer}
ItemSeparatorComponent={renderSeparator}
keyExtractor={channelKeyExtractor}

View File

@@ -389,13 +389,13 @@ export default function CreateDirectMessage({
<UserList
currentUserId={currentUserId}
handleSelectProfile={handleSelectProfile}
isSearch={isSearch}
loading={loading}
profiles={data}
selectedIds={selectedIds}
showNoResults={!loading && page.current !== -1}
teammateNameDisplay={teammateNameDisplay}
fetchMore={getProfiles}
term={term}
testID='create_direct_message.user_list'
/>
</SafeAreaView>

View File

@@ -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<UserProfile>) => {
// 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 (
<View style={style.loadingContainer}>
<FormattedText
id='mobile.loading_members'
defaultMessage='Loading Members...'
style={style.loadingText}
/>
</View>
<Loading
containerStyle={style.loadingContainer}
style={style.loading}
color={theme.buttonBg}
/>
);
}, [loading && style]);
}, [loading, theme]);
const renderNoResults = useCallback(() => {
if (!showNoResults) {
if (!showNoResults || !term) {
return null;
}
return (
<View style={style.noResultContainer}>
<FormattedText
id='mobile.custom_list.no_results'
defaultMessage='No Results'
style={style.noResultText}
/>
<View style={noResutsStyle}>
<NoResultsWithTerm term={term}/>
</View>
);
}, [showNoResults && style]);
}, [showNoResults && style, term, noResutsStyle]);
const renderSectionHeader = useCallback(({section}: {section: SectionListData<UserProfile>}) => {
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<SectionListData<UserProfile>>);

View File

@@ -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}.",