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