diff --git a/src/components/right/management/ManageGroupAdminRights.tsx b/src/components/right/management/ManageGroupAdminRights.tsx index 8f67d7a6b..b9800e7b2 100644 --- a/src/components/right/management/ManageGroupAdminRights.tsx +++ b/src/components/right/management/ManageGroupAdminRights.tsx @@ -1,7 +1,7 @@ import React, { FC, memo, useCallback, useEffect, useMemo, useState, } from '../../../lib/teact/teact'; -import { getDispatch, withGlobal } from '../../../lib/teact/teactn'; +import { getDispatch, getGlobal, withGlobal } from '../../../lib/teact/teactn'; import { ApiChat, ApiChatAdminRights, ApiUser } from '../../../api/types'; import { ManagementScreens } from '../../../types'; @@ -22,7 +22,7 @@ import InputText from '../../ui/InputText'; type OwnProps = { chatId: string; - selectedChatMemberId?: string; + selectedUserId?: string; isPromotedByCurrentUser?: boolean; isNewAdmin?: boolean; onScreenSelect: (screen: ManagementScreens) => void; @@ -43,7 +43,7 @@ const CUSTOM_TITLE_MAX_LENGTH = 16; const ManageGroupAdminRights: FC = ({ isNewAdmin, - selectedChatMemberId, + selectedUserId, defaultRights, onScreenSelect, chat, @@ -66,28 +66,38 @@ const ManageGroupAdminRights: FC = ({ useHistoryBack(isActive, onClose); const selectedChatMember = useMemo(() => { - const selectedAdminMember = chat.fullInfo?.adminMembers?.find(({ userId }) => userId === selectedChatMemberId); + const selectedAdminMember = chat.fullInfo?.adminMembers?.find(({ userId }) => userId === selectedUserId); + + // If `selectedAdminMember` variable is filled with a value, then we have already saved the administrator, + // so now we need to return to the list of administrators + if (isNewAdmin && (selectedAdminMember || !selectedUserId)) { + return undefined; + } if (isNewAdmin) { - // If selectedAdminMember is fullfilled, it means that we are editing an existing admin (after a user - // has been promoted as admin) - return selectedAdminMember - ? undefined - : chat.fullInfo?.members?.find(({ userId }) => userId === selectedChatMemberId); + const user = getGlobal().users.byId[selectedUserId!]; + + return user ? { + userId: user.id, + adminRights: defaultRights, + customTitle: lang('ChannelAdmin'), + isOwner: false, + promotedByUserId: undefined, + } : undefined; } return selectedAdminMember; - }, [chat.fullInfo, isNewAdmin, selectedChatMemberId]); + }, [chat.fullInfo?.adminMembers, defaultRights, isNewAdmin, lang, selectedUserId]); useEffect(() => { - if (chat?.fullInfo && selectedChatMemberId && !selectedChatMember) { + if (chat?.fullInfo && selectedUserId && !selectedChatMember) { onScreenSelect(ManagementScreens.ChatAdministrators); } - }, [chat, onScreenSelect, selectedChatMember, selectedChatMemberId]); + }, [chat, onScreenSelect, selectedChatMember, selectedUserId]); useEffect(() => { - setPermissions((isNewAdmin ? defaultRights : selectedChatMember?.adminRights) || {}); - setCustomTitle(((isNewAdmin ? 'admin' : selectedChatMember?.customTitle) || '').substr(0, CUSTOM_TITLE_MAX_LENGTH)); + setPermissions(selectedChatMember?.adminRights || {}); + setCustomTitle((selectedChatMember?.customTitle || '').substr(0, CUSTOM_TITLE_MAX_LENGTH)); setIsTouched(Boolean(isNewAdmin)); setIsLoading(false); }, [defaultRights, isNewAdmin, selectedChatMember]); @@ -107,31 +117,31 @@ const ManageGroupAdminRights: FC = ({ }, []); const handleSavePermissions = useCallback(() => { - if (!selectedChatMemberId) { + if (!selectedUserId) { return; } setIsLoading(true); updateChatAdmin({ chatId: chat.id, - userId: selectedChatMemberId, + userId: selectedUserId, adminRights: permissions, customTitle, }); - }, [selectedChatMemberId, updateChatAdmin, chat.id, permissions, customTitle]); + }, [selectedUserId, updateChatAdmin, chat.id, permissions, customTitle]); const handleDismissAdmin = useCallback(() => { - if (!selectedChatMemberId) { + if (!selectedUserId) { return; } updateChatAdmin({ chatId: chat.id, - userId: selectedChatMemberId, + userId: selectedUserId, adminRights: {}, }); closeDismissConfirmationDialog(); - }, [chat.id, closeDismissConfirmationDialog, selectedChatMemberId, updateChatAdmin]); + }, [chat.id, closeDismissConfirmationDialog, selectedUserId, updateChatAdmin]); const getControlIsDisabled = useCallback((key: keyof ApiChatAdminRights) => { if (isChatBasicGroup(chat)) { @@ -317,7 +327,7 @@ const ManageGroupAdminRights: FC = ({ /> )} - {currentUserId !== selectedChatMemberId && !isFormFullyDisabled && !isNewAdmin && ( + {currentUserId !== selectedUserId && !isFormFullyDisabled && !isNewAdmin && ( {lang('EditAdminRemoveAdmin')} diff --git a/src/components/right/management/ManageGroupMembers.tsx b/src/components/right/management/ManageGroupMembers.tsx index d72a6f7f1..75bb37efa 100644 --- a/src/components/right/management/ManageGroupMembers.tsx +++ b/src/components/right/management/ManageGroupMembers.tsx @@ -1,18 +1,27 @@ import React, { - FC, memo, useCallback, useMemo, + FC, memo, useCallback, useMemo, useRef, } from '../../../lib/teact/teact'; import { getDispatch, getGlobal, withGlobal } from '../../../lib/teact/teactn'; import { ApiChatMember, ApiUserStatus } from '../../../api/types'; import { ManagementScreens } from '../../../types'; +import { unique } from '../../../util/iteratees'; import { selectChat } from '../../../modules/selectors'; -import { sortUserIds, isChatChannel } from '../../../modules/helpers'; +import { + sortUserIds, isChatChannel, filterUsersByName, sortChatIds, isUserBot, +} from '../../../modules/helpers'; +import useLang from '../../../hooks/useLang'; import useHistoryBack from '../../../hooks/useHistoryBack'; +import useInfiniteScroll from '../../../hooks/useInfiniteScroll'; +import useKeyboardListNavigation from '../../../hooks/useKeyboardListNavigation'; import PrivateChatInfo from '../../common/PrivateChatInfo'; import NothingFound from '../../common/NothingFound'; import ListItem from '../../ui/ListItem'; +import InputText from '../../ui/InputText'; +import InfiniteScroll from '../../ui/InfiniteScroll'; +import Loading from '../../ui/Loading'; type OwnProps = { chatId: string; @@ -28,6 +37,11 @@ type StateProps = { members?: ApiChatMember[]; adminMembers?: ApiChatMember[]; isChannel?: boolean; + localContactIds?: string[]; + searchQuery?: string; + isSearching?: boolean; + localUserIds?: string[]; + globalUserIds?: string[]; serverTimeOffset: number; }; @@ -38,20 +52,33 @@ const ManageGroupMembers: FC = ({ userStatusesById, isChannel, isActive, + globalUserIds, + localContactIds, + localUserIds, + isSearching, + searchQuery, serverTimeOffset, onClose, onScreenSelect, onChatMemberSelect, }) => { - const { openUserInfo } = getDispatch(); + const { openUserInfo, setUserSearchQuery, loadContactList } = getDispatch(); + const lang = useLang(); + // eslint-disable-next-line no-null/no-null + const inputRef = useRef(null); + // eslint-disable-next-line no-null/no-null + const containerRef = useRef(null); + + const adminIds = useMemo(() => { + return noAdmins ? adminMembers?.map(({ userId }) => userId) || [] : []; + }, [adminMembers, noAdmins]); const memberIds = useMemo(() => { // No need for expensive global updates on users, so we avoid them const usersById = getGlobal().users.byId; if (!members || !usersById) { - return undefined; + return []; } - const adminIds = noAdmins ? adminMembers?.map(({ userId }) => userId) || [] : []; const userIds = sortUserIds( members.map(({ userId }) => userId), @@ -62,7 +89,38 @@ const ManageGroupMembers: FC = ({ ); return noAdmins ? userIds.filter((userId) => !adminIds.includes(userId)) : userIds; - }, [members, noAdmins, adminMembers, userStatusesById, serverTimeOffset]); + }, [members, userStatusesById, serverTimeOffset, noAdmins, adminIds]); + + const displayedIds = useMemo(() => { + // No need for expensive global updates on users, so we avoid them + const usersById = getGlobal().users.byId; + const chatsById = getGlobal().chats.byId; + const shouldUseSearchResults = !!searchQuery; + const listedIds = !shouldUseSearchResults + ? memberIds + : (localContactIds ? filterUsersByName(localContactIds, usersById, searchQuery) : []); + + return sortChatIds( + unique([ + ...listedIds, + ...(shouldUseSearchResults ? localUserIds || [] : []), + ...(shouldUseSearchResults ? globalUserIds || [] : []), + ]).filter((contactId) => { + const user = usersById[contactId]; + if (!user) { + return true; + } + + return !user.isSelf + && (isChannel || user.canBeInvitedToGroup || !isUserBot(user)) + && (!noAdmins || !adminIds.includes(contactId)); + }), + chatsById, + true, + ); + }, [memberIds, localContactIds, searchQuery, localUserIds, globalUserIds, isChannel, noAdmins, adminIds]); + + const [viewportIds, getMore] = useInfiniteScroll(loadContactList, displayedIds, Boolean(searchQuery)); const handleMemberClick = useCallback((id: string) => { if (noAdmins) { @@ -73,29 +131,62 @@ const ManageGroupMembers: FC = ({ } }, [noAdmins, onChatMemberSelect, onScreenSelect, openUserInfo]); + const handleFilterChange = useCallback((e: React.ChangeEvent) => { + setUserSearchQuery({ query: e.target.value }); + }, [setUserSearchQuery]); + const handleKeyDown = useKeyboardListNavigation(containerRef, isActive, (index) => { + if (viewportIds && viewportIds.length > 0) { + handleMemberClick(viewportIds[index === -1 ? 0 : index]); + } + }, '.ListItem-button', true); + useHistoryBack(isActive, onClose); + function renderSearchField() { + return ( +
+ +
+ ); + } + return (
+ {noAdmins && renderSearchField()}
-
- {memberIds ? ( - memberIds.map((id, i) => ( - handleMemberClick(id)} - > - - - )) - ) : ( +
+ {viewportIds?.length ? ( + + {viewportIds.map((id) => ( + handleMemberClick(id)} + > + + + ))} + + ) : !isSearching && viewportIds && !viewportIds.length ? ( + ) : ( + )}
@@ -110,12 +201,25 @@ export default memo(withGlobal( const members = chat?.fullInfo?.members; const adminMembers = chat?.fullInfo?.adminMembers; const isChannel = chat && isChatChannel(chat); + const { userIds: localContactIds } = global.contactList || {}; + + const { + query: searchQuery, + fetchingStatus, + globalUserIds, + localUserIds, + } = global.userSearch; return { members, adminMembers, userStatusesById, isChannel, + localContactIds, + searchQuery, + isSearching: fetchingStatus, + globalUserIds, + localUserIds, serverTimeOffset: global.serverTimeOffset, }; }, diff --git a/src/components/right/management/Management.scss b/src/components/right/management/Management.scss index 7478fd70f..7307c2fd8 100644 --- a/src/components/right/management/Management.scss +++ b/src/components/right/management/Management.scss @@ -158,6 +158,31 @@ } } } + + &__filter { + padding: 0 1rem 0.25rem 0.75rem; + border-bottom: 1px solid var(--color-borders); + display: flex; + flex-flow: row wrap; + flex-shrink: 0; + + overflow-y: auto; + max-height: 20rem; + + .input-group { + margin-bottom: 0.5rem; + margin-left: 0.5rem; + flex-grow: 1; + } + + .form-control { + height: 2rem; + border: none; + border-radius: 0; + padding: 0; + box-shadow: none; + } + } } .ManageGroupMembers { diff --git a/src/components/right/management/Management.tsx b/src/components/right/management/Management.tsx index 5a4a4933e..be90d8b44 100644 --- a/src/components/right/management/Management.tsx +++ b/src/components/right/management/Management.tsx @@ -198,24 +198,13 @@ const Management: FC = ({ /> ); + case ManagementScreens.ChatNewAdminRights: case ManagementScreens.ChatAdminRights: return ( - ); - - case ManagementScreens.ChatNewAdminRights: - return ( -