539 lines
18 KiB
TypeScript
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));
|