TelegramPWA/src/components/common/DeleteMessageModal.tsx
2026-02-22 23:49:11 +01:00

539 lines
18 KiB
TypeScript

import type { FC } from '../../lib/teact/teact';
import {
memo,
useEffect,
useMemo,
useState,
} from '../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../global';
import type { ApiChat, ApiChatMember } from '../../api/types';
import type { IRadioOption } from '../ui/CheckboxGroup';
import {
getHasAdminRight,
getUserFirstOrLastName, isChatBasicGroup,
isChatChannel,
isChatSuperGroup,
isSystemBot,
} from '../../global/helpers';
import { getPeerTitle } from '../../global/helpers/peers';
import {
getSendersFromSelectedMessages,
selectBot,
selectCanDeleteSelectedMessages,
selectChat,
selectChatFullInfo,
selectIsChatWithBot,
selectSenderFromMessage,
selectTabState,
selectUser,
} from '../../global/selectors';
import buildClassName from '../../util/buildClassName';
import { isUserId } from '../../util/entities/ids';
import { buildCollectionByCallback, unique } from '../../util/iteratees';
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
import renderText from './helpers/renderText';
import useLang from '../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback';
import useOldLang from '../../hooks/useOldLang';
import usePreviousDeprecated from '../../hooks/usePreviousDeprecated';
import useManagePermissions from '../right/hooks/useManagePermissions';
import PermissionCheckboxList from '../main/PermissionCheckboxList';
import Button from '../ui/Button';
import Checkbox from '../ui/Checkbox';
import CheckboxGroup from '../ui/CheckboxGroup';
import ListItem from '../ui/ListItem';
import Modal from '../ui/Modal';
import Avatar from './Avatar';
import AvatarList from './AvatarList';
import Icon from './icons/Icon';
import styles from './DeleteMessageModal.module.scss';
export type OwnProps = {
isOpen?: boolean;
};
type StateProps = {
chat?: ApiChat;
isChannel?: boolean;
isSuperGroup?: boolean;
messageIds?: number[];
canDeleteForAll?: boolean;
contactName?: string;
currentUserId?: string;
willDeleteForCurrentUserOnly?: boolean;
willDeleteForAll?: boolean;
adminMembersById?: Record<string, ApiChatMember>;
chatBot?: boolean;
isSchedule?: boolean;
onConfirm?: NoneToVoidFunction;
canBanUsers?: boolean;
isCreator?: boolean;
linkedChatId?: string;
};
const DeleteMessageModal: FC<OwnProps & StateProps> = ({
isOpen,
chat,
isChannel,
isSuperGroup,
isSchedule,
currentUserId,
messageIds,
isCreator,
canDeleteForAll,
contactName,
willDeleteForCurrentUserOnly,
willDeleteForAll,
chatBot,
adminMembersById,
canBanUsers,
linkedChatId,
onConfirm,
}) => {
const {
closeDeleteMessageModal,
deleteMessages,
reportChannelSpam,
deleteChatMember,
deleteScheduledMessages,
exitMessageSelectMode,
updateChatMemberBannedRights,
deleteParticipantHistory,
} = getActions();
const prevIsOpen = usePreviousDeprecated(isOpen);
const oldLang = useOldLang();
const lang = useLang();
const {
permissions, havePermissionChanged, handlePermissionChange, resetPermissions,
} = useManagePermissions(chat?.defaultBannedRights);
const [peerIdsToDeleteAll, setPeerIdsToDeleteAll] = useState<string[]>([]);
const [peerIdsToBan, setPeerIdsToBan] = useState<string[]>([]);
const [peerIdsToReportSpam, setPeerIdsToReportSpam] = useState<string[]>([]);
const [isMediaDropdownOpen, setIsMediaDropdownOpen] = useState(false);
const [isAdditionalOptionsVisible, setIsAdditionalOptionsVisible] = useState(false);
const [shouldDeleteForAll, setShouldDeleteForAll] = useState(true);
const peerList = useMemo(() => {
if (isChannel || !messageIds || !chat) {
return MEMO_EMPTY_ARRAY;
}
const global = getGlobal();
const senderArray = getSendersFromSelectedMessages(global, chat.id, messageIds);
return senderArray ? unique(senderArray)
.filter((peer) => (
peer?.id !== chat?.id
&& peer?.id !== linkedChatId
&& peer?.id !== chat?.linkedMonoforumId
)) : MEMO_EMPTY_ARRAY;
}, [chat, isChannel, linkedChatId, messageIds]);
const buildNestedOptionListWithAvatars = useLastCallback(() => {
return peerList.map((member) => {
return {
value: member.id,
label: getPeerTitle(lang, member) || '',
leftElement: <Avatar size="small" peer={member} />,
};
});
});
const peerListToDeleteAll = useMemo(() => {
return peerList.filter((peer) => (
peer.id !== linkedChatId
&& peer.id !== chat?.linkedMonoforumId
&& peer.id !== currentUserId
));
}, [peerList, currentUserId, linkedChatId, chat?.linkedMonoforumId]);
const peerListToReportSpam = useMemo(() => {
return peerList.filter((peer) => (
peer.id !== currentUserId
&& peer.id !== linkedChatId
&& peer.id !== chat?.linkedMonoforumId
));
}, [peerList, currentUserId, linkedChatId, chat?.linkedMonoforumId]);
const peerListToBan = useMemo(() => {
const isCurrentUserInList = peerList.some((peer) => peer.id === currentUserId);
const shouldReturnEmpty = !canBanUsers || isCurrentUserInList;
if (shouldReturnEmpty) {
return MEMO_EMPTY_ARRAY;
}
return peerList.filter((peer) => {
const isAdmin = adminMembersById?.[peer.id];
return isCreator || !isAdmin;
});
}, [peerList, isCreator, currentUserId, canBanUsers, adminMembersById]);
const shouldShowAdditionalOptions = useMemo(() => {
return Boolean(peerListToDeleteAll.length || peerListToReportSpam.length || peerListToBan.length);
}, [peerListToDeleteAll, peerListToReportSpam, peerListToBan]);
const shouldShowOption = shouldShowAdditionalOptions
&& !canDeleteForAll && !isSchedule && isSuperGroup;
const peerNames = useMemo(() => {
if (!peerList || isSchedule) return {};
return buildCollectionByCallback(peerList, (peer) => [peer.id, getPeerTitle(lang, peer)]);
}, [isSchedule, lang, peerList]);
const ACTION_SPAM_OPTION: IRadioOption[] = useMemo(() => {
return [
{
value: messageIds && peerList.length >= 2 ? 'spam' : peerList?.[0]?.id,
label: oldLang('ReportSpamTitle'),
nestedOptions: messageIds && peerList.length >= 2 ? [
...buildNestedOptionListWithAvatars().filter((opt) => opt.value !== linkedChatId
&& opt.value !== chat?.linkedMonoforumId
&& opt.value !== currentUserId),
] : undefined,
},
];
}, [messageIds, peerList, oldLang, linkedChatId, chat?.linkedMonoforumId, currentUserId]);
const ACTION_DELETE_OPTION: IRadioOption[] = useMemo(() => {
return [
{
value: messageIds && peerList.length >= 2 ? 'delete_all' : peerList?.[0]?.id,
label: messageIds && peerList.length >= 2
? oldLang('DeleteAllFromUsers')
: oldLang('DeleteAllFrom', Object.values(peerNames)[0]),
nestedOptions: messageIds && peerList.length >= 2 ? [
...buildNestedOptionListWithAvatars().filter((opt) => opt.value !== linkedChatId
&& opt.value !== chat?.linkedMonoforumId
&& opt.value !== currentUserId),
] : undefined,
},
];
}, [messageIds, peerList, oldLang, peerNames, linkedChatId, chat?.linkedMonoforumId, currentUserId]);
const ACTION_BAN_OPTION: IRadioOption[] = useMemo(() => {
return [
{
value: messageIds && peerList.length >= 2 ? 'ban' : peerList?.[0]?.id,
label: messageIds && peerList.length >= 2
? (isAdditionalOptionsVisible ? oldLang('DeleteRestrictUsers') : oldLang('DeleteBanUsers'))
: (isAdditionalOptionsVisible ? oldLang('KickFromSupergroup')
: oldLang('DeleteBan', Object.values(peerNames)[0])),
nestedOptions: messageIds && peerList.length >= 2 ? [
...buildNestedOptionListWithAvatars(),
] : undefined,
},
];
}, [isAdditionalOptionsVisible, oldLang, messageIds, peerList, peerNames]);
const toggleAdditionalOptions = useLastCallback(() => {
setIsAdditionalOptionsVisible((prev) => !prev);
});
const filterMessageIdByPeerId = useLastCallback((peerIds: string[], selectedMessageIdList: number[]) => {
if (!chat) return MEMO_EMPTY_ARRAY;
const global = getGlobal();
return selectedMessageIdList.filter((msgId) => {
const sender = selectSenderFromMessage(global, chat.id, msgId);
return sender && peerIds.includes(sender.id);
});
});
const handleReportSpam = useLastCallback((userMessagesMap: Record<string, number[]>) => {
Object.entries(userMessagesMap).forEach(([userId, messageIdList]) => {
if (messageIdList.length) {
reportChannelSpam({
participantId: userId,
chatId: chat!.id,
messageIds: messageIdList,
});
}
});
});
const handleDeleteMessages = useLastCallback((filteredMessageIdList: number[]) => {
deleteMessages({ messageIds: filteredMessageIdList, shouldDeleteForAll: true });
});
const handleDeleteAllPeerMessages = useLastCallback((peerIdList: string[]) => {
if (!chat) return;
peerIdList.forEach((peerId) => {
deleteParticipantHistory({ peerId, chatId: chat.id });
});
});
const handleDeleteMember = useLastCallback((filteredUserIdList: string[]) => {
filteredUserIdList.forEach((userId) => {
deleteChatMember({ chatId: chat!.id, userId });
});
});
const handleUpdateChatMemberBannedRights = useLastCallback((filteredUserIdList: string[]) => {
filteredUserIdList.forEach((userId) => {
updateChatMemberBannedRights({
chatId: chat!.id,
userId,
bannedRights: permissions,
});
});
});
const handleDeleteMessageList = useLastCallback(() => {
if (!chat || !messageIds) return;
onConfirm?.();
if (isSchedule) {
deleteScheduledMessages({ messageIds });
} else if (shouldShowOption) {
if (peerIdsToReportSpam?.length) {
const global = getGlobal();
const peerIdList = peerIdsToReportSpam.filter((option) => !Number.isNaN(Number(option)));
const messageList = messageIds.reduce<Record<string, number[]>>((acc, msgId) => {
const peer = selectSenderFromMessage(global, chat.id, msgId);
if (peer && peerIdList.includes(peer.id)) {
if (!acc[peer.id]) {
acc[peer.id] = [];
}
acc[peer.id].push(Number(msgId));
}
return acc;
}, {});
handleReportSpam(messageList);
}
if (peerIdsToDeleteAll?.length) {
const peerIdList = peerIdsToDeleteAll.filter((option) => !Number.isNaN(Number(option)));
handleDeleteAllPeerMessages(peerIdList);
}
if (peerIdsToBan?.length && !havePermissionChanged) {
const peerIdList = peerIdsToBan.filter((option) => !Number.isNaN(Number(option)));
handleDeleteMember(peerIdList);
const filteredMessageIdList = filterMessageIdByPeerId(peerIdList, messageIds);
handleDeleteMessages(filteredMessageIdList);
}
if (peerIdsToBan?.length && havePermissionChanged) {
const peerIdList = peerIdsToBan.filter((option) => !Number.isNaN(Number(option)));
handleUpdateChatMemberBannedRights(peerIdList);
}
if (!peerIdsToReportSpam?.length || !peerIdsToDeleteAll?.length || !peerIdsToBan?.length) {
deleteMessages({ messageIds, shouldDeleteForAll });
}
} else {
deleteMessages({ messageIds, shouldDeleteForAll });
}
closeDeleteMessageModal();
exitMessageSelectMode();
});
const onCloseHandler = useLastCallback(() => {
closeDeleteMessageModal();
});
useEffect(() => {
if (!isOpen && prevIsOpen) {
setPeerIdsToReportSpam([]);
setPeerIdsToDeleteAll([]);
setPeerIdsToBan([]);
setShouldDeleteForAll(true);
setIsMediaDropdownOpen(false);
setIsAdditionalOptionsVisible(false);
resetPermissions();
}
}, [isOpen, prevIsOpen, resetPermissions]);
function renderHeader() {
return (
<div
className={shouldShowOption && styles.container}
dir={lang.isRtl ? 'rtl' : undefined}
>
{shouldShowOption && (
<AvatarList
size="small"
peers={peerList}
/>
)}
<h3 className={buildClassName(shouldShowOption ? styles.title : styles.singleTitle)}>
{oldLang('Chat.DeleteMessagesConfirmation', messageIds?.length)}
</h3>
</div>
);
}
function renderAdditionalActionOptions() {
return (
<div className={styles.options}>
<CheckboxGroup
options={ACTION_SPAM_OPTION}
onChange={setPeerIdsToReportSpam}
selected={peerIdsToReportSpam}
nestedCheckbox={messageIds && peerList.length >= 2}
/>
{peerListToDeleteAll?.length > 0 && (
<CheckboxGroup
options={ACTION_DELETE_OPTION}
onChange={setPeerIdsToDeleteAll}
selected={peerIdsToDeleteAll}
nestedCheckbox={messageIds && peerList.length >= 2}
/>
)}
{peerListToBan?.length > 0 && (
<CheckboxGroup
options={ACTION_BAN_OPTION}
onChange={setPeerIdsToBan}
selected={peerIdsToBan}
nestedCheckbox={messageIds && peerList.length >= 2}
/>
)}
</div>
);
}
function renderPartiallyRestrictedUser() {
return (
<div className={buildClassName(styles.restrictionContainer,
isAdditionalOptionsVisible && styles.restrictionContainerOpen)}
>
<h3 className={buildClassName(styles.actionTitle, styles.restrictionTitle)}>
{oldLang('UserRestrictionsCanDoUsers', peerList.length)}
</h3>
<PermissionCheckboxList
withCheckbox
chatId={chat?.id}
isMediaDropdownOpen={isMediaDropdownOpen}
setIsMediaDropdownOpen={setIsMediaDropdownOpen}
handlePermissionChange={handlePermissionChange}
permissions={permissions}
className={buildClassName(
styles.dropdownList,
isMediaDropdownOpen && styles.dropdownListOpen,
)}
/>
</div>
);
}
return (
<Modal
isOpen={isOpen}
onClose={onCloseHandler}
onEnter={handleDeleteMessageList}
className={styles.root}
>
<div className={styles.main}>
{renderHeader()}
{shouldShowOption && (
<>
<p className={styles.actionTitle}>{oldLang('DeleteAdditionalActions')}</p>
{renderAdditionalActionOptions()}
{renderPartiallyRestrictedUser()}
{peerIdsToBan?.length && canBanUsers ? (
<ListItem
narrow
buttonClassName={styles.button}
onClick={toggleAdditionalOptions}
>
{oldLang(isAdditionalOptionsVisible ? 'DeleteToggleBanUsers' : 'DeleteToggleRestrictUsers')}
<Icon
name={isAdditionalOptionsVisible ? 'up' : 'down'}
className={buildClassName(styles.button, 'ml-2')}
/>
</ListItem>
) : setIsAdditionalOptionsVisible(false)}
</>
)}
{(canDeleteForAll || chatBot || !shouldShowOption) && (
<>
<p>
{messageIds && messageIds.length > 1
? lang('AreYouSureDeleteFewMessages') : lang('AreYouSureDeleteSingleMessage')}
</p>
{willDeleteForCurrentUserOnly && (
<p>{oldLang('lng_delete_for_me_chat_hint', 1, 'i')}</p>
)}
{willDeleteForAll && (
<p>{oldLang('lng_delete_for_everyone_hint', 1, 'i')}</p>
)}
</>
)}
{canDeleteForAll && (
<Checkbox
className="dialog-checkbox"
label={contactName ? renderText(oldLang('DeleteMessagesOptionAlso', contactName))
: oldLang('Conversation.DeleteMessagesForEveryone')}
checked={shouldDeleteForAll}
onCheck={setShouldDeleteForAll}
/>
)}
<div className={buildClassName('dialog-buttons',
isMediaDropdownOpen ? styles.dialogButtons : styles.proceedButtons)}
>
<Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteMessageList}>
{shouldShowOption ? oldLang('DeleteProceedBtn') : lang('Delete')}
</Button>
<Button className="confirm-dialog-button" isText onClick={onCloseHandler}>{oldLang('Cancel')}</Button>
</div>
</div>
</Modal>
);
};
export default memo(withGlobal<OwnProps>(
(global): Complete<StateProps> => {
const {
deleteMessageModal,
} = selectTabState(global);
const messageIds = deleteMessageModal?.messageIds;
const chatId = deleteMessageModal?.chatId;
const { canDeleteForAll } = selectCanDeleteSelectedMessages(global, messageIds);
const chat = chatId ? selectChat(global, chatId) : undefined;
const chatFullInfo = chat && selectChatFullInfo(global, chat.id);
const linkedChatId = chatFullInfo?.linkedChatId;
const isChannel = Boolean(chat) && isChatChannel(chat);
const isSuperGroup = Boolean(chat) && isChatSuperGroup(chat);
const isSchedule = deleteMessageModal?.isSchedule;
const onConfirm = deleteMessageModal?.onConfirm;
const contactName = chatId && isUserId(chatId)
? getUserFirstOrLastName(selectUser(global, chatId))
: undefined;
const chatBot = Boolean(chat && !isSystemBot(chat.id) && selectBot(global, chat.id));
const adminMembersById = chatFullInfo?.adminMembersById;
const canBanUsers = chat && getHasAdminRight(chat, 'banUsers') && !chat.isMonoforum; // TODO: Ban in channel in case of monoforum
const isCreator = chat?.isCreator;
const isChatWithBot = chatId ? selectIsChatWithBot(global, chatId) : undefined;
const willDeleteForCurrentUserOnly = (chat && isChatBasicGroup(chat) && !canDeleteForAll) || isChatWithBot;
const willDeleteForAll = chat && (isChatSuperGroup(chat) || isChannel);
return {
chat,
isChannel,
isSuperGroup,
messageIds,
currentUserId: global.currentUserId,
canDeleteForAll: !isSchedule && canDeleteForAll,
contactName,
willDeleteForCurrentUserOnly,
willDeleteForAll,
adminMembersById,
chatBot,
canBanUsers,
linkedChatId,
isSchedule,
isCreator,
onConfirm,
};
},
)(DeleteMessageModal));