forked from Ivasoft/mattermost-mobile
[Gekidou] new UI for no results found (#6165)
* new UI for no results found * feedback review
This commit is contained in:
@@ -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};
|
||||
|
||||
59
app/components/no_results_with_term/index.tsx
Normal file
59
app/components/no_results_with_term/index.tsx
Normal 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;
|
||||
49
app/components/no_results_with_term/search_illustration.tsx
Normal file
49
app/components/no_results_with_term/search_illustration.tsx
Normal 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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -236,9 +236,9 @@ export default function BrowseChannels(props: Props) {
|
||||
<ChannelList
|
||||
channels={channels}
|
||||
onEndReached={onEndReached}
|
||||
isSearch={Boolean(term)}
|
||||
loading={loading}
|
||||
onSelectChannel={onSelectChannel}
|
||||
term={term}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>>);
|
||||
|
||||
@@ -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}.",
|
||||
|
||||
Reference in New Issue
Block a user