From 84a6443042439b0925b7fadeb906fd39d4c3cad5 Mon Sep 17 00:00:00 2001 From: Jason Frerich Date: Sat, 3 Dec 2022 13:12:36 -0600 Subject: [PATCH] Add selected users panel --- app/components/selected_users/index.tsx | 2 - app/constants/general.ts | 1 + .../channel_add_people/channel_add_people.tsx | 126 ++++++++++-------- .../create_direct_message.tsx | 75 +++++------ assets/base/i18n/en.json | 1 + 5 files changed, 105 insertions(+), 100 deletions(-) diff --git a/app/components/selected_users/index.tsx b/app/components/selected_users/index.tsx index 72a639aee6..22c3089f92 100644 --- a/app/components/selected_users/index.tsx +++ b/app/components/selected_users/index.tsx @@ -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} /> diff --git a/app/constants/general.ts b/app/constants/general.ts index 1be49a7013..395e57385c 100644 --- a/app/constants/general.ts +++ b/app/constants/general.ts @@ -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, diff --git a/app/screens/channel_add_people/channel_add_people.tsx b/app/screens/channel_add_people/channel_add_people.tsx index 3b5b20f5f9..e9dd789839 100644 --- a/app/screens/channel_add_people/channel_add_people.tsx +++ b/app/screens/channel_add_people/channel_add_people.tsx @@ -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(null); + const modalPosition = useModalPosition(mainView); const [profiles, setProfiles] = useState([]); const [searchResults, setSearchResults] = useState([]); @@ -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 => { @@ -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({ {hasProfiles && @@ -300,20 +308,6 @@ export default function ChannelAddPeople({ /> } - {/* - 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 && - - } */} + ); } diff --git a/app/screens/create_direct_message/create_direct_message.tsx b/app/screens/create_direct_message/create_direct_message.tsx index 4a894b9ef5..1430e711e5 100644 --- a/app/screens/create_direct_message/create_direct_message.tsx +++ b/app/screens/create_direct_message/create_direct_message.tsx @@ -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(null); const modalPosition = useModalPosition(mainView); - const [profiles, dispatchProfiles] = useReducer(reduceProfiles, []); - const [searchResults, setSearchResults] = useState([]); + const [profiles, setProfiles] = useState(EMPTY); + const [searchResults, setSearchResults] = useState(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} diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index 9d1f95ca66..95f350f5eb 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -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",