521 lines
16 KiB
TypeScript
521 lines
16 KiB
TypeScript
import type { ChangeEvent } from 'react';
|
|
import type { FC } from '../../../lib/teact/teact';
|
|
import React, {
|
|
memo, useEffect, useMemo, useRef, useState,
|
|
} from '../../../lib/teact/teact';
|
|
import { getActions, withGlobal } from '../../../global';
|
|
|
|
import type {
|
|
ApiAvailableReaction, ApiChat, ApiChatBannedRights, ApiChatFullInfo, ApiExportedInvite,
|
|
} from '../../../api/types';
|
|
import { ApiMediaFormat } from '../../../api/types';
|
|
import { ManagementProgress, ManagementScreens } from '../../../types';
|
|
|
|
import {
|
|
getChatAvatarHash,
|
|
getHasAdminRight,
|
|
isChatBasicGroup,
|
|
isChatPublic,
|
|
} from '../../../global/helpers';
|
|
import { selectChat, selectChatFullInfo, selectTabState } from '../../../global/selectors';
|
|
import { debounce } from '../../../util/schedulers';
|
|
import { formatInteger } from '../../../util/textFormat';
|
|
import renderText from '../../common/helpers/renderText';
|
|
|
|
import useFlag from '../../../hooks/useFlag';
|
|
import useHistoryBack from '../../../hooks/useHistoryBack';
|
|
import useLang from '../../../hooks/useLang';
|
|
import useLastCallback from '../../../hooks/useLastCallback';
|
|
import useMedia from '../../../hooks/useMedia';
|
|
|
|
import AvatarEditable from '../../ui/AvatarEditable';
|
|
import Checkbox from '../../ui/Checkbox';
|
|
import ConfirmDialog from '../../ui/ConfirmDialog';
|
|
import FloatingActionButton from '../../ui/FloatingActionButton';
|
|
import InputText from '../../ui/InputText';
|
|
import ListItem from '../../ui/ListItem';
|
|
import Spinner from '../../ui/Spinner';
|
|
import Switcher from '../../ui/Switcher';
|
|
import TextArea from '../../ui/TextArea';
|
|
|
|
import './Management.scss';
|
|
|
|
type OwnProps = {
|
|
chatId: string;
|
|
onScreenSelect: (screen: ManagementScreens) => void;
|
|
onClose: NoneToVoidFunction;
|
|
isActive: boolean;
|
|
};
|
|
|
|
type StateProps = {
|
|
chat: ApiChat;
|
|
chatFullInfo?: ApiChatFullInfo;
|
|
progress?: ManagementProgress;
|
|
isBasicGroup: boolean;
|
|
hasLinkedChannel: boolean;
|
|
canChangeInfo?: boolean;
|
|
canBanUsers?: boolean;
|
|
canInvite?: boolean;
|
|
canEditForum?: boolean;
|
|
exportedInvites?: ApiExportedInvite[];
|
|
isChannelsPremiumLimitReached: boolean;
|
|
availableReactions?: ApiAvailableReaction[];
|
|
};
|
|
|
|
const GROUP_TITLE_EMPTY = 'Group title can\'t be empty';
|
|
const GROUP_MAX_DESCRIPTION = 255;
|
|
|
|
const ALL_PERMISSIONS: Array<keyof ApiChatBannedRights> = [
|
|
'sendMessages',
|
|
'embedLinks',
|
|
'sendPolls',
|
|
'changeInfo',
|
|
'inviteUsers',
|
|
'pinMessages',
|
|
'manageTopics',
|
|
'sendPhotos',
|
|
'sendVideos',
|
|
'sendRoundvideos',
|
|
'sendVoices',
|
|
'sendAudios',
|
|
'sendDocs',
|
|
];
|
|
// Some checkboxes control multiple rights, and some rights are not controlled from Permissions screen,
|
|
// so we need to define the amount manually
|
|
const TOTAL_PERMISSIONS_COUNT = ALL_PERMISSIONS.length + 1;
|
|
|
|
const runDebounced = debounce((cb) => cb(), 500, false);
|
|
|
|
const ManageGroup: FC<OwnProps & StateProps> = ({
|
|
chatId,
|
|
chat,
|
|
chatFullInfo,
|
|
progress,
|
|
isBasicGroup,
|
|
hasLinkedChannel,
|
|
canChangeInfo,
|
|
canBanUsers,
|
|
canInvite,
|
|
canEditForum,
|
|
isActive,
|
|
exportedInvites,
|
|
isChannelsPremiumLimitReached,
|
|
availableReactions,
|
|
onScreenSelect,
|
|
onClose,
|
|
}) => {
|
|
const {
|
|
togglePreHistoryHidden,
|
|
updateChat,
|
|
deleteChat,
|
|
leaveChannel,
|
|
deleteChannel,
|
|
closeManagement,
|
|
openChat,
|
|
loadExportedChatInvites,
|
|
loadChatJoinRequests,
|
|
toggleForum,
|
|
} = getActions();
|
|
|
|
const [isDeleteDialogOpen, openDeleteDialog, closeDeleteDialog] = useFlag();
|
|
const currentTitle = chat.title;
|
|
const currentAbout = chatFullInfo?.about || '';
|
|
|
|
const [isProfileFieldsTouched, setIsProfileFieldsTouched] = useState(false);
|
|
const [title, setTitle] = useState(currentTitle);
|
|
const [about, setAbout] = useState(currentAbout);
|
|
const [photo, setPhoto] = useState<File | undefined>();
|
|
const [error, setError] = useState<string | undefined>();
|
|
const [isForumEnabled, setIsForumEnabled] = useState(chat.isForum);
|
|
const imageHash = getChatAvatarHash(chat);
|
|
const currentAvatarBlobUrl = useMedia(imageHash, false, ApiMediaFormat.BlobUrl);
|
|
const isPublicGroup = useMemo(() => isChatPublic(chat), [chat]);
|
|
const lang = useLang();
|
|
// eslint-disable-next-line no-null/no-null
|
|
const isPreHistoryHiddenCheckboxRef = useRef<HTMLDivElement>(null);
|
|
|
|
useHistoryBack({
|
|
isActive,
|
|
onBack: onClose,
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (canInvite) {
|
|
loadExportedChatInvites({ chatId });
|
|
loadExportedChatInvites({ chatId, isRevoked: true });
|
|
loadChatJoinRequests({ chatId });
|
|
}
|
|
}, [chatId, canInvite]);
|
|
|
|
// Resetting `isForum` switch on flood wait error
|
|
useEffect(() => {
|
|
setIsForumEnabled(Boolean(chat.isForum));
|
|
}, [chat.isForum]);
|
|
|
|
useEffect(() => {
|
|
if (progress === ManagementProgress.Complete) {
|
|
setIsProfileFieldsTouched(false);
|
|
setError(undefined);
|
|
}
|
|
}, [progress]);
|
|
|
|
const handleClickEditType = useLastCallback(() => {
|
|
onScreenSelect(ManagementScreens.ChatPrivacyType);
|
|
});
|
|
|
|
const handleClickDiscussion = useLastCallback(() => {
|
|
onScreenSelect(ManagementScreens.Discussion);
|
|
});
|
|
|
|
const handleClickReactions = useLastCallback(() => {
|
|
onScreenSelect(ManagementScreens.Reactions);
|
|
});
|
|
|
|
const handleClickPermissions = useLastCallback(() => {
|
|
onScreenSelect(ManagementScreens.GroupPermissions);
|
|
});
|
|
|
|
const handleClickAdministrators = useLastCallback(() => {
|
|
onScreenSelect(ManagementScreens.ChatAdministrators);
|
|
});
|
|
|
|
const handleClickInvites = useLastCallback(() => {
|
|
onScreenSelect(ManagementScreens.Invites);
|
|
});
|
|
|
|
const handleClickRequests = useLastCallback(() => {
|
|
onScreenSelect(ManagementScreens.JoinRequests);
|
|
});
|
|
|
|
const handleSetPhoto = useLastCallback((file: File) => {
|
|
setPhoto(file);
|
|
setIsProfileFieldsTouched(true);
|
|
});
|
|
|
|
const handleTitleChange = useLastCallback((e: ChangeEvent<HTMLInputElement>) => {
|
|
setTitle(e.target.value);
|
|
setIsProfileFieldsTouched(true);
|
|
});
|
|
|
|
const handleAboutChange = useLastCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
|
|
setAbout(e.target.value);
|
|
setIsProfileFieldsTouched(true);
|
|
});
|
|
|
|
const handleUpdateGroup = useLastCallback(() => {
|
|
const trimmedTitle = title.trim();
|
|
const trimmedAbout = about.trim();
|
|
|
|
if (!trimmedTitle.length) {
|
|
setError(GROUP_TITLE_EMPTY);
|
|
return;
|
|
}
|
|
|
|
updateChat({
|
|
chatId,
|
|
title: trimmedTitle,
|
|
about: trimmedAbout,
|
|
photo,
|
|
});
|
|
});
|
|
|
|
const handleClickMembers = useLastCallback(() => {
|
|
onScreenSelect(ManagementScreens.GroupMembers);
|
|
});
|
|
|
|
const handleTogglePreHistory = useLastCallback(() => {
|
|
if (!chatFullInfo) {
|
|
return;
|
|
}
|
|
|
|
const { isPreHistoryHidden } = chatFullInfo;
|
|
|
|
togglePreHistoryHidden({ chatId: chat.id, isEnabled: !isPreHistoryHidden });
|
|
});
|
|
|
|
const handleForumToggle = useLastCallback(() => {
|
|
setIsForumEnabled((current) => {
|
|
const newIsForumEnabled = !current;
|
|
|
|
runDebounced(() => {
|
|
toggleForum({ chatId, isEnabled: newIsForumEnabled });
|
|
});
|
|
|
|
return newIsForumEnabled;
|
|
});
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (!isChannelsPremiumLimitReached) {
|
|
return;
|
|
}
|
|
|
|
// Teact does not have full support of controlled form components, we need to "disable" input value change manually
|
|
// TODO Teact support added, this can now be removed
|
|
const checkbox = isPreHistoryHiddenCheckboxRef.current?.querySelector('input') as HTMLInputElement;
|
|
checkbox.checked = !chatFullInfo?.isPreHistoryHidden;
|
|
}, [isChannelsPremiumLimitReached, chatFullInfo?.isPreHistoryHidden]);
|
|
|
|
const chatReactionsDescription = useMemo(() => {
|
|
if (!chatFullInfo?.enabledReactions) {
|
|
return lang('ReactionsOff');
|
|
}
|
|
|
|
if (chatFullInfo.enabledReactions.type === 'all') {
|
|
return lang('ReactionsAll');
|
|
}
|
|
|
|
const enabledLength = chatFullInfo.enabledReactions.allowed.length;
|
|
const totalLength = availableReactions?.filter((reaction) => !reaction.isInactive).length || 0;
|
|
|
|
return totalLength
|
|
? `${enabledLength} / ${totalLength}`
|
|
: `${enabledLength}`;
|
|
}, [availableReactions, chatFullInfo?.enabledReactions, lang]);
|
|
|
|
const enabledPermissionsCount = useMemo(() => {
|
|
if (!chat.defaultBannedRights) {
|
|
return 0;
|
|
}
|
|
|
|
let totalCount = ALL_PERMISSIONS.filter(
|
|
(key) => {
|
|
if (key === 'manageTopics' && !isForumEnabled) return false;
|
|
return !chat.defaultBannedRights![key as keyof ApiChatBannedRights];
|
|
},
|
|
).length;
|
|
|
|
const { sendStickers, sendGifs } = chat.defaultBannedRights;
|
|
|
|
// These two rights are controlled with a single checkbox
|
|
if (!sendStickers && !sendGifs) {
|
|
totalCount += 1;
|
|
}
|
|
|
|
return totalCount;
|
|
}, [chat.defaultBannedRights, isForumEnabled]);
|
|
|
|
const adminsCount = useMemo(() => {
|
|
return Object.keys(chatFullInfo?.adminMembersById || {}).length;
|
|
}, [chatFullInfo?.adminMembersById]);
|
|
|
|
const handleDeleteGroup = useLastCallback(() => {
|
|
if (isBasicGroup) {
|
|
deleteChat({ chatId: chat.id });
|
|
} else if (!chat.isCreator) {
|
|
leaveChannel({ chatId: chat.id });
|
|
} else {
|
|
deleteChannel({ chatId: chat.id });
|
|
}
|
|
closeDeleteDialog();
|
|
closeManagement();
|
|
openChat({ id: undefined });
|
|
});
|
|
|
|
if (chat.isRestricted || chat.isForbidden) {
|
|
return undefined;
|
|
}
|
|
|
|
const isLoading = progress === ManagementProgress.InProgress;
|
|
|
|
return (
|
|
<div className="Management">
|
|
<div className="custom-scroll">
|
|
<div className="section">
|
|
<AvatarEditable
|
|
isForForum={isForumEnabled}
|
|
currentAvatarBlobUrl={currentAvatarBlobUrl}
|
|
onChange={handleSetPhoto}
|
|
disabled={!canChangeInfo}
|
|
/>
|
|
<InputText
|
|
id="group-title"
|
|
label={lang('GroupName')}
|
|
onChange={handleTitleChange}
|
|
value={title}
|
|
error={error === GROUP_TITLE_EMPTY ? error : undefined}
|
|
disabled={!canChangeInfo}
|
|
/>
|
|
<TextArea
|
|
id="group-about"
|
|
className="mb-2"
|
|
label={lang('DescriptionPlaceholder')}
|
|
maxLength={GROUP_MAX_DESCRIPTION}
|
|
maxLengthIndicator={(GROUP_MAX_DESCRIPTION - about.length).toString()}
|
|
onChange={handleAboutChange}
|
|
value={about}
|
|
disabled={!canChangeInfo}
|
|
noReplaceNewlines
|
|
/>
|
|
{chat.isCreator && (
|
|
<ListItem icon="lock" multiline onClick={handleClickEditType}>
|
|
<span className="title">{lang('GroupType')}</span>
|
|
<span className="subtitle">{isPublicGroup ? lang('TypePublic') : lang('TypePrivate')}</span>
|
|
</ListItem>
|
|
)}
|
|
{hasLinkedChannel && (
|
|
<ListItem
|
|
icon="message"
|
|
multiline
|
|
onClick={handleClickDiscussion}
|
|
>
|
|
<span className="title">{lang('LinkedChannel')}</span>
|
|
<span className="subtitle">{lang('DiscussionUnlink')}</span>
|
|
</ListItem>
|
|
)}
|
|
<ListItem
|
|
icon="permissions"
|
|
multiline
|
|
onClick={handleClickPermissions}
|
|
disabled={!canBanUsers}
|
|
>
|
|
<span className="title">{lang('ChannelPermissions')}</span>
|
|
<span className="subtitle" dir="auto">
|
|
{enabledPermissionsCount}/{TOTAL_PERMISSIONS_COUNT - (isForumEnabled ? 0 : 1)}
|
|
</span>
|
|
</ListItem>
|
|
<ListItem
|
|
icon="heart-outline"
|
|
multiline
|
|
onClick={handleClickReactions}
|
|
disabled={!canChangeInfo}
|
|
>
|
|
<span className="title">{lang('Reactions')}</span>
|
|
<span className="subtitle" dir="auto">
|
|
{chatReactionsDescription}
|
|
</span>
|
|
</ListItem>
|
|
<ListItem
|
|
icon="admin"
|
|
multiline
|
|
onClick={handleClickAdministrators}
|
|
>
|
|
<span className="title">{lang('ChannelAdministrators')}</span>
|
|
<span className="subtitle">{formatInteger(adminsCount)}</span>
|
|
</ListItem>
|
|
{canInvite && (
|
|
<ListItem
|
|
icon="link"
|
|
onClick={handleClickInvites}
|
|
multiline
|
|
disabled={!exportedInvites}
|
|
>
|
|
<span className="title">{lang('GroupInfo.InviteLinks')}</span>
|
|
<span className="subtitle">
|
|
{exportedInvites ? formatInteger(exportedInvites.length) : lang('Loading')}
|
|
</span>
|
|
</ListItem>
|
|
)}
|
|
{Boolean(chat.joinRequests?.length) && (
|
|
<ListItem
|
|
icon="add-user-filled"
|
|
onClick={handleClickRequests}
|
|
multiline
|
|
>
|
|
<span className="title">{lang('MemberRequests')}</span>
|
|
<span className="subtitle">
|
|
{formatInteger(chat.joinRequests!.length)}
|
|
</span>
|
|
</ListItem>
|
|
)}
|
|
{canEditForum && (
|
|
<>
|
|
<ListItem icon="forums" ripple onClick={handleForumToggle}>
|
|
<span>{lang('ChannelTopics')}</span>
|
|
<Switcher
|
|
id="group-notifications"
|
|
label={lang('ChannelTopics')}
|
|
checked={isForumEnabled}
|
|
inactive
|
|
/>
|
|
</ListItem>
|
|
<div className="section-info section-info_push">{lang('ForumToggleDescription')}</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
<div className="section">
|
|
<ListItem icon="group" multiline onClick={handleClickMembers}>
|
|
<span className="title">{lang('GroupMembers')}</span>
|
|
<span className="subtitle">{formatInteger(chat.membersCount ?? 0)}</span>
|
|
</ListItem>
|
|
|
|
{!isPublicGroup && !hasLinkedChannel && Boolean(chatFullInfo) && (
|
|
<div className="ListItem narrow" ref={isPreHistoryHiddenCheckboxRef}>
|
|
<Checkbox
|
|
checked={!chatFullInfo.isPreHistoryHidden}
|
|
label={lang('ChatHistory')}
|
|
onChange={handleTogglePreHistory}
|
|
disabled={!canBanUsers}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="section">
|
|
<ListItem icon="delete" ripple destructive onClick={openDeleteDialog}>
|
|
{lang('DeleteMega')}
|
|
</ListItem>
|
|
</div>
|
|
</div>
|
|
<FloatingActionButton
|
|
isShown={isProfileFieldsTouched}
|
|
onClick={handleUpdateGroup}
|
|
disabled={isLoading}
|
|
ariaLabel={lang('Save')}
|
|
>
|
|
{isLoading ? (
|
|
<Spinner color="white" />
|
|
) : (
|
|
<i className="icon icon-check" />
|
|
)}
|
|
</FloatingActionButton>
|
|
<ConfirmDialog
|
|
isOpen={isDeleteDialogOpen}
|
|
onClose={closeDeleteDialog}
|
|
textParts={renderText(
|
|
isBasicGroup || !chat.isCreator
|
|
? lang('AreYouSureDeleteAndExit')
|
|
: lang('AreYouSureDeleteThisChatWithGroup', chat.title),
|
|
['br', 'simple_markdown'],
|
|
)}
|
|
confirmLabel={isBasicGroup || !chat.isCreator ? lang('DeleteMega') : lang('DeleteGroupForAll')}
|
|
confirmHandler={handleDeleteGroup}
|
|
confirmIsDestructive
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default memo(withGlobal<OwnProps>(
|
|
(global, { chatId }): StateProps => {
|
|
const chat = selectChat(global, chatId)!;
|
|
const chatFullInfo = selectChatFullInfo(global, chatId);
|
|
const { management, limitReachedModal } = selectTabState(global);
|
|
const { progress } = management;
|
|
const hasLinkedChannel = Boolean(chatFullInfo?.linkedChatId);
|
|
const isBasicGroup = isChatBasicGroup(chat);
|
|
const { invites } = management.byChatId[chatId] || {};
|
|
const canEditForum = !hasLinkedChannel && (getHasAdminRight(chat, 'changeInfo') || chat.isCreator);
|
|
const canChangeInfo = chat.isCreator || getHasAdminRight(chat, 'changeInfo');
|
|
const canBanUsers = chat.isCreator || getHasAdminRight(chat, 'banUsers');
|
|
const canInvite = chat.isCreator || getHasAdminRight(chat, 'inviteUsers');
|
|
|
|
return {
|
|
chat,
|
|
chatFullInfo,
|
|
progress,
|
|
isBasicGroup,
|
|
hasLinkedChannel,
|
|
canChangeInfo,
|
|
canBanUsers,
|
|
canInvite,
|
|
exportedInvites: invites,
|
|
isChannelsPremiumLimitReached: limitReachedModal?.limit === 'channels',
|
|
availableReactions: global.reactions.availableReactions,
|
|
canEditForum,
|
|
};
|
|
},
|
|
(global, { chatId }) => {
|
|
return Boolean(selectChat(global, chatId));
|
|
},
|
|
)(ManageGroup));
|