forked from Ivasoft/mattermost-mobile
move search and search functionality into the screen and pass the result
to the users modal for rendering
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user