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); setIsPromotedByCurrentUser(undefined);
break; break;
case ManagementScreens.ChatAdminRights: case ManagementScreens.ChatAdminRights:
case ManagementScreens.ChatNewAdminRights:
case ManagementScreens.GroupAddAdmins:
case ManagementScreens.GroupRecentActions: case ManagementScreens.GroupRecentActions:
setManagementScreen(ManagementScreens.ChatAdministrators); setManagementScreen(ManagementScreens.ChatAdministrators);
break; break;

View File

@ -77,7 +77,9 @@ enum HeaderContent {
ManageGroupUserPermissions, ManageGroupUserPermissions,
ManageGroupRecentActions, ManageGroupRecentActions,
ManageGroupAdminRights, ManageGroupAdminRights,
ManageGroupNewAdminRights,
ManageGroupMembers, ManageGroupMembers,
ManageGroupAddAdmins,
StickerSearch, StickerSearch,
GifSearch, GifSearch,
PollResults, PollResults,
@ -187,8 +189,12 @@ const RightHeader: FC<OwnProps & StateProps> = ({
HeaderContent.ManageGroupRecentActions HeaderContent.ManageGroupRecentActions
) : managementScreen === ManagementScreens.ChatAdminRights ? ( ) : managementScreen === ManagementScreens.ChatAdminRights ? (
HeaderContent.ManageGroupAdminRights HeaderContent.ManageGroupAdminRights
) : managementScreen === ManagementScreens.ChatNewAdminRights ? (
HeaderContent.ManageGroupNewAdminRights
) : managementScreen === ManagementScreens.GroupMembers ? ( ) : managementScreen === ManagementScreens.GroupMembers ? (
HeaderContent.ManageGroupMembers HeaderContent.ManageGroupMembers
) : managementScreen === ManagementScreens.GroupAddAdmins ? (
HeaderContent.ManageGroupAddAdmins
) : undefined // Never reached ) : undefined // Never reached
) : undefined; // When column is closed ) : undefined; // When column is closed
@ -235,6 +241,8 @@ const RightHeader: FC<OwnProps & StateProps> = ({
return <h3>{lang('Group.Info.AdminLog')}</h3>; return <h3>{lang('Group.Info.AdminLog')}</h3>;
case HeaderContent.ManageGroupAdminRights: case HeaderContent.ManageGroupAdminRights:
return <h3>{lang('EditAdminRights')}</h3>; return <h3>{lang('EditAdminRights')}</h3>;
case HeaderContent.ManageGroupNewAdminRights:
return <h3>{lang('SetAsAdmin')}</h3>;
case HeaderContent.ManageGroupPermissions: case HeaderContent.ManageGroupPermissions:
return <h3>{lang('ChannelPermissions')}</h3>; return <h3>{lang('ChannelPermissions')}</h3>;
case HeaderContent.ManageGroupRemovedUsers: case HeaderContent.ManageGroupRemovedUsers:
@ -243,6 +251,8 @@ const RightHeader: FC<OwnProps & StateProps> = ({
return <h3>{lang('ChannelAddException')}</h3>; return <h3>{lang('ChannelAddException')}</h3>;
case HeaderContent.ManageGroupUserPermissions: case HeaderContent.ManageGroupUserPermissions:
return <h3>{lang('UserRestrictions')}</h3>; return <h3>{lang('UserRestrictions')}</h3>;
case HeaderContent.ManageGroupAddAdmins:
return <h3>{lang('Channel.Management.AddModerator')}</h3>;
case HeaderContent.StickerSearch: case HeaderContent.StickerSearch:
return ( return (
<SearchInput <SearchInput

View File

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

View File

@ -24,6 +24,7 @@ type OwnProps = {
chatId: string; chatId: string;
selectedChatMemberId?: string; selectedChatMemberId?: string;
isPromotedByCurrentUser?: boolean; isPromotedByCurrentUser?: boolean;
isNewAdmin?: boolean;
onScreenSelect: (screen: ManagementScreens) => void; onScreenSelect: (screen: ManagementScreens) => void;
onClose: NoneToVoidFunction; onClose: NoneToVoidFunction;
isActive: boolean; isActive: boolean;
@ -35,12 +36,15 @@ type StateProps = {
currentUserId?: string; currentUserId?: string;
isChannel: boolean; isChannel: boolean;
isFormFullyDisabled: boolean; isFormFullyDisabled: boolean;
defaultRights?: ApiChatAdminRights;
}; };
const CUSTOM_TITLE_MAX_LENGTH = 16; const CUSTOM_TITLE_MAX_LENGTH = 16;
const ManageGroupAdminRights: FC<OwnProps & StateProps> = ({ const ManageGroupAdminRights: FC<OwnProps & StateProps> = ({
isNewAdmin,
selectedChatMemberId, selectedChatMemberId,
defaultRights,
onScreenSelect, onScreenSelect,
chat, chat,
usersById, usersById,
@ -53,7 +57,7 @@ const ManageGroupAdminRights: FC<OwnProps & StateProps> = ({
const { updateChatAdmin } = getDispatch(); const { updateChatAdmin } = getDispatch();
const [permissions, setPermissions] = useState<ApiChatAdminRights>({}); const [permissions, setPermissions] = useState<ApiChatAdminRights>({});
const [isTouched, setIsTouched] = useState(false); const [isTouched, setIsTouched] = useState(isNewAdmin);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isDismissConfirmationDialogOpen, openDismissConfirmationDialog, closeDismissConfirmationDialog] = useFlag(); const [isDismissConfirmationDialogOpen, openDismissConfirmationDialog, closeDismissConfirmationDialog] = useFlag();
const [customTitle, setCustomTitle] = useState(''); const [customTitle, setCustomTitle] = useState('');
@ -62,12 +66,18 @@ const ManageGroupAdminRights: FC<OwnProps & StateProps> = ({
useHistoryBack(isActive, onClose); useHistoryBack(isActive, onClose);
const selectedChatMember = useMemo(() => { const selectedChatMember = useMemo(() => {
if (!chat.fullInfo || !chat.fullInfo.adminMembers) { const selectedAdminMember = chat.fullInfo?.adminMembers?.find(({ userId }) => userId === selectedChatMemberId);
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);
} }
return chat.fullInfo.adminMembers.find(({ userId }) => userId === selectedChatMemberId); return selectedAdminMember;
}, [chat, selectedChatMemberId]); }, [chat.fullInfo, isNewAdmin, selectedChatMemberId]);
useEffect(() => { useEffect(() => {
if (chat?.fullInfo && selectedChatMemberId && !selectedChatMember) { if (chat?.fullInfo && selectedChatMemberId && !selectedChatMember) {
@ -76,11 +86,11 @@ const ManageGroupAdminRights: FC<OwnProps & StateProps> = ({
}, [chat, onScreenSelect, selectedChatMember, selectedChatMemberId]); }, [chat, onScreenSelect, selectedChatMember, selectedChatMemberId]);
useEffect(() => { useEffect(() => {
setPermissions((selectedChatMember?.adminRights) || {}); setPermissions((isNewAdmin ? defaultRights : selectedChatMember?.adminRights) || {});
setCustomTitle(((selectedChatMember?.customTitle) || '').substr(0, CUSTOM_TITLE_MAX_LENGTH)); setCustomTitle(((isNewAdmin ? 'admin' : selectedChatMember?.customTitle) || '').substr(0, CUSTOM_TITLE_MAX_LENGTH));
setIsTouched(false); setIsTouched(Boolean(isNewAdmin));
setIsLoading(false); setIsLoading(false);
}, [selectedChatMember]); }, [defaultRights, isNewAdmin, selectedChatMember]);
const handlePermissionChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => { const handlePermissionChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const { name } = e.target; const { name } = e.target;
@ -108,7 +118,7 @@ const ManageGroupAdminRights: FC<OwnProps & StateProps> = ({
adminRights: permissions, adminRights: permissions,
customTitle, customTitle,
}); });
}, [chat, selectedChatMemberId, permissions, customTitle, updateChatAdmin]); }, [selectedChatMemberId, updateChatAdmin, chat.id, permissions, customTitle]);
const handleDismissAdmin = useCallback(() => { const handleDismissAdmin = useCallback(() => {
if (!selectedChatMemberId) { if (!selectedChatMemberId) {
@ -136,7 +146,7 @@ const ManageGroupAdminRights: FC<OwnProps & StateProps> = ({
}, [chat, isFormFullyDisabled]); }, [chat, isFormFullyDisabled]);
const memberStatus = useMemo(() => { const memberStatus = useMemo(() => {
if (!selectedChatMember) { if (isNewAdmin || !selectedChatMember) {
return undefined; return undefined;
} }
@ -153,7 +163,7 @@ const ManageGroupAdminRights: FC<OwnProps & StateProps> = ({
} }
return lang('ChannelAdmin'); return lang('ChannelAdmin');
}, [selectedChatMember, usersById, lang]); }, [isNewAdmin, selectedChatMember, usersById, lang]);
const handleCustomTitleChange = useCallback((e) => { const handleCustomTitleChange = useCallback((e) => {
const { value } = e.target; 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}> <ListItem icon="delete" ripple destructive onClick={openDismissConfirmationDialog}>
{lang('EditAdminRemoveAdmin')} {lang('EditAdminRemoveAdmin')}
</ListItem> </ListItem>
@ -328,14 +338,16 @@ const ManageGroupAdminRights: FC<OwnProps & StateProps> = ({
)} )}
</FloatingActionButton> </FloatingActionButton>
<ConfirmDialog {!isNewAdmin && (
isOpen={isDismissConfirmationDialogOpen} <ConfirmDialog
onClose={closeDismissConfirmationDialog} isOpen={isDismissConfirmationDialogOpen}
text="Are you sure you want to dismiss this admin?" onClose={closeDismissConfirmationDialog}
confirmLabel="Dismiss" text="Are you sure you want to dismiss this admin?"
confirmHandler={handleDismissAdmin} confirmLabel={lang('Channel.Admin.Dismiss')}
confirmIsDestructive confirmHandler={handleDismissAdmin}
/> confirmIsDestructive
/>
)}
</div> </div>
); );
}; };
@ -354,6 +366,7 @@ export default memo(withGlobal<OwnProps>(
currentUserId, currentUserId,
isChannel, isChannel,
isFormFullyDisabled, isFormFullyDisabled,
defaultRights: chat.adminRights,
}; };
}, },
)(ManageGroupAdminRights)); )(ManageGroupAdminRights));

View File

@ -1,9 +1,11 @@
import React, { import React, {
FC, memo, useCallback, useMemo, FC, memo, useCallback, useMemo,
} from '../../../lib/teact/teact'; } 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 { selectChat } from '../../../modules/selectors';
import { sortUserIds, isChatChannel } from '../../../modules/helpers'; import { sortUserIds, isChatChannel } from '../../../modules/helpers';
import useHistoryBack from '../../../hooks/useHistoryBack'; import useHistoryBack from '../../../hooks/useHistoryBack';
@ -14,46 +16,62 @@ import ListItem from '../../ui/ListItem';
type OwnProps = { type OwnProps = {
chatId: string; chatId: string;
onClose: NoneToVoidFunction;
isActive: boolean; isActive: boolean;
noAdmins?: boolean;
onClose: NoneToVoidFunction;
onScreenSelect?: (screen: ManagementScreens) => void;
onChatMemberSelect?: (memberId: string, isPromotedByCurrentUser?: boolean) => void;
}; };
type StateProps = { type StateProps = {
usersById: Record<string, ApiUser>;
userStatusesById: Record<string, ApiUserStatus>; userStatusesById: Record<string, ApiUserStatus>;
members?: ApiChatMember[]; members?: ApiChatMember[];
adminMembers?: ApiChatMember[];
isChannel?: boolean; isChannel?: boolean;
serverTimeOffset: number; serverTimeOffset: number;
}; };
const ManageGroupMembers: FC<OwnProps & StateProps> = ({ const ManageGroupMembers: FC<OwnProps & StateProps> = ({
noAdmins,
members, members,
usersById, adminMembers,
userStatusesById, userStatusesById,
isChannel, isChannel,
onClose,
isActive, isActive,
serverTimeOffset, serverTimeOffset,
onClose,
onScreenSelect,
onChatMemberSelect,
}) => { }) => {
const { openUserInfo } = getDispatch(); const { openUserInfo } = getDispatch();
const memberIds = useMemo(() => { const memberIds = useMemo(() => {
// No need for expensive global updates on users, so we avoid them
const usersById = getGlobal().users.byId;
if (!members || !usersById) { if (!members || !usersById) {
return undefined; return undefined;
} }
const adminIds = noAdmins ? adminMembers?.map(({ userId }) => userId) || [] : [];
return sortUserIds( const userIds = sortUserIds(
members.map(({ userId }) => userId), members.map(({ userId }) => userId),
usersById, usersById,
userStatusesById, userStatusesById,
undefined, undefined,
serverTimeOffset, serverTimeOffset,
); );
}, [members, serverTimeOffset, usersById, userStatusesById]);
return noAdmins ? userIds.filter((userId) => !adminIds.includes(userId)) : userIds;
}, [members, noAdmins, adminMembers, userStatusesById, serverTimeOffset]);
const handleMemberClick = useCallback((id: string) => { const handleMemberClick = useCallback((id: string) => {
openUserInfo({ id }); if (noAdmins) {
}, [openUserInfo]); onChatMemberSelect!(id, false);
onScreenSelect!(ManagementScreens.ChatNewAdminRights);
} else {
openUserInfo({ id });
}
}, [noAdmins, onChatMemberSelect, onScreenSelect, openUserInfo]);
useHistoryBack(isActive, onClose); useHistoryBack(isActive, onClose);
@ -88,13 +106,14 @@ const ManageGroupMembers: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global, { chatId }): StateProps => { (global, { chatId }): StateProps => {
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
const { byId: usersById, statusesById: userStatusesById } = global.users; const { statusesById: userStatusesById } = global.users;
const members = chat?.fullInfo?.members; const members = chat?.fullInfo?.members;
const adminMembers = chat?.fullInfo?.adminMembers;
const isChannel = chat && isChatChannel(chat); const isChannel = chat && isChatChannel(chat);
return { return {
members, members,
usersById, adminMembers,
userStatusesById, userStatusesById,
isChannel, isChannel,
serverTimeOffset: global.serverTimeOffset, serverTimeOffset: global.serverTimeOffset,

View File

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

View File

@ -763,8 +763,8 @@ addReducer('updateChatAdmin', (global, actions, payload) => {
chat, user, adminRights, customTitle, chat, user, adminRights, customTitle,
}); });
const chatAfterUpdate = await callApi('fetchFullChat', chat);
const newGlobal = getGlobal(); const newGlobal = getGlobal();
const chatAfterUpdate = selectChat(newGlobal, chatId);
if (!chatAfterUpdate || !chatAfterUpdate.fullInfo) { if (!chatAfterUpdate || !chatAfterUpdate.fullInfo) {
return; return;

View File

@ -317,7 +317,9 @@ export enum ManagementScreens {
ChatAdministrators, ChatAdministrators,
GroupRecentActions, GroupRecentActions,
ChatAdminRights, ChatAdminRights,
ChatNewAdminRights,
GroupMembers, GroupMembers,
GroupAddAdmins,
} }
export type ManagementType = 'user' | 'group' | 'channel'; 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', USER_ALREADY_PARTICIPANT: 'You already in the group',
SCHEDULE_DATE_INVALID: 'Invalid schedule date provided', SCHEDULE_DATE_INVALID: 'Invalid schedule date provided',
WALLPAPER_DIMENSIONS_INVALID: 'The wallpaper dimensions are invalid, please select another file', 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> = { export const SHIPPING_ERRORS: Record<string, ApiFieldError> = {