Files
mattermost-mobile/app/screens/manage_channel_members/manage_channel_members.tsx
2023-05-23 11:33:17 -04:00

301 lines
11 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {defineMessages, useIntl} from 'react-intl';
import {DeviceEventEmitter, Keyboard, Platform, StyleSheet, View} from 'react-native';
import {SafeAreaView} from 'react-native-safe-area-context';
import {fetchChannelMemberships} from '@actions/remote/channel';
import {fetchUsersByIds, searchProfiles} from '@actions/remote/user';
import {PER_PAGE_DEFAULT} from '@client/rest/constants';
import Search from '@components/search';
import UserList from '@components/user_list';
import {Events, General, Screens} from '@constants';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import useNavButtonPressed from '@hooks/navigation_button_pressed';
import {openAsBottomSheet, setButtons} from '@screens/navigation';
import NavigationStore from '@store/navigation_store';
import {showRemoveChannelUserSnackbar} from '@utils/snack_bar';
import {changeOpacity, getKeyboardAppearanceFromTheme} from '@utils/theme';
import {displayUsername, filterProfilesMatchingTerm} from '@utils/user';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
canManageAndRemoveMembers: boolean;
channelId: string;
componentId: AvailableScreens;
currentTeamId: string;
currentUserId: string;
tutorialWatched: boolean;
teammateDisplayNameSetting: string;
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
searchBar: {
marginLeft: 12,
marginRight: Platform.select({ios: 4, default: 12}),
marginVertical: 12,
},
});
const messages = defineMessages({
button_manage: {
id: 'mobile.manage_members.manage',
defaultMessage: 'Manage',
},
button_done: {
id: 'mobile.manage_members.done',
defaultMessage: 'Done',
},
});
const sortUsers = (a: UserProfile, b: UserProfile, locale: string, teammateDisplayNameSetting: string) => {
const aName = displayUsername(a, locale, teammateDisplayNameSetting);
const bName = displayUsername(b, locale, teammateDisplayNameSetting);
return aName.localeCompare(bName, locale);
};
const MANAGE_BUTTON = 'manage-button';
const EMPTY: UserProfile[] = [];
const EMPTY_MEMBERS: ChannelMembership[] = [];
const EMPTY_IDS = {};
const {USER_PROFILE} = Screens;
const CLOSE_BUTTON_ID = 'close-user-profile';
export default function ManageChannelMembers({
canManageAndRemoveMembers,
channelId,
componentId,
currentTeamId,
currentUserId,
tutorialWatched,
teammateDisplayNameSetting,
}: Props) {
const serverUrl = useServerUrl();
const theme = useTheme();
const {formatMessage, locale} = useIntl();
const searchTimeoutId = useRef<NodeJS.Timeout | null>(null);
const mounted = useRef(false);
const [isManageMode, setIsManageMode] = useState(false);
const [profiles, setProfiles] = useState<UserProfile[]>(EMPTY);
const hasMoreProfiles = useRef(false);
const [channelMembers, setChannelMembers] = useState<ChannelMembership[]>(EMPTY_MEMBERS);
const [searchResults, setSearchResults] = useState<UserProfile[]>(EMPTY);
const [loading, setLoading] = useState(true);
const [term, setTerm] = useState('');
const [searchedTerm, setSearchedTerm] = useState('');
const clearSearch = useCallback(() => {
setTerm('');
setSearchResults(EMPTY);
}, []);
const handleSelectProfile = useCallback(async (profile: UserProfile) => {
if (profile.id === currentUserId && isManageMode) {
return;
}
if (profile.id !== currentUserId) {
await fetchUsersByIds(serverUrl, [profile.id]);
}
const title = formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
const props = {
channelId,
closeButtonId: CLOSE_BUTTON_ID,
location: USER_PROFILE,
manageMode: isManageMode,
userId: profile.id,
canManageAndRemoveMembers,
};
Keyboard.dismiss();
openAsBottomSheet({screen: USER_PROFILE, title, theme, closeButtonId: CLOSE_BUTTON_ID, props});
}, [canManageAndRemoveMembers, channelId, isManageMode, currentUserId]);
const searchUsers = useCallback(async (searchTerm: string) => {
setSearchedTerm(searchTerm);
if (!hasMoreProfiles.current) {
return;
}
const lowerCasedTerm = searchTerm.toLowerCase();
setLoading(true);
const options: SearchUserOptions = {team_id: currentTeamId, in_channel_id: channelId, allow_inactive: false};
const {data = EMPTY} = await searchProfiles(serverUrl, lowerCasedTerm, options);
setSearchResults(data.sort((a, b) => sortUsers(a, b, locale, teammateDisplayNameSetting)));
setLoading(false);
}, [serverUrl, channelId, currentTeamId, locale, teammateDisplayNameSetting]);
const search = useCallback(() => {
searchUsers(term);
}, [searchUsers, term]);
const onSearch = useCallback((text: string) => {
if (!text) {
clearSearch();
return;
}
setTerm(text);
if (searchTimeoutId.current) {
clearTimeout(searchTimeoutId.current);
}
searchTimeoutId.current = setTimeout(() => {
searchUsers(text);
}, General.SEARCH_TIMEOUT_MILLISECONDS);
}, [searchUsers, clearSearch]);
const updateNavigationButtons = useCallback((manage: boolean) => {
setButtons(componentId, {
rightButtons: [{
color: theme.sidebarHeaderTextColor,
enabled: true,
id: MANAGE_BUTTON,
showAsAction: 'always',
testID: 'manage_members.button',
text: formatMessage(manage ? messages.button_done : messages.button_manage),
}],
});
}, [theme.sidebarHeaderTextColor]);
const toggleManageEnabled = useCallback(() => {
updateNavigationButtons(!isManageMode);
setIsManageMode((prev) => !prev);
}, [isManageMode, updateNavigationButtons]);
const handleRemoveUser = useCallback(async (userId: string) => {
const pIndex = profiles.findIndex((user) => user.id === userId);
const mIndex = channelMembers.findIndex((m) => m.user_id === userId);
if (pIndex !== -1) {
const newProfiles = [...profiles];
newProfiles.splice(pIndex, 1);
setProfiles(newProfiles);
const newMembers = [...channelMembers];
newMembers.splice(mIndex, 1);
setChannelMembers(newMembers);
await NavigationStore.waitUntilScreensIsRemoved(USER_PROFILE);
showRemoveChannelUserSnackbar();
}
}, [profiles, channelMembers]);
const handleUserChangeRole = useCallback(async ({userId, schemeAdmin}: {userId: string; schemeAdmin: boolean}) => {
const clone = channelMembers.map((m) => {
if (m.user_id === userId) {
m.scheme_admin = schemeAdmin;
return m;
}
return m;
});
setChannelMembers(clone);
}, [channelMembers]);
const sortedProfiles = useMemo(() => [...profiles].sort((a, b) => {
return sortUsers(a, b, locale, teammateDisplayNameSetting);
}), [profiles, locale, teammateDisplayNameSetting]);
const data = useMemo(() => {
const isSearch = Boolean(searchedTerm);
if (isSearch) {
return filterProfilesMatchingTerm(searchResults.length ? searchResults : sortedProfiles, searchedTerm);
}
return profiles;
}, [searchResults, profiles, searchedTerm, sortedProfiles]);
useEffect(() => {
if (!term) {
setSearchResults(EMPTY);
setSearchedTerm('');
}
}, [Boolean(term)]);
useNavButtonPressed(MANAGE_BUTTON, componentId, toggleManageEnabled, [toggleManageEnabled]);
useEffect(() => {
mounted.current = true;
const options: GetUsersOptions = {sort: 'admin', active: true, per_page: PER_PAGE_DEFAULT};
fetchChannelMemberships(serverUrl, channelId, options, true).then(({users, members}) => {
if (!mounted.current) {
return;
}
if (users.length >= PER_PAGE_DEFAULT) {
hasMoreProfiles.current = true;
}
if (users.length) {
setProfiles(users);
setChannelMembers(members);
}
setLoading(false);
});
return () => {
mounted.current = false;
};
}, []);
useEffect(() => {
if (canManageAndRemoveMembers) {
updateNavigationButtons(false);
}
}, [canManageAndRemoveMembers]);
useEffect(() => {
const removeUserListener = DeviceEventEmitter.addListener(Events.REMOVE_USER_FROM_CHANNEL, handleRemoveUser);
const changeUserRoleListener = DeviceEventEmitter.addListener(Events.MANAGE_USER_CHANGE_ROLE, handleUserChangeRole);
return (() => {
removeUserListener?.remove();
changeUserRoleListener?.remove();
});
}, [handleRemoveUser, handleUserChangeRole]);
return (
<SafeAreaView
style={styles.container}
testID='manage_members.screen'
>
<View style={styles.searchBar}>
<Search
autoCapitalize='none'
cancelButtonTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
onCancel={clearSearch}
onChangeText={onSearch}
onSubmitEditing={search}
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
testID='manage_members.search_bar'
value={term}
/>
</View>
<UserList
currentUserId={currentUserId}
handleSelectProfile={handleSelectProfile}
loading={loading}
manageMode={true} // default true to change row select icon to a dropdown
profiles={data}
channelMembers={channelMembers}
selectedIds={EMPTY_IDS}
showManageMode={canManageAndRemoveMembers && isManageMode}
showNoResults={!loading}
term={searchedTerm}
testID='manage_members.user_list'
tutorialWatched={tutorialWatched}
includeUserMargin={true}
/>
</SafeAreaView>
);
}