Add selected users panel

This commit is contained in:
Jason Frerich
2022-12-03 13:12:36 -06:00
parent 205fe2dae9
commit 84a6443042
5 changed files with 105 additions and 100 deletions

View File

@@ -7,7 +7,6 @@ import Animated, {useAnimatedStyle, useDerivedValue, useSharedValue, withTiming}
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import Toast from '@components/toast';
import {General} from '@constants';
import {useTheme} from '@context/theme';
import {useIsTablet, useKeyboardHeightWithDuration} from '@hooks/device';
import Button from '@screens/bottom_sheet/button';
@@ -275,7 +274,6 @@ export default function SelectedUsers({
onPress={handlePress}
icon={buttonIcon}
text={buttonText}
disabled={numberSelectedIds > General.MAX_USERS_IN_GM}
/>
</Animated.View>
</Animated.View>

View File

@@ -29,6 +29,7 @@ export default {
SHOW_FULLNAME: 'full_name',
},
SPECIAL_MENTIONS: new Set(['all', 'channel', 'here']),
MAX_USERS_ADD_TO_CHANNEL: 25,
MAX_USERS_IN_GM: 7,
MIN_USERS_IN_GM: 3,
MAX_GROUP_CHANNELS_FOR_PROFILES: 50,

View File

@@ -3,30 +3,27 @@
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {defineMessages, useIntl} from 'react-intl';
import {Keyboard, Platform, StyleSheet, View} from 'react-native';
import {Keyboard, LayoutChangeEvent, Platform, StyleSheet, View} from 'react-native';
import {SafeAreaView} from 'react-native-safe-area-context';
// import SelectedUsers from '@components/selected_users_panel';
import {addMembersToChannel} from '@actions/remote/channel';
import {fetchProfilesNotInChannel, searchProfiles} from '@actions/remote/user';
import Loading from '@components/loading';
import Search from '@components/search';
import SelectedUsers from '@components/selected_users';
import UserList from '@components/user_list';
import {General} from '@constants';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {ChannelModel} from '@database/models/server';
import {debounce} from '@helpers/api/general';
import useDidUpdate from '@hooks/did_update';
import useNavButtonPressed from '@hooks/navigation_button_pressed';
import {useModalPosition} from '@hooks/device';
import {t} from '@i18n';
import {popTopScreen, setButtons} from '@screens/navigation';
import {popTopScreen} from '@screens/navigation';
import {alertErrorWithFallback} from '@utils/draft';
import {changeOpacity, getKeyboardAppearanceFromTheme} from '@utils/theme';
import {filterProfilesMatchingTerm} from '@utils/user';
const ADD_BUTTON = 'add-button';
const close = () => {
Keyboard.dismiss();
popTopScreen();
@@ -52,6 +49,10 @@ const messages = defineMessages({
id: t('mobile.channel_add_people.title'),
defaultMessage: 'Add Members',
},
toastMessage: {
id: t('mobile.channel_add_people.max_limit_reached'),
defaultMessage: 'Max selected users are limited to {maxCount} members',
},
});
type Props = {
@@ -64,8 +65,17 @@ type Props = {
tutorialWatched: boolean;
}
const MAX_SELECTED_USERS = General.MAX_USERS_ADD_TO_CHANNEL;
function removeProfileFromList(list: {[id: string]: UserProfile}, id: string) {
const newSelectedIds = Object.assign({}, list);
Reflect.deleteProperty(newSelectedIds, id);
return newSelectedIds;
}
export default function ChannelAddPeople({
componentId,
// componentId,
currentChannel,
currentTeamId,
currentUserId,
@@ -82,6 +92,8 @@ export default function ChannelAddPeople({
const next = useRef(true);
const page = useRef(-1);
const mounted = useRef(false);
const mainView = useRef<View>(null);
const modalPosition = useModalPosition(mainView);
const [profiles, setProfiles] = useState<UserProfile[]>([]);
const [searchResults, setSearchResults] = useState<UserProfile[]>([]);
@@ -89,6 +101,9 @@ export default function ChannelAddPeople({
const [term, setTerm] = useState('');
const [startingAddPeople, setStartingAddPeople] = useState(false);
const [selectedIds, setSelectedIds] = useState<{[id: string]: UserProfile}>({});
const [containerHeight, setContainerHeight] = useState(0);
const [showToast, setShowToast] = useState(false);
const selectedCount = Object.keys(selectedIds).length;
const groupConstrained = currentChannel.isGroupConstrained;
const currentChannelId = currentChannel.id;
@@ -121,11 +136,7 @@ export default function ChannelAddPeople({
}, 100), [loading, isSearch, serverUrl, currentTeamId]);
const handleRemoveProfile = useCallback((id: string) => {
const newSelectedIds = Object.assign({}, selectedIds);
Reflect.deleteProperty(newSelectedIds, id);
setSelectedIds(newSelectedIds);
setSelectedIds((current) => removeProfileFromList(current, id));
}, [selectedIds]);
const addPeopleToChannel = useCallback(async (ids: string[]): Promise<boolean> => {
@@ -167,17 +178,27 @@ export default function ChannelAddPeople({
}, [startingAddPeople, selectedIds, addPeopleToChannel]);
const handleSelectProfile = useCallback((user: UserProfile) => {
if (selectedIds[user.id]) {
handleRemoveProfile(user.id);
return;
}
const newSelectedIds = Object.assign({}, selectedIds);
newSelectedIds[user.id] = user;
setSelectedIds(newSelectedIds);
clearSearch();
}, [selectedIds, handleRemoveProfile, startAddPeople, clearSearch]);
setSelectedIds((current) => {
if (current[user.id]) {
return removeProfileFromList(current, user.id);
}
const wasSelected = current[user.id];
if (!wasSelected && selectedCount >= MAX_SELECTED_USERS) {
setShowToast(true);
return current;
}
const newSelectedIds = Object.assign({}, current);
if (!wasSelected) {
newSelectedIds[user.id] = user;
}
return newSelectedIds;
});
}, [clearSearch, selectedIds, startAddPeople]);
const searchUsers = useCallback(async (searchTerm: string) => {
const lowerCasedTerm = searchTerm.toLowerCase();
@@ -218,36 +239,21 @@ export default function ChannelAddPeople({
}
}, [searchUsers, clearSearch]);
const updateNavigationButtons = useCallback(async (startEnabled: boolean) => {
if (hasProfiles) {
setButtons(componentId, {
rightButtons: [{
color: theme.sidebarHeaderTextColor,
id: ADD_BUTTON,
text: formatMessage({id: 'mobile.channel_add_people.button', defaultMessage: 'Add'}),
showAsAction: 'always',
enabled: startEnabled,
testID: 'add_members.start.button',
}],
});
}
}, [intl.locale, hasProfiles, theme]);
useNavButtonPressed(ADD_BUTTON, componentId, startAddPeople, [startAddPeople]);
useEffect(() => {
mounted.current = true;
updateNavigationButtons(false);
getProfiles();
return () => {
mounted.current = false;
};
}, []);
useDidUpdate(() => {
const canStart = selectedCount > 0 && !startingAddPeople;
updateNavigationButtons(canStart);
}, [selectedCount > 0, startingAddPeople, updateNavigationButtons]);
useEffect(() => {
setShowToast(selectedCount >= MAX_SELECTED_USERS);
}, [selectedCount >= MAX_SELECTED_USERS]);
const onLayout = useCallback((e: LayoutChangeEvent) => {
setContainerHeight(e.nativeEvent.layout.height);
}, []);
const data = useMemo(() => {
if (isSearch) {
@@ -283,6 +289,8 @@ export default function ChannelAddPeople({
<SafeAreaView
style={style.container}
testID='add_members.screen'
onLayout={onLayout}
edges={['top', 'left', 'right']}
>
{hasProfiles &&
<View style={style.searchBar}>
@@ -300,20 +308,6 @@ export default function ChannelAddPeople({
/>
</View>
}
{/*
https://mattermost.atlassian.net/browse/MM-48489
V1 does not have the selected users modal.
Add this back in after build the scrollable selectable users panel
*/}
{/* {selectedCount > 0 &&
<SelectedUsers
selectedIds={selectedIds}
warnCount={General.MAX_ADD_USERS - 2}
maxCount={General.MAX_ADD_USERS}
onRemove={handleRemoveProfile}
teammateNameDisplay={teammateNameDisplay}
/>
} */}
<UserList
currentUserId={currentUserId}
handleSelectProfile={handleSelectProfile}
@@ -327,6 +321,20 @@ export default function ChannelAddPeople({
testID='add_members.user_list'
tutorialWatched={tutorialWatched}
/>
<SelectedUsers
containerHeight={containerHeight}
modalPosition={modalPosition}
showToast={showToast}
setShowToast={setShowToast}
toastIcon={'check'}
toastMessage={formatMessage(messages.toastMessage, {maxCount: MAX_SELECTED_USERS})}
selectedIds={selectedIds}
onRemove={handleRemoveProfile}
teammateNameDisplay={teammateNameDisplay}
onPress={startAddPeople}
buttonIcon={'account-plus-outline'}
buttonText={formatMessage(messages.button)}
/>
</SafeAreaView>
);
}

View File

@@ -1,7 +1,7 @@
// 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, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {defineMessages, useIntl} from 'react-intl';
import {Keyboard, LayoutChangeEvent, Platform, View} from 'react-native';
import {SafeAreaView} from 'react-native-safe-area-context';
@@ -56,6 +56,9 @@ type Props = {
tutorialWatched: boolean;
}
const MAX_SELECTED_USERS = General.MAX_USERS_IN_GM;
const EMPTY: UserProfile[] = [];
const close = () => {
Keyboard.dismiss();
dismissModal();
@@ -93,13 +96,6 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
};
});
function reduceProfiles(state: UserProfile[], action: {type: 'add'; values?: UserProfile[]}) {
if (action.type === 'add' && action.values?.length) {
return [...state, ...action.values];
}
return state;
}
function removeProfileFromList(list: {[id: string]: UserProfile}, id: string) {
const newSelectedIds = Object.assign({}, list);
@@ -128,8 +124,8 @@ export default function CreateDirectMessage({
const mainView = useRef<View>(null);
const modalPosition = useModalPosition(mainView);
const [profiles, dispatchProfiles] = useReducer(reduceProfiles, []);
const [searchResults, setSearchResults] = useState<UserProfile[]>([]);
const [profiles, setProfiles] = useState<UserProfile[]>(EMPTY);
const [searchResults, setSearchResults] = useState<UserProfile[]>(EMPTY);
const [loading, setLoading] = useState(false);
const [term, setTerm] = useState('');
const [startingConversation, setStartingConversation] = useState(false);
@@ -140,7 +136,7 @@ export default function CreateDirectMessage({
const isSearch = Boolean(term);
const loadedProfiles = ({users}: {users?: UserProfile[]}) => {
const loadedProfiles = ({users}: {users: UserProfile[]}) => {
if (mounted.current) {
if (users && !users.length) {
next.current = false;
@@ -148,13 +144,13 @@ export default function CreateDirectMessage({
page.current += 1;
setLoading(false);
dispatchProfiles({type: 'add', values: users});
setProfiles((prev: UserProfile[]) => [...prev, ...users]);
}
};
const data = useMemo(() => {
if (term) {
const exactMatches: UserProfile[] = [];
const exactMatches: UserProfile[] = EMPTY;
const filterByTerm = (p: UserProfile) => {
if (selectedCount > 0 && p.id === currentUserId) {
return false;
@@ -247,29 +243,30 @@ export default function CreateDirectMessage({
};
startConversation(selectedId);
} else {
clearSearch();
setSelectedIds((current) => {
if (current[user.id]) {
return removeProfileFromList(current, user.id);
}
const wasSelected = current[user.id];
if (!wasSelected && selectedCount >= General.MAX_USERS_IN_GM) {
setShowToast(true);
return current;
}
const newSelectedIds = Object.assign({}, current);
if (!wasSelected) {
newSelectedIds[user.id] = user;
}
return newSelectedIds;
});
return;
}
}, [currentUserId, clearSearch]);
clearSearch();
setSelectedIds((current) => {
if (current[user.id]) {
return removeProfileFromList(current, user.id);
}
const wasSelected = current[user.id];
if (!wasSelected && selectedCount >= MAX_SELECTED_USERS) {
setShowToast(true);
return current;
}
const newSelectedIds = Object.assign({}, current);
if (!wasSelected) {
newSelectedIds[user.id] = user;
}
return newSelectedIds;
});
}, [currentUserId, clearSearch, selectedCount]);
const searchUsers = useCallback(async (searchTerm: string) => {
const lowerCasedTerm = searchTerm.toLowerCase();
@@ -282,7 +279,7 @@ export default function CreateDirectMessage({
results = await searchProfiles(serverUrl, lowerCasedTerm, {allow_inactive: true});
}
let searchData: UserProfile[] = [];
let searchData: UserProfile[] = EMPTY;
if (results.data) {
searchData = results.data;
}
@@ -338,8 +335,8 @@ export default function CreateDirectMessage({
}, []);
useEffect(() => {
setShowToast(selectedCount >= General.MAX_USERS_IN_GM);
}, [selectedCount >= General.MAX_USERS_IN_GM]);
setShowToast(selectedCount >= MAX_SELECTED_USERS);
}, [selectedCount >= MAX_SELECTED_USERS]);
if (startingConversation) {
return (
@@ -390,7 +387,7 @@ export default function CreateDirectMessage({
showToast={showToast}
setShowToast={setShowToast}
toastIcon={'check'}
toastMessage={formatMessage(messages.toastMessage, {maxCount: General.MAX_USERS_IN_GM})}
toastMessage={formatMessage(messages.toastMessage, {maxCount: MAX_SELECTED_USERS})}
selectedIds={selectedIds}
onRemove={handleRemoveProfile}
teammateNameDisplay={teammateNameDisplay}

View File

@@ -428,6 +428,7 @@
"mobile.channel_add_people.error": "We could not add those users to the channel. Please check your connection and try again.",
"mobile.channel_add_people.title": "Add Members",
"mobile.channel_add_people.button": "Add",
"mobile.channel_add_people.max_limit_reached": "Max selected users are limited to {maxCount} members",
"mobile.channel_info.alertNo": "No",
"mobile.channel_info.alertYes": "Yes",
"mobile.channel_list.recent": "Recent",