From 0b40f27ed9d9ac115df45d5ff4d4e1802f63372b Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Mon, 10 Jan 2022 15:17:48 +0100 Subject: [PATCH] Management: Support promoting members to admins (#1629) --- src/components/right/RightColumn.tsx | 2 + src/components/right/RightHeader.tsx | 10 ++++ .../management/ManageChatAdministrators.tsx | 25 ++++++--- .../management/ManageGroupAdminRights.tsx | 55 ++++++++++++------- .../right/management/ManageGroupMembers.tsx | 43 +++++++++++---- .../right/management/Management.tsx | 28 ++++++++++ src/modules/actions/api/chats.ts | 2 +- src/types/index.ts | 2 + src/util/getReadableErrorText.ts | 5 ++ 9 files changed, 131 insertions(+), 41 deletions(-) diff --git a/src/components/right/RightColumn.tsx b/src/components/right/RightColumn.tsx index 073eff4cd..2160ed9d1 100644 --- a/src/components/right/RightColumn.tsx +++ b/src/components/right/RightColumn.tsx @@ -133,6 +133,8 @@ const RightColumn: FC = ({ setIsPromotedByCurrentUser(undefined); break; case ManagementScreens.ChatAdminRights: + case ManagementScreens.ChatNewAdminRights: + case ManagementScreens.GroupAddAdmins: case ManagementScreens.GroupRecentActions: setManagementScreen(ManagementScreens.ChatAdministrators); break; diff --git a/src/components/right/RightHeader.tsx b/src/components/right/RightHeader.tsx index f7077ce91..5520187a2 100644 --- a/src/components/right/RightHeader.tsx +++ b/src/components/right/RightHeader.tsx @@ -77,7 +77,9 @@ enum HeaderContent { ManageGroupUserPermissions, ManageGroupRecentActions, ManageGroupAdminRights, + ManageGroupNewAdminRights, ManageGroupMembers, + ManageGroupAddAdmins, StickerSearch, GifSearch, PollResults, @@ -187,8 +189,12 @@ const RightHeader: FC = ({ HeaderContent.ManageGroupRecentActions ) : managementScreen === ManagementScreens.ChatAdminRights ? ( HeaderContent.ManageGroupAdminRights + ) : managementScreen === ManagementScreens.ChatNewAdminRights ? ( + HeaderContent.ManageGroupNewAdminRights ) : managementScreen === ManagementScreens.GroupMembers ? ( HeaderContent.ManageGroupMembers + ) : managementScreen === ManagementScreens.GroupAddAdmins ? ( + HeaderContent.ManageGroupAddAdmins ) : undefined // Never reached ) : undefined; // When column is closed @@ -235,6 +241,8 @@ const RightHeader: FC = ({ return

{lang('Group.Info.AdminLog')}

; case HeaderContent.ManageGroupAdminRights: return

{lang('EditAdminRights')}

; + case HeaderContent.ManageGroupNewAdminRights: + return

{lang('SetAsAdmin')}

; case HeaderContent.ManageGroupPermissions: return

{lang('ChannelPermissions')}

; case HeaderContent.ManageGroupRemovedUsers: @@ -243,6 +251,8 @@ const RightHeader: FC = ({ return

{lang('ChannelAddException')}

; case HeaderContent.ManageGroupUserPermissions: return

{lang('UserRestrictions')}

; + case HeaderContent.ManageGroupAddAdmins: + return

{lang('Channel.Management.AddModerator')}

; case HeaderContent.StickerSearch: return ( ; }; const ManageChatAdministrators: FC = ({ chat, isChannel, currentUserId, - usersById, onScreenSelect, onChatMemberSelect, onClose, @@ -68,11 +67,17 @@ const ManageChatAdministrators: FC = ({ onScreenSelect(ManagementScreens.ChatAdminRights); }, [currentUserId, onChatMemberSelect, onScreenSelect]); + const handleAddAdminClick = useCallback(() => { + onScreenSelect(ManagementScreens.GroupAddAdmins); + }, [onScreenSelect]); + const getMemberStatus = useCallback((member: ApiChatMember) => { if (member.isOwner) { return lang('ChannelCreator'); } + // No need for expensive global updates on users, so we avoid them + const usersById = getGlobal().users.byId; const promotedByUser = member.promotedByUserId ? usersById[member.promotedByUserId] : undefined; if (promotedByUser) { @@ -80,7 +85,7 @@ const ManageChatAdministrators: FC = ({ } return lang('ChannelAdmin'); - }, [lang, usersById]); + }, [lang]); return (
@@ -116,6 +121,14 @@ const ManageChatAdministrators: FC = ({ /> ))} + + + +
@@ -125,13 +138,11 @@ const ManageChatAdministrators: FC = ({ export default memo(withGlobal( (global, { chatId }): StateProps => { const chat = selectChat(global, chatId)!; - const { byId: usersById } = global.users; return { chat, currentUserId: global.currentUserId, isChannel: isChatChannel(chat), - usersById, }; }, )(ManageChatAdministrators)); diff --git a/src/components/right/management/ManageGroupAdminRights.tsx b/src/components/right/management/ManageGroupAdminRights.tsx index 1beb767b1..8f67d7a6b 100644 --- a/src/components/right/management/ManageGroupAdminRights.tsx +++ b/src/components/right/management/ManageGroupAdminRights.tsx @@ -24,6 +24,7 @@ type OwnProps = { chatId: string; selectedChatMemberId?: string; isPromotedByCurrentUser?: boolean; + isNewAdmin?: boolean; onScreenSelect: (screen: ManagementScreens) => void; onClose: NoneToVoidFunction; isActive: boolean; @@ -35,12 +36,15 @@ type StateProps = { currentUserId?: string; isChannel: boolean; isFormFullyDisabled: boolean; + defaultRights?: ApiChatAdminRights; }; const CUSTOM_TITLE_MAX_LENGTH = 16; const ManageGroupAdminRights: FC = ({ + isNewAdmin, selectedChatMemberId, + defaultRights, onScreenSelect, chat, usersById, @@ -53,7 +57,7 @@ const ManageGroupAdminRights: FC = ({ const { updateChatAdmin } = getDispatch(); const [permissions, setPermissions] = useState({}); - const [isTouched, setIsTouched] = useState(false); + const [isTouched, setIsTouched] = useState(isNewAdmin); const [isLoading, setIsLoading] = useState(false); const [isDismissConfirmationDialogOpen, openDismissConfirmationDialog, closeDismissConfirmationDialog] = useFlag(); const [customTitle, setCustomTitle] = useState(''); @@ -62,12 +66,18 @@ const ManageGroupAdminRights: FC = ({ useHistoryBack(isActive, onClose); const selectedChatMember = useMemo(() => { - if (!chat.fullInfo || !chat.fullInfo.adminMembers) { - return undefined; + const selectedAdminMember = chat.fullInfo?.adminMembers?.find(({ userId }) => userId === selectedChatMemberId); + + 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); } - return chat.fullInfo.adminMembers.find(({ userId }) => userId === selectedChatMemberId); - }, [chat, selectedChatMemberId]); + return selectedAdminMember; + }, [chat.fullInfo, isNewAdmin, selectedChatMemberId]); useEffect(() => { if (chat?.fullInfo && selectedChatMemberId && !selectedChatMember) { @@ -76,11 +86,11 @@ const ManageGroupAdminRights: FC = ({ }, [chat, onScreenSelect, selectedChatMember, selectedChatMemberId]); useEffect(() => { - setPermissions((selectedChatMember?.adminRights) || {}); - setCustomTitle(((selectedChatMember?.customTitle) || '').substr(0, CUSTOM_TITLE_MAX_LENGTH)); - setIsTouched(false); + setPermissions((isNewAdmin ? defaultRights : selectedChatMember?.adminRights) || {}); + setCustomTitle(((isNewAdmin ? 'admin' : selectedChatMember?.customTitle) || '').substr(0, CUSTOM_TITLE_MAX_LENGTH)); + setIsTouched(Boolean(isNewAdmin)); setIsLoading(false); - }, [selectedChatMember]); + }, [defaultRights, isNewAdmin, selectedChatMember]); const handlePermissionChange = useCallback((e: React.ChangeEvent) => { const { name } = e.target; @@ -108,7 +118,7 @@ const ManageGroupAdminRights: FC = ({ adminRights: permissions, customTitle, }); - }, [chat, selectedChatMemberId, permissions, customTitle, updateChatAdmin]); + }, [selectedChatMemberId, updateChatAdmin, chat.id, permissions, customTitle]); const handleDismissAdmin = useCallback(() => { if (!selectedChatMemberId) { @@ -136,7 +146,7 @@ const ManageGroupAdminRights: FC = ({ }, [chat, isFormFullyDisabled]); const memberStatus = useMemo(() => { - if (!selectedChatMember) { + if (isNewAdmin || !selectedChatMember) { return undefined; } @@ -153,7 +163,7 @@ const ManageGroupAdminRights: FC = ({ } return lang('ChannelAdmin'); - }, [selectedChatMember, usersById, lang]); + }, [isNewAdmin, selectedChatMember, usersById, lang]); const handleCustomTitleChange = useCallback((e) => { const { value } = e.target; @@ -307,7 +317,7 @@ const ManageGroupAdminRights: FC = ({ /> )} - {currentUserId !== selectedChatMemberId && !isFormFullyDisabled && ( + {currentUserId !== selectedChatMemberId && !isFormFullyDisabled && !isNewAdmin && ( {lang('EditAdminRemoveAdmin')} @@ -328,14 +338,16 @@ const ManageGroupAdminRights: FC = ({ )} - + {!isNewAdmin && ( + + )} ); }; @@ -354,6 +366,7 @@ export default memo(withGlobal( currentUserId, isChannel, isFormFullyDisabled, + defaultRights: chat.adminRights, }; }, )(ManageGroupAdminRights)); diff --git a/src/components/right/management/ManageGroupMembers.tsx b/src/components/right/management/ManageGroupMembers.tsx index 8b0499e23..d72a6f7f1 100644 --- a/src/components/right/management/ManageGroupMembers.tsx +++ b/src/components/right/management/ManageGroupMembers.tsx @@ -1,9 +1,11 @@ import React, { FC, memo, useCallback, useMemo, } from '../../../lib/teact/teact'; -import { getDispatch, withGlobal } from '../../../lib/teact/teactn'; +import { getDispatch, getGlobal, withGlobal } from '../../../lib/teact/teactn'; + +import { ApiChatMember, ApiUserStatus } from '../../../api/types'; +import { ManagementScreens } from '../../../types'; -import { ApiChatMember, ApiUser, ApiUserStatus } from '../../../api/types'; import { selectChat } from '../../../modules/selectors'; import { sortUserIds, isChatChannel } from '../../../modules/helpers'; import useHistoryBack from '../../../hooks/useHistoryBack'; @@ -14,46 +16,62 @@ import ListItem from '../../ui/ListItem'; type OwnProps = { chatId: string; - onClose: NoneToVoidFunction; isActive: boolean; + noAdmins?: boolean; + onClose: NoneToVoidFunction; + onScreenSelect?: (screen: ManagementScreens) => void; + onChatMemberSelect?: (memberId: string, isPromotedByCurrentUser?: boolean) => void; }; type StateProps = { - usersById: Record; userStatusesById: Record; members?: ApiChatMember[]; + adminMembers?: ApiChatMember[]; isChannel?: boolean; serverTimeOffset: number; }; const ManageGroupMembers: FC = ({ + noAdmins, members, - usersById, + adminMembers, userStatusesById, isChannel, - onClose, isActive, serverTimeOffset, + onClose, + onScreenSelect, + onChatMemberSelect, }) => { const { openUserInfo } = getDispatch(); 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; } + const adminIds = noAdmins ? adminMembers?.map(({ userId }) => userId) || [] : []; - return sortUserIds( + const userIds = sortUserIds( members.map(({ userId }) => userId), usersById, userStatusesById, undefined, serverTimeOffset, ); - }, [members, serverTimeOffset, usersById, userStatusesById]); + + return noAdmins ? userIds.filter((userId) => !adminIds.includes(userId)) : userIds; + }, [members, noAdmins, adminMembers, userStatusesById, serverTimeOffset]); const handleMemberClick = useCallback((id: string) => { - openUserInfo({ id }); - }, [openUserInfo]); + if (noAdmins) { + onChatMemberSelect!(id, false); + onScreenSelect!(ManagementScreens.ChatNewAdminRights); + } else { + openUserInfo({ id }); + } + }, [noAdmins, onChatMemberSelect, onScreenSelect, openUserInfo]); useHistoryBack(isActive, onClose); @@ -88,13 +106,14 @@ const ManageGroupMembers: FC = ({ export default memo(withGlobal( (global, { chatId }): StateProps => { const chat = selectChat(global, chatId); - const { byId: usersById, statusesById: userStatusesById } = global.users; + const { statusesById: userStatusesById } = global.users; const members = chat?.fullInfo?.members; + const adminMembers = chat?.fullInfo?.adminMembers; const isChannel = chat && isChatChannel(chat); return { members, - usersById, + adminMembers, userStatusesById, isChannel, serverTimeOffset: global.serverTimeOffset, diff --git a/src/components/right/management/Management.tsx b/src/components/right/management/Management.tsx index b49bde86c..8ec9f1d5e 100644 --- a/src/components/right/management/Management.tsx +++ b/src/components/right/management/Management.tsx @@ -73,6 +73,7 @@ const Management: FC = ({ ManagementScreens.GroupUserPermissionsCreate, ManagementScreens.GroupUserPermissions, ManagementScreens.ChatAdminRights, + ManagementScreens.ChatNewAdminRights, ManagementScreens.GroupRecentActions, ].includes(currentScreen)} /> @@ -90,6 +91,7 @@ const Management: FC = ({ ManagementScreens.Discussion, ManagementScreens.ChatPrivacyType, ManagementScreens.ChatAdminRights, + ManagementScreens.ChatNewAdminRights, ManagementScreens.GroupRecentActions, ].includes(currentScreen)} /> @@ -175,6 +177,7 @@ const Management: FC = ({ onChatMemberSelect={onChatMemberSelect} isActive={isActive || [ ManagementScreens.ChatAdminRights, + ManagementScreens.ChatNewAdminRights, ManagementScreens.GroupRecentActions, ].includes(currentScreen)} onClose={onClose} @@ -202,6 +205,19 @@ const Management: FC = ({ /> ); + case ManagementScreens.ChatNewAdminRights: + return ( + + ); + case ManagementScreens.ChannelSubscribers: case ManagementScreens.GroupMembers: return ( @@ -211,6 +227,18 @@ const Management: FC = ({ onClose={onClose} /> ); + + case ManagementScreens.GroupAddAdmins: + return ( + + ); } return undefined; // Never reached diff --git a/src/modules/actions/api/chats.ts b/src/modules/actions/api/chats.ts index 98e75b082..377ba84d5 100644 --- a/src/modules/actions/api/chats.ts +++ b/src/modules/actions/api/chats.ts @@ -763,8 +763,8 @@ addReducer('updateChatAdmin', (global, actions, payload) => { chat, user, adminRights, customTitle, }); + const chatAfterUpdate = await callApi('fetchFullChat', chat); const newGlobal = getGlobal(); - const chatAfterUpdate = selectChat(newGlobal, chatId); if (!chatAfterUpdate || !chatAfterUpdate.fullInfo) { return; diff --git a/src/types/index.ts b/src/types/index.ts index 7623750c3..deaa7d56e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -317,7 +317,9 @@ export enum ManagementScreens { ChatAdministrators, GroupRecentActions, ChatAdminRights, + ChatNewAdminRights, GroupMembers, + GroupAddAdmins, } export type ManagementType = 'user' | 'group' | 'channel'; diff --git a/src/util/getReadableErrorText.ts b/src/util/getReadableErrorText.ts index ff2183552..c248fbba4 100644 --- a/src/util/getReadableErrorText.ts +++ b/src/util/getReadableErrorText.ts @@ -63,6 +63,11 @@ const READABLE_ERROR_MESSAGES: Record = { USER_ALREADY_PARTICIPANT: 'You already in the group', SCHEDULE_DATE_INVALID: 'Invalid schedule date provided', WALLPAPER_DIMENSIONS_INVALID: 'The wallpaper dimensions are invalid, please select another file', + ADMINS_TOO_MUCH: 'There are too many admins', + ADMIN_RANK_EMOJI_NOT_ALLOWED: 'An admin rank cannot contain emojis', + ADMIN_RANK_INVALID: 'The specified admin rank is invalid', + FRESH_CHANGE_ADMINS_FORBIDDEN: 'You were just elected admin, you can\'t add or modify other admins yet', + INPUT_USER_DEACTIVATED: 'The specified user was deleted', }; export const SHIPPING_ERRORS: Record = {