move search and search functionality into the screen and pass the result

to the users modal for rendering
This commit is contained in:
Jason Frerich
2022-11-14 05:38:54 -06:00
parent 5d0fb1444c
commit eb76543229
2 changed files with 126 additions and 108 deletions

View File

@@ -1,22 +1,18 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useEffect, useMemo, useReducer, useRef, useState} from 'react';
import React, {forwardRef, MutableRefObject, useCallback, useEffect, useMemo, useReducer, useRef, useState} from 'react';
import {MessageDescriptor, useIntl} from 'react-intl';
import {Keyboard, Platform, StyleSheet, View} from 'react-native';
import {SafeAreaView} from 'react-native-safe-area-context';
import CompassIcon from '@components/compass_icon';
import Loading from '@components/loading';
import Search from '@components/search';
import SelectedUsersPanel from '@components/selected_users_panel';
import UserList from '@components/user_list';
import {General} from '@constants';
import {useTheme} from '@context/theme';
import {debounce} from '@helpers/api/general';
import useNavButtonPressed from '@hooks/navigation_button_pressed';
import {dismissModal, setButtons} from '@screens/navigation';
import {changeOpacity, getKeyboardAppearanceFromTheme} from '@utils/theme';
import {filterProfilesMatchingTerm} from '@utils/user';
const style = StyleSheet.create({
@@ -38,16 +34,6 @@ const close = () => {
dismissModal();
};
type searchError = {
data?: undefined;
error: unknown;
}
type searchSuccess = {
data: UserProfile[];
error?: undefined;
}
type getProfilesError = {
users?: undefined;
error: unknown;
@@ -60,17 +46,19 @@ type getProfilesSuccess = {
type Props = {
buttonText: MessageDescriptor;
clearSearch: () => void;
componentId: string;
currentUserId: string;
getProfiles: () => Promise<getProfilesSuccess | getProfilesError>;
loading: boolean;
maxSelectedUsers: number;
page: number;
searchUsers: (searchTerm: string) => Promise<searchError | searchSuccess>;
selectedIds: {[id: string]: UserProfile};
setPage: (page: number) => void;
setSelectedIds: (ids: {[id: string]: UserProfile}) => void;
onButtonTap: (selectedId?: {[id: string]: boolean}) => Promise<boolean>;
searchResults: UserProfile[];
selectedIds: {[id: string]: UserProfile};
setLoading: (loading: boolean) => void;
setSelectedIds: (ids: {[id: string]: UserProfile}) => void;
teammateNameDisplay: string;
term: string;
tutorialWatched: boolean;
}
@@ -81,58 +69,53 @@ function reduceProfiles(state: UserProfile[], action: {type: 'add'; values?: Use
return state;
}
const UsersModal = ({
const UsersModal = forwardRef(({
buttonText,
clearSearch,
componentId,
currentUserId,
getProfiles,
loading,
maxSelectedUsers,
page,
searchUsers,
selectedIds,
setPage,
setSelectedIds,
onButtonTap,
searchResults,
selectedIds,
setLoading,
setSelectedIds,
teammateNameDisplay,
term,
tutorialWatched,
}: Props) => {
}: Props, pageRef: MutableRefObject<number>) => {
const theme = useTheme();
const {formatMessage, locale} = useIntl();
const searchTimeoutId = useRef<NodeJS.Timeout | null>(null);
const mounted = useRef(false);
const next = useRef(true);
const selectedCount = Object.keys(selectedIds).length;
const [term, setTerm] = useState('');
const [startingButtonAction, setStartingButtonAction] = useState(false);
const [searchResults, setSearchResults] = useState<UserProfile[]>([]);
const [profiles, dispatchProfiles] = useReducer(reduceProfiles, []);
const [loading, setLoading] = useState(false);
const isSearch = Boolean(term);
let hasUsers = false;
if (pageRef != null && typeof pageRef !== 'function') {
hasUsers = pageRef?.current !== -1;
}
const loadedProfiles = useCallback(({users}: {users?: UserProfile[]}) => {
if (mounted.current) {
if (users && !users.length) {
next.current = false;
}
setPage(page + 1);
if (pageRef != null && typeof pageRef !== 'function') {
pageRef.current += 1;
}
setLoading(false);
dispatchProfiles({type: 'add', values: users});
}
}, [page, setPage]);
const handleSearchUsers = useCallback(async (searchTerm: string) => {
setLoading(true);
const results = await searchUsers(searchTerm);
setSearchResults(results?.data || []);
setLoading(false);
}, [searchUsers]);
}, []);
const handleButtonTap = useCallback(async (selectedId?: {[id: string]: boolean}) => {
if (startingButtonAction) {
@@ -159,31 +142,6 @@ const UsersModal = ({
}
}, 100), [getProfiles, loading, term]);
const search = useCallback(() => {
handleSearchUsers(term);
}, [handleSearchUsers, term]);
const clearSearch = useCallback(() => {
setTerm('');
setSearchResults([]);
}, []);
const onSearch = useCallback((text: string) => {
if (text) {
setTerm(text);
if (searchTimeoutId.current) {
clearTimeout(searchTimeoutId.current);
}
searchTimeoutId.current = setTimeout(() => {
handleSearchUsers(text);
}, General.SEARCH_TIMEOUT_MILLISECONDS);
return;
}
clearSearch();
}, [clearSearch, handleSearchUsers]);
const handleRemoveProfile = useCallback((id: string) => {
const newSelectedIds = Object.assign({}, selectedIds);
@@ -285,24 +243,7 @@ const UsersModal = ({
}
return (
<SafeAreaView
style={style.container}
testID='members_modal.screen'
>
<View style={style.searchBar}>
<Search
testID='members_modal.search_bar'
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
cancelButtonTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
onChangeText={onSearch}
onSubmitEditing={search}
onCancel={clearSearch}
autoCapitalize='none'
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
value={term}
/>
</View>
<>
{selectedCount > 0 &&
<SelectedUsersPanel
selectedIds={selectedIds}
@@ -319,14 +260,15 @@ const UsersModal = ({
loading={loading}
profiles={data}
selectedIds={selectedIds}
showNoResults={!loading && page !== -1}
showNoResults={!loading && hasUsers}
teammateNameDisplay={teammateNameDisplay}
term={term}
testID='members_modal.user_list'
tutorialWatched={tutorialWatched}
/>
</SafeAreaView>
</>
);
};
});
UsersModal.displayName = 'UsersModal';
export default UsersModal;

View File

@@ -1,18 +1,33 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useState} from 'react';
import React, {useCallback, useRef, useState} from 'react';
import {defineMessages, useIntl} from 'react-intl';
import {Platform, SafeAreaView, StyleSheet, View} from 'react-native';
import {makeDirectChannel, makeGroupChannel} from '@actions/remote/channel';
import {fetchProfiles, fetchProfilesInTeam, searchProfiles} from '@actions/remote/user';
import Search from '@components/search';
import UsersModal from '@components/users_modal';
import {General} from '@constants';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {t} from '@i18n';
import {alertErrorWithFallback} from '@utils/draft';
import {changeOpacity, getKeyboardAppearanceFromTheme} from '@utils/theme';
import {displayUsername} from '@utils/user';
const style = StyleSheet.create({
container: {
flex: 1,
},
searchBar: {
marginLeft: 12,
marginRight: Platform.select({ios: 4, default: 12}),
marginVertical: 12,
},
});
const messages = defineMessages({
dm: {
id: t('mobile.open_dm.error'),
@@ -44,10 +59,16 @@ export default function CreateDirectMessage({
teammateNameDisplay,
}: Props) {
const serverUrl = useServerUrl();
const theme = useTheme();
const intl = useIntl();
const [selectedIds, setSelectedIds] = useState<{[id: string]: UserProfile}>({});
const [page, setPage] = useState(-1);
const [searchResults, setSearchResults] = useState<UserProfile[]>([]);
const [term, setTerm] = useState('');
const [loading, setLoading] = useState(false);
const pageRef = useRef<number>(-1);
const searchTimeoutId = useRef<NodeJS.Timeout | null>(null);
const createDirectChannel = useCallback(async (id: string): Promise<boolean> => {
const user = selectedIds[id];
@@ -70,9 +91,9 @@ export default function CreateDirectMessage({
const getProfiles = useCallback(async () => {
if (restrictDirectMessage) {
return fetchProfilesInTeam(serverUrl, currentTeamId, page + 1, General.PROFILE_CHUNK_SIZE);
return fetchProfilesInTeam(serverUrl, currentTeamId, pageRef.current + 1, General.PROFILE_CHUNK_SIZE);
}
return fetchProfiles(serverUrl, page + 1, General.PROFILE_CHUNK_SIZE);
return fetchProfiles(serverUrl, pageRef.current + 1, General.PROFILE_CHUNK_SIZE);
}, [restrictDirectMessage, serverUrl, currentTeamId]);
const onButtonTap = useCallback(async (selectedId?: {[id: string]: boolean}) => {
@@ -91,21 +112,76 @@ export default function CreateDirectMessage({
return searchProfiles(serverUrl, lowerCasedTerm, {allow_inactive: true});
}, [restrictDirectMessage, serverUrl, currentTeamId]);
const handleSearchUsers = useCallback(async (searchTerm: string) => {
setLoading(true);
const results = await searchUsers(searchTerm);
setSearchResults(results?.data || []);
setLoading(false);
}, [searchUsers]);
const search = useCallback(() => {
handleSearchUsers(term);
}, [handleSearchUsers, term]);
const clearSearch = useCallback(() => {
setTerm('');
setSearchResults([]);
}, []);
const onSearch = useCallback((text: string) => {
if (text) {
setTerm(text);
if (searchTimeoutId.current) {
clearTimeout(searchTimeoutId.current);
}
searchTimeoutId.current = setTimeout(() => {
handleSearchUsers(text);
}, General.SEARCH_TIMEOUT_MILLISECONDS);
return;
}
clearSearch();
}, [clearSearch, handleSearchUsers]);
return (
<UsersModal
buttonText={messages.button}
componentId={componentId}
currentUserId={currentUserId}
getProfiles={getProfiles}
maxSelectedUsers={General.MAX_USERS_IN_GM}
page={page}
searchUsers={searchUsers}
selectedIds={selectedIds}
setPage={setPage}
setSelectedIds={setSelectedIds}
teammateNameDisplay={teammateNameDisplay}
onButtonTap={onButtonTap}
/>
<SafeAreaView
style={style.container}
testID='members_modal.screen'
>
<View style={style.searchBar}>
<Search
testID='members_modal.search_bar'
placeholder={intl.formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
cancelButtonTitle={intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
onChangeText={onSearch}
onSubmitEditing={search}
onCancel={clearSearch}
autoCapitalize='none'
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
value={term}
/>
</View>
<UsersModal
ref={pageRef}
term={term}
clearSearch={clearSearch}
buttonText={messages.button}
componentId={componentId}
currentUserId={currentUserId}
getProfiles={getProfiles}
loading={loading}
setLoading={setLoading}
maxSelectedUsers={General.MAX_USERS_IN_GM}
searchResults={searchResults}
selectedIds={selectedIds}
setSelectedIds={setSelectedIds}
teammateNameDisplay={teammateNameDisplay}
onButtonTap={onButtonTap}
/>
</SafeAreaView>
);
}