Management: Support promoting members to admins (#1629)

This commit is contained in:
Alexander Zinchuk 2022-01-10 15:17:48 +01:00
parent bad220ff29
commit 0b40f27ed9
9 changed files with 131 additions and 41 deletions

View File

@ -133,6 +133,8 @@ const RightColumn: FC<StateProps> = ({
setIsPromotedByCurrentUser(undefined);
break;
case ManagementScreens.ChatAdminRights:
case ManagementScreens.ChatNewAdminRights:
case ManagementScreens.GroupAddAdmins:
case ManagementScreens.GroupRecentActions:
setManagementScreen(ManagementScreens.ChatAdministrators);
break;

View File

@ -77,7 +77,9 @@ enum HeaderContent {
ManageGroupUserPermissions,
ManageGroupRecentActions,
ManageGroupAdminRights,
ManageGroupNewAdminRights,
ManageGroupMembers,
ManageGroupAddAdmins,
StickerSearch,
GifSearch,
PollResults,
@ -187,8 +189,12 @@ const RightHeader: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
return <h3>{lang('Group.Info.AdminLog')}</h3>;
case HeaderContent.ManageGroupAdminRights:
return <h3>{lang('EditAdminRights')}</h3>;
case HeaderContent.ManageGroupNewAdminRights:
return <h3>{lang('SetAsAdmin')}</h3>;
case HeaderContent.ManageGroupPermissions:
return <h3>{lang('ChannelPermissions')}</h3>;
case HeaderContent.ManageGroupRemovedUsers:
@ -243,6 +251,8 @@ const RightHeader: FC<OwnProps & StateProps> = ({
return <h3>{lang('ChannelAddException')}</h3>;
case HeaderContent.ManageGroupUserPermissions:
return <h3>{lang('UserRestrictions')}</h3>;
case HeaderContent.ManageGroupAddAdmins:
return <h3>{lang('Channel.Management.AddModerator')}</h3>;
case HeaderContent.StickerSearch:
return (
<SearchInput

View File

@ -1,10 +1,10 @@
import React, {
FC, memo, useCallback, useMemo,
} from '../../../lib/teact/teact';
import { withGlobal } from '../../../lib/teact/teactn';
import { getGlobal, withGlobal } from '../../../lib/teact/teactn';
import { ManagementScreens } from '../../../types';
import { ApiChat, ApiChatMember, ApiUser } from '../../../api/types';
import { ApiChat, ApiChatMember } from '../../../api/types';
import { getUserFullName, isChatChannel } from '../../../modules/helpers';
import { selectChat } from '../../../modules/selectors';
@ -13,6 +13,7 @@ import useHistoryBack from '../../../hooks/useHistoryBack';
import ListItem from '../../ui/ListItem';
import PrivateChatInfo from '../../common/PrivateChatInfo';
import FloatingActionButton from '../../ui/FloatingActionButton';
type OwnProps = {
chatId: string;
@ -26,14 +27,12 @@ type StateProps = {
chat: ApiChat;
currentUserId?: string;
isChannel: boolean;
usersById: Record<string, ApiUser>;
};
const ManageChatAdministrators: FC<OwnProps & StateProps> = ({
chat,
isChannel,
currentUserId,
usersById,
onScreenSelect,
onChatMemberSelect,
onClose,
@ -68,11 +67,17 @@ const ManageChatAdministrators: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
}
return lang('ChannelAdmin');
}, [lang, usersById]);
}, [lang]);
return (
<div className="Management">
@ -116,6 +121,14 @@ const ManageChatAdministrators: FC<OwnProps & StateProps> = ({
/>
</ListItem>
))}
<FloatingActionButton
isShown
onClick={handleAddAdminClick}
ariaLabel={lang('Channel.Management.AddModerator')}
>
<i className="icon-add-user-filled" />
</FloatingActionButton>
</div>
</div>
</div>
@ -125,13 +138,11 @@ const ManageChatAdministrators: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>(
(global, { chatId }): StateProps => {
const chat = selectChat(global, chatId)!;
const { byId: usersById } = global.users;
return {
chat,
currentUserId: global.currentUserId,
isChannel: isChatChannel(chat),
usersById,
};
},
)(ManageChatAdministrators));

View File

@ -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<OwnProps & StateProps> = ({
isNewAdmin,
selectedChatMemberId,
defaultRights,
onScreenSelect,
chat,
usersById,
@ -53,7 +57,7 @@ const ManageGroupAdminRights: FC<OwnProps & StateProps> = ({
const { updateChatAdmin } = getDispatch();
const [permissions, setPermissions] = useState<ApiChatAdminRights>({});
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<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
}, [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<HTMLInputElement>) => {
const { name } = e.target;
@ -108,7 +118,7 @@ const ManageGroupAdminRights: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
}, [chat, isFormFullyDisabled]);
const memberStatus = useMemo(() => {
if (!selectedChatMember) {
if (isNewAdmin || !selectedChatMember) {
return undefined;
}
@ -153,7 +163,7 @@ const ManageGroupAdminRights: FC<OwnProps & StateProps> = ({
}
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<OwnProps & StateProps> = ({
/>
)}
{currentUserId !== selectedChatMemberId && !isFormFullyDisabled && (
{currentUserId !== selectedChatMemberId && !isFormFullyDisabled && !isNewAdmin && (
<ListItem icon="delete" ripple destructive onClick={openDismissConfirmationDialog}>
{lang('EditAdminRemoveAdmin')}
</ListItem>
@ -328,14 +338,16 @@ const ManageGroupAdminRights: FC<OwnProps & StateProps> = ({
)}
</FloatingActionButton>
<ConfirmDialog
isOpen={isDismissConfirmationDialogOpen}
onClose={closeDismissConfirmationDialog}
text="Are you sure you want to dismiss this admin?"
confirmLabel="Dismiss"
confirmHandler={handleDismissAdmin}
confirmIsDestructive
/>
{!isNewAdmin && (
<ConfirmDialog
isOpen={isDismissConfirmationDialogOpen}
onClose={closeDismissConfirmationDialog}
text="Are you sure you want to dismiss this admin?"
confirmLabel={lang('Channel.Admin.Dismiss')}
confirmHandler={handleDismissAdmin}
confirmIsDestructive
/>
)}
</div>
);
};
@ -354,6 +366,7 @@ export default memo(withGlobal<OwnProps>(
currentUserId,
isChannel,
isFormFullyDisabled,
defaultRights: chat.adminRights,
};
},
)(ManageGroupAdminRights));

View File

@ -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<string, ApiUser>;
userStatusesById: Record<string, ApiUserStatus>;
members?: ApiChatMember[];
adminMembers?: ApiChatMember[];
isChannel?: boolean;
serverTimeOffset: number;
};
const ManageGroupMembers: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>(
(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,

View File

@ -73,6 +73,7 @@ const Management: FC<OwnProps & StateProps> = ({
ManagementScreens.GroupUserPermissionsCreate,
ManagementScreens.GroupUserPermissions,
ManagementScreens.ChatAdminRights,
ManagementScreens.ChatNewAdminRights,
ManagementScreens.GroupRecentActions,
].includes(currentScreen)}
/>
@ -90,6 +91,7 @@ const Management: FC<OwnProps & StateProps> = ({
ManagementScreens.Discussion,
ManagementScreens.ChatPrivacyType,
ManagementScreens.ChatAdminRights,
ManagementScreens.ChatNewAdminRights,
ManagementScreens.GroupRecentActions,
].includes(currentScreen)}
/>
@ -175,6 +177,7 @@ const Management: FC<OwnProps & StateProps> = ({
onChatMemberSelect={onChatMemberSelect}
isActive={isActive || [
ManagementScreens.ChatAdminRights,
ManagementScreens.ChatNewAdminRights,
ManagementScreens.GroupRecentActions,
].includes(currentScreen)}
onClose={onClose}
@ -202,6 +205,19 @@ const Management: FC<OwnProps & StateProps> = ({
/>
);
case ManagementScreens.ChatNewAdminRights:
return (
<ManageGroupAdminRights
chatId={chatId}
isNewAdmin
selectedChatMemberId={selectedChatMemberId}
isPromotedByCurrentUser={isPromotedByCurrentUser}
onScreenSelect={onScreenSelect}
isActive={isActive}
onClose={onClose}
/>
);
case ManagementScreens.ChannelSubscribers:
case ManagementScreens.GroupMembers:
return (
@ -211,6 +227,18 @@ const Management: FC<OwnProps & StateProps> = ({
onClose={onClose}
/>
);
case ManagementScreens.GroupAddAdmins:
return (
<ManageGroupMembers
chatId={chatId}
noAdmins
isActive={isActive}
onClose={onClose}
onScreenSelect={onScreenSelect}
onChatMemberSelect={onChatMemberSelect}
/>
);
}
return undefined; // Never reached

View File

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

View File

@ -317,7 +317,9 @@ export enum ManagementScreens {
ChatAdministrators,
GroupRecentActions,
ChatAdminRights,
ChatNewAdminRights,
GroupMembers,
GroupAddAdmins,
}
export type ManagementType = 'user' | 'group' | 'channel';

View File

@ -63,6 +63,11 @@ const READABLE_ERROR_MESSAGES: Record<string, string> = {
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<string, ApiFieldError> = {