Management / New Admin: Allow to search globally (#1674)

This commit is contained in:
Alexander Zinchuk 2022-01-28 20:59:32 +01:00
parent 62e716f97a
commit 6142f1c44e
4 changed files with 182 additions and 54 deletions

View File

@ -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<OwnProps & StateProps> = ({
isNewAdmin,
selectedChatMemberId,
selectedUserId,
defaultRights,
onScreenSelect,
chat,
@ -66,28 +66,38 @@ const ManageGroupAdminRights: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
}, []);
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<OwnProps & StateProps> = ({
/>
)}
{currentUserId !== selectedChatMemberId && !isFormFullyDisabled && !isNewAdmin && (
{currentUserId !== selectedUserId && !isFormFullyDisabled && !isNewAdmin && (
<ListItem icon="delete" ripple destructive onClick={openDismissConfirmationDialog}>
{lang('EditAdminRemoveAdmin')}
</ListItem>

View File

@ -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<OwnProps & StateProps> = ({
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<HTMLInputElement>(null);
// eslint-disable-next-line no-null/no-null
const containerRef = useRef<HTMLDivElement>(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<OwnProps & StateProps> = ({
);
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<OwnProps & StateProps> = ({
}
}, [noAdmins, onChatMemberSelect, onScreenSelect, openUserInfo]);
const handleFilterChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
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 (
<div className="Management__filter" dir={lang.isRtl ? 'rtl' : undefined}>
<InputText
ref={inputRef}
value={searchQuery}
onChange={handleFilterChange}
placeholder={lang('Search')}
/>
</div>
);
}
return (
<div className="Management">
{noAdmins && renderSearchField()}
<div className="custom-scroll">
<div className="section" teactFastList>
{memberIds ? (
memberIds.map((id, i) => (
<ListItem
key={id}
teactOrderKey={i}
className="chat-item-clickable scroll-item"
onClick={() => handleMemberClick(id)}
>
<PrivateChatInfo userId={id} forceShowSelf />
</ListItem>
))
) : (
<div className="section">
{viewportIds?.length ? (
<InfiniteScroll
className="picker-list custom-scroll"
items={displayedIds}
onLoadMore={getMore}
noScrollRestore={Boolean(searchQuery)}
ref={containerRef}
onKeyDown={handleKeyDown}
>
{viewportIds.map((id) => (
<ListItem
key={id}
className="chat-item-clickable scroll-item"
onClick={() => handleMemberClick(id)}
>
<PrivateChatInfo userId={id} forceShowSelf />
</ListItem>
))}
</InfiniteScroll>
) : !isSearching && viewportIds && !viewportIds.length ? (
<NothingFound
teactOrderKey={0}
key="nothing-found"
text={isChannel ? 'No subscribers found' : 'No members found'}
/>
) : (
<Loading />
)}
</div>
</div>
@ -110,12 +201,25 @@ export default memo(withGlobal<OwnProps>(
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,
};
},

View File

@ -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 {

View File

@ -198,24 +198,13 @@ const Management: FC<OwnProps & StateProps> = ({
/>
);
case ManagementScreens.ChatNewAdminRights:
case ManagementScreens.ChatAdminRights:
return (
<ManageGroupAdminRights
chatId={chatId}
selectedChatMemberId={selectedChatMemberId}
isPromotedByCurrentUser={isPromotedByCurrentUser}
onScreenSelect={onScreenSelect}
isActive={isActive}
onClose={onClose}
/>
);
case ManagementScreens.ChatNewAdminRights:
return (
<ManageGroupAdminRights
chatId={chatId}
isNewAdmin
selectedChatMemberId={selectedChatMemberId}
isNewAdmin={currentScreen === ManagementScreens.ChatNewAdminRights}
selectedUserId={selectedChatMemberId}
isPromotedByCurrentUser={isPromotedByCurrentUser}
onScreenSelect={onScreenSelect}
isActive={isActive}