Delete Message Modal: Add linked chat moderation (#5519)

This commit is contained in:
Alexander Zinchuk 2025-02-13 14:27:54 +01:00
parent 45fc7aac7c
commit e90bd8ba9a
28 changed files with 488 additions and 886 deletions

View File

@ -840,14 +840,15 @@ export function joinChannel({
}
export function deleteChatUser({
chat, user,
chat, user, shouldRevokeHistory,
}: {
chat: ApiChat; user: ApiUser;
chat: ApiChat; user: ApiUser; shouldRevokeHistory?: boolean;
}) {
if (chat.type !== 'chatTypeBasicGroup') return undefined;
return invokeRequest(new GramJs.messages.DeleteChatUser({
chatId: buildInputEntity(chat.id, chat.accessHash) as BigInt.BigInteger,
userId: buildInputEntity(user.id, user.accessHash) as GramJs.InputUser,
revokeHistory: shouldRevokeHistory || undefined,
}), {
shouldReturnTrue: true,
});

View File

@ -832,6 +832,37 @@ export async function deleteMessages({
});
}
export async function deleteParticipantHistory({
chat, peer, isRepeat = false,
}: {
chat: ApiChat; peer: ApiPeer; isRepeat?: boolean;
}) {
const result = await invokeRequest(
new GramJs.channels.DeleteParticipantHistory({
channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel,
participant: buildInputPeer(peer.id, peer.accessHash),
}),
);
if (!result) {
return;
}
processAffectedHistory(chat, result);
if (!isRepeat) {
sendApiUpdate({
'@type': 'deleteParticipantHistory',
chatId: chat.id,
peerId: peer.id,
});
}
if (result.offset) {
await deleteParticipantHistory({ chat, peer, isRepeat: true });
}
}
export function deleteScheduledMessages({
chat, messageIds,
}: {

View File

@ -351,6 +351,12 @@ export type ApiUpdateDeleteHistory = {
chatId: string;
};
export type ApiDeleteParticipantHistory = {
'@type': 'deleteParticipantHistory';
chatId: string;
peerId: string;
};
export type ApiUpdateDeleteSavedHistory = {
'@type': 'deleteSavedHistory';
chatId: string;
@ -806,8 +812,9 @@ export type ApiUpdate = (
ApiUpdateChatListType | ApiUpdateChatFolder | ApiUpdateChatFoldersOrder | ApiUpdateRecommendedChatFolders |
ApiUpdateNewMessage | ApiUpdateMessage | ApiUpdateThreadInfo | ApiUpdateCommonBoxMessages |
ApiUpdateDeleteMessages | ApiUpdateMessagePoll | ApiUpdateMessagePollVote | ApiUpdateDeleteHistory |
ApiUpdateMessageSendSucceeded | ApiUpdateMessageSendFailed | ApiUpdateServiceNotification |
ApiDeleteContact | ApiUpdateUser | ApiUpdateUserStatus | ApiUpdateUserFullInfo | ApiUpdateVideoProcessingPending |
ApiDeleteParticipantHistory | ApiUpdateMessageSendSucceeded | ApiUpdateMessageSendFailed |
ApiUpdateServiceNotification | ApiDeleteContact | ApiUpdateUser | ApiUpdateUserStatus |
ApiUpdateUserFullInfo | ApiUpdateVideoProcessingPending |
ApiUpdateAvatar | ApiUpdateMessageImage | ApiUpdateDraftMessage |
ApiUpdateError | ApiUpdateResetContacts | ApiUpdateStartEmojiInteraction |
ApiUpdateFavoriteStickers | ApiUpdateStickerSet | ApiUpdateStickerSets | ApiUpdateStickerSetsOrder |

View File

@ -1,12 +1,18 @@
.mainContainer {
.main {
max-height: 90vh;
}
.root {
:global(.modal-dialog) {
max-width: 22.5rem;
}
}
.container {
display: flex;
align-items: center;
gap: 1rem;
margin: 0 1rem;
margin-left: -0.5rem;
}
.title {
@ -19,20 +25,13 @@
.actionTitle {
margin-top: 1.5rem;
margin-left: 1rem;
color: var(--color-links);
font-size: 1rem;
font-weight: bold;
font-weight: var(--font-weight-semibold);
}
.restrictionTitle {
margin-bottom: 1.5rem;
margin-top: 0;
}
.listItemButton {
margin-top: 0.1875rem;
margin-left: 1rem;
margin: 1rem 0 0.5rem 1rem;
}
.button {
@ -43,17 +42,25 @@
}
}
.options {
margin: 0 -1rem;
}
.dropdownList {
padding-left: 3.1875rem;
margin-block: -0.375rem;
padding: 0 1rem 0 4rem;
}
.dialogButtons {
padding-bottom: 1rem;
}
.proceedButtons {
margin-top: 0.5rem;
}
.restrictionContainer,
.dropdownList {
margin: 0 -1rem;
overflow: hidden;
max-height: 0;
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
@ -62,6 +69,7 @@
.restrictionContainerOpen,
.dropdownListOpen {
margin: 0 -1rem;
max-height: 100vh;
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
transition: max-height 0.3s ease-in-out;

View File

@ -1,37 +1,41 @@
import type { FC } from '../../lib/teact/teact';
import React, {
memo, useEffect,
memo,
useEffect,
useMemo,
useState,
} from '../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../global';
import type {
ApiChat, ApiChatMember, ApiMessage, ApiPeer,
} from '../../api/types';
import type { IAlbum } from '../../types';
import type { ApiChat, ApiChatMember } from '../../api/types';
import type { IRadioOption } from '../ui/CheckboxGroup';
import {
getHasAdminRight,
getPeerTitle,
getPrivateChatUserId,
getUserFirstOrLastName, getUserFullName,
isChatBasicGroup,
isChatSuperGroup, isOwnMessage,
getUserFirstOrLastName, isChatBasicGroup,
isChatChannel,
isChatSuperGroup,
isSystemBot,
isUserId,
} from '../../global/helpers';
import {
selectAllowedMessageActionsSlow,
getSendersFromSelectedMessages,
selectBot,
selectChat, selectChatFullInfo, selectCurrentMessageIds,
selectCurrentMessageList, selectSender, selectSenderFromMessage, selectTabState,
selectCanDeleteSelectedMessages,
selectChat,
selectChatFullInfo, selectIsChatWithBot,
selectSenderFromMessage,
selectTabState,
selectUser,
} from '../../global/selectors';
import buildClassName from '../../util/buildClassName';
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';
@ -39,10 +43,12 @@ 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';
@ -54,146 +60,199 @@ export type OwnProps = {
type StateProps = {
chat?: ApiChat;
isGroup?: boolean;
isChannel?: boolean;
isSuperGroup?: boolean;
sender: ApiPeer | undefined;
currentUserId?: string;
messageIds?: number[];
canDeleteForAll?: boolean;
contactName?: string;
currentUserId?: string;
willDeleteForCurrentUserOnly?: boolean;
willDeleteForAll?: boolean;
messageIdList: number[] | undefined;
adminMembersById?: Record<string, ApiChatMember>;
chatBot?: boolean;
isSchedule?: boolean;
message?: ApiMessage;
album?: IAlbum;
onConfirm?: NoneToVoidFunction;
isOwn?: boolean;
canBanUsers?: boolean;
isCreator?: boolean;
linkedChatId?: string;
};
const DeleteMessageModal: FC<OwnProps & StateProps> = ({
isOpen,
chat,
isGroup,
isChannel,
isSuperGroup,
sender,
currentUserId,
messageIdList,
isSchedule,
message,
album,
currentUserId,
messageIds,
isCreator,
canDeleteForAll,
contactName,
willDeleteForCurrentUserOnly,
willDeleteForAll,
onConfirm,
adminMembersById,
chatBot,
isOwn,
adminMembersById,
canBanUsers,
linkedChatId,
}) => {
const {
closeDeleteMessageModal,
deleteMessages,
deleteScheduledMessages,
reportChannelSpam,
deleteChatMember,
deleteScheduledMessages,
exitMessageSelectMode,
updateChatMemberBannedRights,
closeDeleteMessageModal,
deleteParticipantHistory,
} = getActions();
const prevIsOpen = usePreviousDeprecated(isOpen);
const lang = useOldLang();
const oldLang = useOldLang();
const lang = useLang();
const {
permissions, havePermissionChanged, handlePermissionChange, resetPermissions,
} = useManagePermissions(chat?.defaultBannedRights);
const [chosenDeleteOption, setChosenDeleteOption] = useState<string[] | undefined>(undefined);
const [chosenBanOption, setChosenBanOptions] = useState<string[] | undefined>(undefined);
const [chosenSpanOption, setChosenSpanOptions] = useState<string[] | undefined>(undefined);
const [peerIdsToDeleteAll, setPeerIdsToDeleteAll] = useState<string[] | undefined>(undefined);
const [peerIdsToBan, setPeerIdsToBan] = useState<string[] | undefined>(undefined);
const [peerIdsToReportSpam, setPeerIdsToReportSpam] = useState<string[] | undefined>(undefined);
const [isMediaDropdownOpen, setIsMediaDropdownOpen] = useState(false);
const [isAdditionalOptionsVisible, setIsAdditionalOptionsVisible] = useState(false);
const isSenderOwner = useMemo(() => {
return sender && adminMembersById && adminMembersById[sender.id] && adminMembersById[sender.id].isOwner;
}, [sender, adminMembersById]);
const [shouldDeleteForAll, setShouldDeleteForAll] = useState(true);
const user = useMemo(() => {
const usersById = getGlobal().users.byId;
if (!sender || isSchedule) return undefined;
return usersById[sender.id];
}, [isSchedule, sender]);
const shouldShowAdditionalOptions = useMemo(() => {
return user && user.id !== currentUserId;
}, [user, currentUserId]);
const shouldShowOptions = shouldShowAdditionalOptions && !canDeleteForAll && !isSchedule && (isGroup || isSuperGroup);
const userName = useMemo(() => {
const usersById = getGlobal().users.byId;
if (!sender || isSchedule) return '';
return getUserFullName(usersById[sender.id]);
}, [isSchedule, sender]);
const ACTION_SPAM_OPTION: IRadioOption[] = useMemo(() => {
if (!user) {
return [];
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) : MEMO_EMPTY_ARRAY;
}, [chat, isChannel, messageIds]);
return [
{
value: user.id,
label: lang('ReportSpamTitle'),
},
];
}, [lang, user]);
const ACTION_DELETE_OPTION: IRadioOption[] = useMemo(() => {
if (!user) {
return [];
}
return [
{
value: user.id,
label: lang('DeleteAllFrom', userName),
},
];
}, [lang, user, userName]);
const ACTION_BAN_OPTION: IRadioOption[] = useMemo(() => {
if (!user) {
return [];
}
return [
{
value: user.id,
label: (message && isAdditionalOptionsVisible ? lang('KickFromSupergroup') : lang('DeleteBan', userName)),
},
];
}, [isAdditionalOptionsVisible, lang, message, user, userName]);
const toggleAdditionalOptions = useLastCallback(() => {
setIsAdditionalOptionsVisible(!isAdditionalOptionsVisible);
const buildNestedOptionListWithAvatars = useLastCallback(() => {
return peerList.map((member) => {
return {
value: `${member.id}`,
label: getPeerTitle(lang, member) || '',
leftElement: <Avatar size="small" peer={member} />,
};
});
});
const filterMessageIdByUserId = useLastCallback((userIds: string[], selectedMessageIdList: number[]) => {
const peerListToDeleteAll = useMemo(() => {
return peerList.filter((peer) => peer.id !== linkedChatId && peer.id !== currentUserId);
}, [peerList, currentUserId, linkedChatId]);
const peerListToReportSpam = useMemo(() => {
return peerList.filter((peer) => peer.id !== currentUserId && peer.id !== linkedChatId);
}, [peerList, currentUserId, linkedChatId]);
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 !== currentUserId),
] : undefined,
},
];
}, [messageIds, peerList, oldLang, linkedChatId, 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 !== currentUserId),
] : undefined,
},
];
}, [messageIds, peerList, oldLang, peerNames, linkedChatId, 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 senderPeer = selectSenderFromMessage(getGlobal(), chat.id, msgId);
return senderPeer && userIds.includes(senderPeer.id);
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[]) => {
if (filteredMessageIdList?.length) {
deleteMessages({ messageIds: filteredMessageIdList, shouldDeleteForAll: true });
}
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[]) => {
@ -212,86 +271,68 @@ const DeleteMessageModal: FC<OwnProps & StateProps> = ({
});
});
const handleDeleteMessageForAll = useLastCallback(() => {
onConfirm?.();
const messageIds = album?.messages
? album.messages.map(({ id }) => id)
: [message!.id];
deleteMessages({ messageIds, shouldDeleteForAll: true });
closeDeleteMessageModal();
});
const handleDeleteMessageForSelf = useLastCallback(() => {
if (!chat) return;
const handleDeleteMessageList = useLastCallback(() => {
if (!chat || !messageIds) return;
onConfirm?.();
const messageIds = album?.messages
? album.messages.map(({ id }) => id)
: [message!.id];
if (isSchedule) {
deleteScheduledMessages({ messageIds });
} else if (!isOwn && (chosenSpanOption || chosenDeleteOption || chosenBanOption) && (isGroup || isSuperGroup)) {
if (chosenSpanOption && sender) {
const filteredMessageIdList = filterMessageIdByUserId(chosenSpanOption, messageIdList!);
if (filteredMessageIdList && filteredMessageIdList.length) {
reportChannelSpam({ participantId: sender.id, chatId: chat.id, messageIds: filteredMessageIdList });
}
} else if (shouldShowOption) {
if (peerIdsToReportSpam) {
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 (chosenDeleteOption) {
const filteredMessageIdList = filterMessageIdByUserId(chosenDeleteOption, messageIdList!);
if (peerIdsToDeleteAll) {
const peerIdList = peerIdsToDeleteAll.filter((option) => !Number.isNaN(Number(option)));
handleDeleteAllPeerMessages(peerIdList);
}
if (peerIdsToBan && !havePermissionChanged) {
const peerIdList = peerIdsToBan.filter((option) => !Number.isNaN(Number(option)));
handleDeleteMember(peerIdList);
const filteredMessageIdList = filterMessageIdByPeerId(peerIdList, messageIds!);
handleDeleteMessages(filteredMessageIdList);
}
if (chosenBanOption && !havePermissionChanged && message) {
const filteredUserIdList = chosenBanOption.filter((userId) => messageIds?.some((msgId) => {
const senderPeer = selectSenderFromMessage(getGlobal(), chat.id, msgId);
return senderPeer && senderPeer.id === userId;
}));
handleDeleteMember(filteredUserIdList);
deleteMessages({
messageIds: [message.id],
shouldDeleteForAll: false,
});
if (peerIdsToBan && havePermissionChanged) {
const peerIdList = peerIdsToBan.filter((option) => !Number.isNaN(Number(option)));
handleUpdateChatMemberBannedRights(peerIdList);
}
if (chosenBanOption && havePermissionChanged) {
const filteredUserIdList = chosenBanOption.filter((userId) => messageIds?.some((msgId) => {
const senderPeer = selectSenderFromMessage(getGlobal(), chat.id, msgId);
return senderPeer && senderPeer.id === userId;
}));
handleUpdateChatMemberBannedRights(filteredUserIdList);
if (!peerIdsToReportSpam || !peerIdsToDeleteAll || !peerIdsToBan) {
deleteMessages({ messageIds, shouldDeleteForAll });
}
} else {
deleteMessages({
messageIds,
shouldDeleteForAll: false,
});
deleteMessages({ messageIds, shouldDeleteForAll });
}
closeDeleteMessageModal();
exitMessageSelectMode();
});
const handleDeleteOptionChange = useLastCallback((options: string[]) => {
setChosenDeleteOption(options);
});
const handleBanOptionChange = useLastCallback((options: string[]) => {
setChosenBanOptions(options);
});
const handleSpanOptionChange = useLastCallback((options: string[]) => {
setChosenSpanOptions(options);
});
const handleClose = useLastCallback(() => {
const onCloseHandler = useLastCallback(() => {
closeDeleteMessageModal();
});
useEffect(() => {
if (!isOpen && prevIsOpen) {
setChosenSpanOptions(undefined);
setChosenDeleteOption(undefined);
setChosenBanOptions(undefined);
setPeerIdsToReportSpam(undefined);
setPeerIdsToDeleteAll(undefined);
setPeerIdsToBan(undefined);
setShouldDeleteForAll(true);
setIsMediaDropdownOpen(false);
setIsAdditionalOptionsVisible(false);
resetPermissions();
@ -300,14 +341,19 @@ const DeleteMessageModal: FC<OwnProps & StateProps> = ({
function renderHeader() {
return (
<div className={shouldShowOptions && styles.container} dir={lang.isRtl ? 'rtl' : undefined}>
{shouldShowOptions && (
<Avatar
<div
className={shouldShowOption && styles.container}
dir={oldLang.isRtl ? 'rtl' : undefined}
>
{shouldShowOption && (
<AvatarList
size="small"
peer={user!}
peers={peerList}
/>
)}
<h3 className={shouldShowOptions ? styles.title : styles.singleTitle}>{lang('DeleteSingleMessagesTitle')}</h3>
<h3 className={buildClassName(shouldShowOption ? styles.title : styles.singleTitle)}>
{oldLang('Chat.DeleteMessagesConfirmation', messageIds?.length)}
</h3>
</div>
);
}
@ -317,19 +363,24 @@ const DeleteMessageModal: FC<OwnProps & StateProps> = ({
<div className={styles.options}>
<CheckboxGroup
options={ACTION_SPAM_OPTION}
onChange={handleSpanOptionChange}
selected={chosenSpanOption}
onChange={setPeerIdsToReportSpam}
selected={peerIdsToReportSpam}
nestedCheckbox={messageIds && peerList.length >= 2}
/>
<CheckboxGroup
options={ACTION_DELETE_OPTION}
onChange={handleDeleteOptionChange}
selected={chosenDeleteOption}
/>
{!isSenderOwner && canBanUsers && (
{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={handleBanOptionChange}
selected={chosenBanOption}
onChange={setPeerIdsToBan}
selected={peerIdsToBan}
nestedCheckbox={messageIds && peerList.length >= 2}
/>
)}
</div>
@ -342,11 +393,10 @@ const DeleteMessageModal: FC<OwnProps & StateProps> = ({
isAdditionalOptionsVisible && styles.restrictionContainerOpen)}
>
<h3 className={buildClassName(styles.actionTitle, styles.restrictionTitle)}>
{lang('UserRestrictionsCanDoUsers', 1)}
{oldLang('UserRestrictionsCanDoUsers', peerList.length)}
</h3>
<PermissionCheckboxList
withCheckbox
permissionGroup
chatId={chat?.id}
isMediaDropdownOpen={isMediaDropdownOpen}
setIsMediaDropdownOpen={setIsMediaDropdownOpen}
@ -361,29 +411,32 @@ const DeleteMessageModal: FC<OwnProps & StateProps> = ({
);
}
if (!messageIds) {
return undefined;
}
return (
<Modal
isOpen={isOpen}
onClose={handleClose}
onEnter={canDeleteForAll ? undefined : handleDeleteMessageForSelf}
className="delete"
onClose={onCloseHandler}
onEnter={canDeleteForAll ? undefined : handleDeleteMessageList}
className={styles.root}
>
<div className={buildClassName(styles.mainContainer, 'custom-scroll')}>
<div className={styles.main}>
{renderHeader()}
{shouldShowOptions && (
{shouldShowOption && (
<>
<p className={styles.actionTitle}>{lang('DeleteAdditionalActions')}</p>
<p className={styles.actionTitle}>{oldLang('DeleteAdditionalActions')}</p>
{renderAdditionalActionOptions()}
{renderPartiallyRestrictedUser()}
{
chosenBanOption && canBanUsers && chosenBanOption?.length ? (
peerIdsToBan && canBanUsers ? (
<ListItem
narrow
className={styles.listItemButton}
buttonClassName={styles.button}
onClick={toggleAdditionalOptions}
>
{lang(isAdditionalOptionsVisible ? 'DeleteToggleBanUsers' : 'DeleteToggleRestrictUsers')}
{oldLang(isAdditionalOptionsVisible ? 'DeleteToggleBanUsers' : 'DeleteToggleRestrictUsers')}
<Icon
name={isAdditionalOptionsVisible ? 'up' : 'down'}
className={buildClassName(styles.button, 'ml-2')}
@ -393,35 +446,35 @@ const DeleteMessageModal: FC<OwnProps & StateProps> = ({
}
</>
)}
{(chatBot || !shouldShowAdditionalOptions) && (
{(canDeleteForAll || chatBot || !shouldShowOption) && (
<>
<p>{lang('AreYouSureDeleteSingleMessage')}</p>
<p>{messageIds.length > 1
? lang('AreYouSureDeleteFewMessages') : lang('AreYouSureDeleteSingleMessage')}
</p>
{willDeleteForCurrentUserOnly && (
<p>{lang('lng_delete_for_me_chat_hint', 1, 'i')}</p>
<p>{oldLang('lng_delete_for_me_chat_hint', 1, 'i')}</p>
)}
{willDeleteForAll && (
<p>{lang('lng_delete_for_everyone_hint', 1, 'i')}</p>
<p>{oldLang('lng_delete_for_everyone_hint', 1, 'i')}</p>
)}
</>
)}
<div className={canDeleteForAll ? 'dialog-buttons-column'
: buildClassName('dialog-buttons', isAdditionalOptionsVisible && styles.dialogButtons)}
{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)}
>
{canDeleteForAll && (
<Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteMessageForAll}>
{contactName && renderText(lang('Conversation.DeleteMessagesFor', contactName))}
{!contactName && lang('Conversation.DeleteMessagesForEveryone')}
</Button>
)}
<Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteMessageForSelf}>
{lang(canDeleteForAll ? 'ChatList.DeleteForCurrentUser' : 'Delete')}
</Button>
<Button
className="confirm-dialog-button"
isText
onClick={handleClose}
>{lang('Cancel')}
<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>
@ -433,48 +486,44 @@ export default memo(withGlobal<OwnProps>(
const {
deleteMessageModal,
} = selectTabState(global);
const chatId = deleteMessageModal && deleteMessageModal.message?.chatId;
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 { threadId, type } = selectCurrentMessageList(global) || {};
const { canDeleteForAll } = (deleteMessageModal && deleteMessageModal.message && threadId
&& selectAllowedMessageActionsSlow(global, deleteMessageModal.message, threadId)) || {};
const adminMembersById = chatFullInfo && chatFullInfo?.adminMembersById;
const messageIdList = chat && selectCurrentMessageIds(global, chat.id, threadId!, type!);
const isGroup = Boolean(chat) && isChatBasicGroup(chat);
const linkedChatId = chatFullInfo?.linkedChatId;
const isChannel = Boolean(chat) && isChatChannel(chat);
const isSuperGroup = Boolean(chat) && isChatSuperGroup(chat);
const sender = deleteMessageModal?.message && selectSender(global, deleteMessageModal.message);
const isSchedule = deleteMessageModal?.isSchedule;
const onConfirm = deleteMessageModal?.onConfirm;
const contactName = chat && isUserId(chat.id)
? getUserFirstOrLastName(selectUser(global, getPrivateChatUserId(chat)!))
: undefined;
const isChatWithBot = Boolean(deleteMessageModal && deleteMessageModal.message
&& selectBot(global, deleteMessageModal.message.chatId));
const chatBot = Boolean(chat && !isSystemBot(chat.id) && selectBot(global, chat.id));
const canBanUsers = chat && (chat.isCreator || getHasAdminRight(chat, 'banUsers'));
const isOwn = deleteMessageModal && deleteMessageModal.message && isOwnMessage(deleteMessageModal.message);
const adminMembersById = chatFullInfo?.adminMembersById;
const canBanUsers = chat && getHasAdminRight(chat, 'banUsers');
const isCreator = chat?.isCreator;
const isChatWithBot = chat ? selectIsChatWithBot(global, chat) : undefined;
const willDeleteForCurrentUserOnly = (chat && isChatBasicGroup(chat) && !canDeleteForAll) || isChatWithBot;
const willDeleteForAll = chat && isChatSuperGroup(chat);
const willDeleteForAll = chat && (isChatSuperGroup(chat) || isChannel);
return {
chat,
isGroup,
isChannel,
isSuperGroup,
messageIds,
currentUserId: global.currentUserId,
sender,
messageIdList,
canDeleteForAll: deleteMessageModal && !deleteMessageModal.isSchedule && canDeleteForAll,
canDeleteForAll: !isSchedule && canDeleteForAll,
contactName,
willDeleteForCurrentUserOnly,
willDeleteForAll,
adminMembersById,
chatBot,
isSchedule: deleteMessageModal && deleteMessageModal.isSchedule,
message: deleteMessageModal && deleteMessageModal.message,
album: deleteMessageModal && deleteMessageModal.album,
onConfirm: deleteMessageModal && deleteMessageModal.onConfirm,
isOwn,
canBanUsers,
linkedChatId,
isSchedule,
isCreator,
onConfirm,
};
},
)(DeleteMessageModal));

View File

@ -2,6 +2,7 @@ import type { FC } from '../../lib/teact/teact';
import React, { memo, useMemo } from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global';
import type { ApiChat } from '../../api/types';
import type { ActiveDownloads, MediaViewerOrigin, MessageListType } from '../../types';
import type { IconName } from '../../types/icons';
import type { MenuItemProps } from '../ui/MenuItem';
@ -16,7 +17,7 @@ import {
} from '../../global/helpers';
import {
selectActiveDownloads,
selectAllowedMessageActionsSlow,
selectAllowedMessageActionsSlow, selectCurrentChat,
selectCurrentMessageList,
selectIsChatProtected,
selectIsMessageProtected,
@ -45,6 +46,7 @@ type StateProps = {
isProtected?: boolean;
isChatProtected?: boolean;
canDelete?: boolean;
chat?: ApiChat;
canUpdate?: boolean;
messageListType?: MessageListType;
origin?: MediaViewerOrigin;
@ -67,6 +69,7 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
item,
mediaData,
isVideo,
chat,
isChatProtected,
isProtected,
canReportAvatar,
@ -211,10 +214,11 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
}
const openDeleteModalHandler = useLastCallback(() => {
if (item?.type === 'message') {
if (item?.type === 'message' && chat) {
openDeleteMessageModal({
chatId: chat?.id,
messageIds: [item.message.id],
isSchedule: messageListType === 'scheduled',
message: item.message,
onConfirm: onBeforeDelete,
});
} else {
@ -394,6 +398,7 @@ export default memo(withGlobal<OwnProps>(
const avatarOwner = item?.type === 'avatar' ? item.avatarOwner : undefined;
const avatarPhoto = item?.type === 'avatar' && item.profilePhotos.photos[item.mediaIndex];
const chat = selectCurrentChat(global);
const currentMessageList = selectCurrentMessageList(global);
const { threadId } = selectCurrentMessageList(global) || {};
const isProtected = selectIsMessageProtected(global, message);
@ -410,6 +415,7 @@ export default memo(withGlobal<OwnProps>(
return {
activeDownloads,
isProtected,
chat,
isChatProtected,
canDelete,
canUpdate,

View File

@ -1,68 +0,0 @@
.main {
max-height: 90vh;
}
.container {
display: flex;
align-items: center;
gap: 1rem;
margin: 0 1rem;
}
.title {
margin: 0;
}
.singleTitle {
margin-bottom: 1rem;
}
.actionTitle {
margin-top: 1.5rem;
margin-left: 1rem;
color: var(--color-links);
font-size: 1rem;
font-weight: var(--font-weight-medium);
}
.restrictionTitle {
margin-bottom: 1.5rem;
margin-top: 0;
}
.listItemButton {
margin-top: 0.1875rem;
margin-left: 1rem;
}
.button {
color: var(--color-links) !important;
padding: 0 !important;
&:hover {
background: none !important;
}
}
.dropdownList {
padding-left: 2.6875rem;
margin-block: -0.375rem;
}
.dialogButtons {
padding-bottom: 1rem;
}
.restrictionContainer,
.dropdownList {
overflow: hidden;
max-height: 0;
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
transition: max-height 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
}
.restrictionContainerOpen,
.dropdownListOpen {
max-height: 100vh;
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
transition: max-height 0.6s ease-in-out;
}

View File

@ -1,508 +0,0 @@
import type { FC } from '../../lib/teact/teact';
import React, {
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,
getPrivateChatUserId,
getUserFirstOrLastName,
getUserFullName,
isChatBasicGroup, isChatChannel,
isChatSuperGroup,
isUserId,
} from '../../global/helpers';
import {
getSendersFromSelectedMessages,
selectCanDeleteSelectedMessages,
selectChatFullInfo,
selectCurrentChat,
selectCurrentMessageIds,
selectCurrentMessageList,
selectSenderFromMessage,
selectTabState,
selectUser,
} from '../../global/selectors';
import buildClassName from '../../util/buildClassName';
import { buildCollectionByCallback } from '../../util/iteratees';
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
import renderText from '../common/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 Avatar from '../common/Avatar';
import AvatarList from '../common/AvatarList';
import Icon from '../common/icons/Icon';
import PermissionCheckboxList from '../main/PermissionCheckboxList';
import Button from '../ui/Button';
import CheckboxGroup from '../ui/CheckboxGroup';
import ListItem from '../ui/ListItem';
import Modal from '../ui/Modal';
import styles from './DeleteSelectedMessageModal.module.scss';
export type OwnProps = {
isOpen: boolean;
isSchedule: boolean;
onClose: () => void;
};
type StateProps = {
chat?: ApiChat;
isGroup?: boolean;
isChannel?: boolean;
isSuperGroup?: boolean;
selectedMessageIds?: number[];
canDeleteForAll?: boolean;
contactName?: string;
currentUserId?: string;
willDeleteForCurrentUserOnly?: boolean;
willDeleteForAll?: boolean;
messageIds: number[] | undefined;
adminMembersById?: Record<string, ApiChatMember>;
canBanUsers?: boolean;
};
const DeleteSelectedMessageModal: FC<OwnProps & StateProps> = ({
chat,
isChannel,
isGroup,
isSuperGroup,
isOpen,
isSchedule,
currentUserId,
selectedMessageIds,
canDeleteForAll,
contactName,
willDeleteForCurrentUserOnly,
willDeleteForAll,
messageIds,
onClose,
adminMembersById,
canBanUsers,
}) => {
const {
deleteMessages,
reportChannelSpam,
deleteChatMember,
deleteScheduledMessages,
exitMessageSelectMode,
updateChatMemberBannedRights,
} = getActions();
const prevIsOpen = usePreviousDeprecated(isOpen);
const oldLang = useOldLang();
const lang = useLang();
const {
permissions, havePermissionChanged, handlePermissionChange, resetPermissions,
} = useManagePermissions(chat?.defaultBannedRights);
const [chosenDeleteOption, setChosenDeleteOption] = useState<string[] | undefined>(undefined);
const [chosenBanOption, setChosenBanOptions] = useState<string[] | undefined>(undefined);
const [chosenSpanOption, setChosenSpanOptions] = useState<string[] | undefined>(undefined);
const [isMediaDropdownOpen, setIsMediaDropdownOpen] = useState(false);
const [isAdditionalOptionsVisible, setIsAdditionalOptionsVisible] = useState(false);
const senderList = useMemo(() => {
if (isChannel) {
return undefined;
}
const senderArray = getSendersFromSelectedMessages(getGlobal(), chat);
return senderArray?.filter(Boolean);
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
}, [chat, isChannel, isOpen]);
const isSenderOwner = useMemo(() => {
if (!senderList) {
return undefined;
}
return senderList.some((sender) => sender
&& adminMembersById
&& adminMembersById[sender.id] && adminMembersById[sender.id].isOwner);
}, [senderList, adminMembersById]);
const userList = useMemo(() => {
const usersById = getGlobal().users.byId;
if (!senderList || isSchedule) return [];
const uniqueUserIds = new Set(senderList.map((user) => user!.id));
return Array.from(uniqueUserIds)
.map((id) => usersById[id])
.filter(Boolean);
}, [isSchedule, senderList]);
const nestedOptionsWithAvatarList = useLastCallback(() => {
return userList.map((user) => ({
value: `${user.id}`,
label: getUserFullName(user) || '',
leftElement: <Avatar size="small" peer={user} />,
}));
});
const showAdditionalOptions = useMemo(() => {
return !userList.some((user) => user.id === currentUserId);
}, [userList, currentUserId]);
const shouldShowOptions = showAdditionalOptions && !canDeleteForAll && !isSchedule && (isGroup || isSuperGroup);
const userNames = useMemo(() => {
const usersById = getGlobal().users.byId;
if (!senderList || isSchedule) return {};
const uniqueUserIds = new Set(senderList.map((user) => user!.id));
const userIds = Array.from(uniqueUserIds).map((userId) => usersById[userId]);
return buildCollectionByCallback(userIds, (user) => [user?.id, getUserFullName(user)]);
}, [isSchedule, senderList]);
const ACTION_SPAM_OPTION: IRadioOption[] = useMemo(() => {
return [
{
value: selectedMessageIds && userList.length >= 2 ? 'spam' : userList?.[0]?.id,
label: oldLang('ReportSpamTitle'),
nestedOptions: selectedMessageIds && userList.length >= 2 ? [
...nestedOptionsWithAvatarList(),
] : undefined,
},
];
}, [oldLang, selectedMessageIds, userList]);
const ACTION_DELETE_OPTION: IRadioOption[] = useMemo(() => {
return [
{
value: selectedMessageIds && userList.length >= 2 ? 'delete_all' : userList?.[0]?.id,
label: selectedMessageIds && userList.length >= 2
? oldLang('DeleteAllFromUsers')
: oldLang('DeleteAllFrom', Object.values(userNames)[0]),
nestedOptions: selectedMessageIds && userList.length >= 2 ? [
...nestedOptionsWithAvatarList(),
] : undefined,
},
];
}, [oldLang, selectedMessageIds, userList, userNames]);
const ACTION_BAN_OPTION: IRadioOption[] = useMemo(() => {
return [
{
value: selectedMessageIds && userList.length >= 2 ? 'ban' : userList?.[0]?.id,
label: selectedMessageIds && userList.length >= 2
? (isAdditionalOptionsVisible ? oldLang('DeleteRestrictUsers') : oldLang('DeleteBanUsers'))
: (isAdditionalOptionsVisible ? oldLang('KickFromSupergroup')
: oldLang('DeleteBan', Object.values(userNames)[0])),
nestedOptions: selectedMessageIds && userList.length >= 2 ? [
...nestedOptionsWithAvatarList(),
] : undefined,
},
];
}, [isAdditionalOptionsVisible, oldLang, selectedMessageIds, userList, userNames]);
const toggleAdditionalOptions = useLastCallback(() => {
setIsAdditionalOptionsVisible((prev) => !prev);
});
const handleDeleteMessageForAll = useLastCallback(() => {
onClose();
deleteMessages({ messageIds: selectedMessageIds!, shouldDeleteForAll: true });
});
const filterMessageIdByUserId = useLastCallback((userIds: string[], selectedMessageIdList: number[]) => {
if (!chat) return MEMO_EMPTY_ARRAY;
return selectedMessageIdList.filter((msgId) => {
const sender = selectSenderFromMessage(getGlobal(), chat.id, msgId);
return sender && userIds.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[]) => {
if (filteredMessageIdList && filteredMessageIdList.length) {
deleteMessages({ messageIds: filteredMessageIdList, shouldDeleteForAll: true });
}
});
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 handleDeleteMessageForSelf = useLastCallback(() => {
if (!chat) return;
if (isSchedule) {
deleteScheduledMessages({ messageIds: selectedMessageIds! });
} else if (!isSenderOwner && shouldShowOptions) {
if (chosenSpanOption) {
const userIdList = chosenSpanOption.filter((option) => !Number.isNaN(Number(option)));
const userMessagesMap = selectedMessageIds!.reduce<Record<string, number[]>>((acc, msgId) => {
const sender = selectSenderFromMessage(getGlobal(), chat.id, msgId);
if (sender && userIdList.includes(sender.id)) {
if (!acc[sender.id]) {
acc[sender.id] = [];
}
acc[sender.id].push(Number(msgId));
}
return acc;
}, {});
handleReportSpam(userMessagesMap);
}
if (chosenDeleteOption) {
const userIdList = chosenDeleteOption.filter((option) => !Number.isNaN(Number(option)));
const filteredMessageIdList = filterMessageIdByUserId(userIdList, messageIds!);
handleDeleteMessages(filteredMessageIdList);
}
if (chosenBanOption && !havePermissionChanged) {
const userIdList = chosenBanOption.filter((option) => !Number.isNaN(Number(option)));
const filteredUserIdList = userIdList.filter((userId) => selectedMessageIds?.some((msgId) => {
const sender = selectSenderFromMessage(getGlobal(), chat.id, msgId);
return sender && sender.id === userId;
}));
handleDeleteMember(filteredUserIdList);
const filteredMessageIdList = filterMessageIdByUserId(userIdList, selectedMessageIds!);
handleDeleteMessages(filteredMessageIdList);
}
if (chosenBanOption && havePermissionChanged) {
const userIdList = chosenBanOption.filter((option) => !Number.isNaN(Number(option)));
const filteredUserIdList = userIdList.filter((userId) => selectedMessageIds?.some((msgId) => {
const sender = selectSenderFromMessage(getGlobal(), chat.id, msgId);
return sender && sender.id === userId;
}));
handleUpdateChatMemberBannedRights(filteredUserIdList);
}
} else {
deleteMessages({ messageIds: selectedMessageIds!, shouldDeleteForAll: false });
}
onClose();
});
const onCloseHandler = useLastCallback(() => {
onClose();
});
const handleDeleteOptionChange = useLastCallback((options: string[]) => {
setChosenDeleteOption(options);
});
const handleBanOptionChange = useLastCallback((options: string[]) => {
setChosenBanOptions(options);
});
const handleSpanOptionChange = useLastCallback((options: string[]) => {
setChosenSpanOptions(options);
});
useEffect(() => {
if (!isOpen && prevIsOpen) {
exitMessageSelectMode();
setChosenSpanOptions(undefined);
setChosenDeleteOption(undefined);
setChosenBanOptions(undefined);
setIsMediaDropdownOpen(false);
setIsAdditionalOptionsVisible(false);
resetPermissions();
}
}, [exitMessageSelectMode, isOpen, prevIsOpen, resetPermissions]);
function renderHeader() {
return (
<div
className={shouldShowOptions && styles.container}
dir={oldLang.isRtl ? 'rtl' : undefined}
>
{shouldShowOptions && (
<AvatarList
size="small"
peers={userList}
/>
)}
<h3 className={buildClassName(shouldShowOptions ? styles.title : styles.singleTitle)}>
{oldLang('Chat.DeleteMessagesConfirmation', selectedMessageIds?.length)}
</h3>
</div>
);
}
function renderAdditionalActionOptions() {
return (
<div className={styles.options}>
<CheckboxGroup
options={ACTION_SPAM_OPTION}
onChange={handleSpanOptionChange}
selected={chosenSpanOption}
nestedCheckbox={selectedMessageIds && userList.length >= 2}
/>
<CheckboxGroup
options={ACTION_DELETE_OPTION}
onChange={handleDeleteOptionChange}
selected={chosenDeleteOption}
nestedCheckbox={selectedMessageIds && userList.length >= 2}
/>
{!isSenderOwner && canBanUsers && (
<CheckboxGroup
options={ACTION_BAN_OPTION}
onChange={handleBanOptionChange}
selected={chosenBanOption}
nestedCheckbox={selectedMessageIds && userList.length >= 2}
/>
)}
</div>
);
}
function renderPartiallyRestrictedUser() {
return (
<div className={buildClassName(styles.restrictionContainer,
isAdditionalOptionsVisible && styles.restrictionContainerOpen)}
>
<h3 className={buildClassName(styles.actionTitle, styles.restrictionTitle)}>
{oldLang('UserRestrictionsCanDoUsers', userList.length)}
</h3>
<PermissionCheckboxList
withCheckbox
permissionGroup
chatId={chat?.id}
isMediaDropdownOpen={isMediaDropdownOpen}
setIsMediaDropdownOpen={setIsMediaDropdownOpen}
handlePermissionChange={handlePermissionChange}
permissions={permissions}
className={buildClassName(styles.dropdownList, isMediaDropdownOpen && styles.dropdownListOpen)}
/>
</div>
);
}
if (!selectedMessageIds) {
return undefined;
}
return (
<Modal
isOpen={isOpen}
onClose={onClose}
onEnter={canDeleteForAll ? undefined : handleDeleteMessageForSelf}
className="delete"
>
<div className={styles.main}>
{renderHeader()}
{!showAdditionalOptions && <p>{lang('AreYouSureDeleteFewMessages')}</p>}
{shouldShowOptions && (
<>
<p className={styles.actionTitle}>{oldLang('DeleteAdditionalActions')}</p>
{renderAdditionalActionOptions()}
{renderPartiallyRestrictedUser()}
{
chosenBanOption && canBanUsers && chosenBanOption?.length ? (
<ListItem
narrow
className={styles.listItemButton}
buttonClassName={styles.button}
onClick={toggleAdditionalOptions}
>
{oldLang(isAdditionalOptionsVisible ? 'DeleteToggleBanUsers' : 'DeleteToggleRestrictUsers')}
<Icon
name={isAdditionalOptionsVisible ? 'up' : 'down'}
className={buildClassName(styles.button, 'ml-2')}
/>
</ListItem>
) : setIsAdditionalOptionsVisible(false)
}
</>
)}
{willDeleteForCurrentUserOnly && lang('DeleteForMeDescription')}
{(willDeleteForAll && !showAdditionalOptions) && lang('DeleteForEveryoneDescription')}
<div className={canDeleteForAll ? 'dialog-buttons-column'
: buildClassName('dialog-buttons', isAdditionalOptionsVisible && styles.dialogButtons)}
>
{canDeleteForAll && (
<Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteMessageForAll}>
{contactName
? renderText(oldLang('ChatList.DeleteForEveryone', contactName))
: oldLang('Conversation.DeleteMessagesForEveryone')}
</Button>
)}
<Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteMessageForSelf}>
{oldLang(canDeleteForAll ? 'ChatList.DeleteForCurrentUser' : 'Delete')}
</Button>
<Button className="confirm-dialog-button" isText onClick={onCloseHandler}>{oldLang('Cancel')}</Button>
</div>
</div>
</Modal>
);
};
export default memo(withGlobal<OwnProps>(
(global, { isSchedule }): StateProps => {
const { messageIds: selectedMessageIds } = selectTabState(global).selectedMessages || {};
const { canDeleteForAll } = selectCanDeleteSelectedMessages(global);
const chat = selectCurrentChat(global);
const chatFullInfo = chat && selectChatFullInfo(global, chat.id);
const { threadId, type } = selectCurrentMessageList(global) || {};
const messageIds = chat && selectCurrentMessageIds(global, chat.id, threadId!, type!);
const isChannel = Boolean(chat) && isChatChannel(chat);
const isGroup = Boolean(chat) && isChatBasicGroup(chat);
const isSuperGroup = Boolean(chat) && isChatSuperGroup(chat);
const contactName = chat && isUserId(chat.id)
? getUserFirstOrLastName(selectUser(global, getPrivateChatUserId(chat)!))
: undefined;
const adminMembersById = chatFullInfo?.adminMembersById;
const canBanUsers = chat && (chat.isCreator || getHasAdminRight(chat, 'banUsers'));
const willDeleteForCurrentUserOnly = chat && isChatBasicGroup(chat) && !canDeleteForAll;
const willDeleteForAll = chat && isChatSuperGroup(chat);
return {
chat,
isGroup,
isChannel,
isSuperGroup,
selectedMessageIds,
currentUserId: global.currentUserId,
canDeleteForAll: !isSchedule && canDeleteForAll,
contactName,
willDeleteForCurrentUserOnly,
willDeleteForAll,
messageIds,
adminMembersById,
canBanUsers,
};
},
)(DeleteSelectedMessageModal));

View File

@ -18,7 +18,6 @@ import {
import buildClassName from '../../util/buildClassName';
import captureKeyboardListeners from '../../util/captureKeyboardListeners';
import useFlag from '../../hooks/useFlag';
import useLastCallback from '../../hooks/useLastCallback';
import useOldLang from '../../hooks/useOldLang';
import usePreviousDeprecated from '../../hooks/usePreviousDeprecated';
@ -26,7 +25,6 @@ import useCopySelectedMessages from './hooks/useCopySelectedMessages';
import Icon from '../common/icons/Icon';
import Button from '../ui/Button';
import DeleteSelectedMessageModal from './DeleteSelectedMessageModal';
import './MessageSelectToolbar.scss';
@ -71,27 +69,35 @@ const MessageSelectToolbar: FC<OwnProps & StateProps> = ({
copySelectedMessages,
showNotification,
reportMessages,
openDeleteMessageModal,
} = getActions();
const lang = useOldLang();
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag();
useCopySelectedMessages(isActive);
const handleExitMessageSelectMode = useLastCallback(() => {
exitMessageSelectMode();
});
const handleDelete = useLastCallback(() => {
if (!selectedMessageIds || !chat) return;
openDeleteMessageModal({
chatId: chat.id,
messageIds: selectedMessageIds,
isSchedule,
});
});
useEffect(() => {
return isActive && !isDeleteModalOpen && !isAnyModalOpen
return isActive && !isAnyModalOpen
? captureKeyboardListeners({
onBackspace: canDeleteMessages ? openDeleteModal : undefined,
onDelete: canDeleteMessages ? openDeleteModal : undefined,
onBackspace: canDeleteMessages ? handleDelete : undefined,
onDelete: canDeleteMessages ? handleDelete : undefined,
onEsc: handleExitMessageSelectMode,
})
: undefined;
}, [
isActive, isDeleteModalOpen, openDeleteModal, handleExitMessageSelectMode, isAnyModalOpen,
isActive, handleDelete, handleExitMessageSelectMode, isAnyModalOpen,
canDeleteMessages,
]);
@ -181,18 +187,11 @@ const MessageSelectToolbar: FC<OwnProps & StateProps> = ({
renderButton('copy', lang('lng_context_copy_selected_items'), handleCopy)
)}
{canDeleteMessages && (
renderButton('delete', lang('EditAdminGroupDeleteMessages'), openDeleteModal, true)
renderButton('delete', lang('EditAdminGroupDeleteMessages'), handleDelete, true)
)}
</div>
)}
</div>
{canDeleteMessages && (
<DeleteSelectedMessageModal
isOpen={isDeleteModalOpen}
isSchedule={isSchedule}
onClose={closeDeleteModal}
/>
)}
</div>
);
};
@ -211,7 +210,8 @@ export default memo(withGlobal<OwnProps>(
const canForward = !isSchedule && chatId ? selectCanForwardMessages(global, chatId, selectedMessageIds) : false;
const isShareMessageModalOpen = tabState.isShareMessageModalShown;
const isAnyModalOpen = Boolean(isShareMessageModalOpen || tabState.requestedDraft
|| tabState.requestedAttachBotInChat || tabState.requestedAttachBotInstall || tabState.reportModal);
|| tabState.requestedAttachBotInChat || tabState.requestedAttachBotInstall || tabState.reportModal
|| tabState.deleteMessageModal);
return {
chat,

View File

@ -152,7 +152,11 @@ const useEditing = (
}
if (!text && !hasMessageMedia(editedMessage)) {
openDeleteMessageModal({ isSchedule: type === 'scheduled', message: editedMessage });
openDeleteMessageModal({
chatId,
messageIds: [editedMessage.id],
isSchedule: type === 'scheduled',
});
return;
}

View File

@ -379,7 +379,14 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
const handleDelete = useLastCallback(() => {
setIsMenuOpen(false);
closeMenu();
openDeleteMessageModal({ isSchedule: messageListType === 'scheduled', album, message });
const messageIds = album?.messages
? album.messages.map(({ id }) => id)
: [message.id];
openDeleteMessageModal({
chatId: message.chatId,
messageIds,
isSchedule: messageListType === 'scheduled',
});
});
const closePinModal = useLastCallback(() => {

View File

@ -173,7 +173,7 @@ const ChatReportPane: FC<OwnProps & StateProps> = ({
<Icon name="close" />
</Button>
<ConfirmDialog
isOpen={isBlockUserModalOpen}
isOpen={true}
onClose={closeBlockUserModal}
title={lang('BlockUserTitle', user ? getUserFirstOrLastName(user) : getChatTitle(lang, chat!))}
text={user

View File

@ -36,7 +36,7 @@ type StateProps = {
members?: ApiChatMember[];
};
const ITEM_HEIGHT = 24 + 20;
const ITEM_HEIGHT = 48;
const BEFORE_ITEMS_COUNT = 2;
const ITEMS_COUNT = 9;

View File

@ -40,7 +40,7 @@ type StateProps = {
isFormFullyDisabled?: boolean;
};
const ITEM_HEIGHT = 24 + 32;
const ITEM_HEIGHT = 48;
const SHIFT_HEIGHT_MINUS = 1;
const BEFORE_ITEMS_COUNT = 2;
const BEFORE_USER_INFO_HEIGHT = 96;

View File

@ -269,11 +269,11 @@
position: absolute;
width: 100%;
left: 0.125rem;
padding: 0 0.5rem 0 3.375rem;
padding: 0 0.5rem 0 3.5rem;
background: var(--color-background);
&--open {
transform: translateY(0.25rem);
transform: translateY(0.025rem);
}
}

View File

@ -166,7 +166,7 @@
content: "";
display: block;
position: absolute;
left: 3.5rem;
left: 1.125rem;
width: 1.125rem;
height: 1.125rem;
}
@ -217,7 +217,7 @@
align-items: center;
gap: 0.3125rem;
margin-block: 0;
padding: 0 0 0 3.5rem;
padding: 0 0 0 4.1875rem;
&:hover,
&:focus-visible {
@ -253,7 +253,8 @@
}
&.avatar {
padding-inline-start: 5.5rem;
padding-inline-start: 3.5rem;
margin-inline-start: 3.125rem;
}
.button {

View File

@ -76,7 +76,7 @@ const CheckboxGroup: FC<OwnProps> = ({
});
const getCheckedNestedCount = useLastCallback((nestedOptions: IRadioOption[]) => {
const checkedCount = nestedOptions?.filter((nestedOption) => values.includes(nestedOption.value)).length;
return checkedCount > 0 ? checkedCount : undefined;
return checkedCount > 0 ? checkedCount : nestedOptions.length;
});
return (

View File

@ -200,7 +200,7 @@
}
.dialog-checkbox {
margin: 1rem -1rem;
margin: 1rem -1.125rem;
}
.confirm-dialog-button {

View File

@ -182,7 +182,12 @@ export const SCROLL_SHORT_TRANSITION_MAX_DISTANCE = 300; // px
// Average duration of message sending animation
export const API_UPDATE_THROTTLE = Math.round((SCROLL_MIN_DURATION + SCROLL_MAX_DURATION) / 2);
export const API_THROTTLE_RESET_UPDATES = new Set([
'newMessage', 'newScheduledMessage', 'deleteMessages', 'deleteScheduledMessages', 'deleteHistory',
'newMessage',
'newScheduledMessage',
'deleteMessages',
'deleteScheduledMessages',
'deleteHistory',
'deleteParticipantHistory',
]);
export const LOCK_SCREEN_ANIMATION_DURATION_MS = 200;

View File

@ -745,7 +745,9 @@ addActionHandler('joinChannel', async (global, actions, payload): Promise<void>
});
addActionHandler('deleteChatUser', (global, actions, payload): ActionReturnType => {
const { chatId, userId, tabId = getCurrentTabId() } = payload;
const {
chatId, userId, shouldRevokeHistory, tabId = getCurrentTabId(),
} = payload;
const chat = selectChat(global, chatId);
const user = selectUser(global, userId);
if (!chat || !user) {
@ -759,7 +761,7 @@ addActionHandler('deleteChatUser', (global, actions, payload): ActionReturnType
actions.openChat({ id: undefined, tabId });
}
void callApi('deleteChatUser', { chat, user });
void callApi('deleteChatUser', { chat, user, shouldRevokeHistory });
});
addActionHandler('deleteChat', (global, actions, payload): ActionReturnType => {

View File

@ -760,6 +760,16 @@ addActionHandler('deleteMessages', (global, actions, payload): ActionReturnType
}
});
addActionHandler('deleteParticipantHistory', (global, actions, payload): ActionReturnType => {
const {
chatId, peerId,
} = payload;
const chat = selectChat(global, chatId)!;
const peer = selectPeer(global, peerId)!;
void callApi('deleteParticipantHistory', { chat, peer });
});
addActionHandler('deleteScheduledMessages', (global, actions, payload): ActionReturnType => {
const { messageIds, tabId = getCurrentTabId() } = payload;
const currentMessageList = selectCurrentMessageList(global, tabId);

View File

@ -25,7 +25,11 @@ import {
isMessageLocal, isUserId,
} from '../../helpers';
import { getMessageReplyInfo, getStoryReplyInfo } from '../../helpers/replies';
import { addActionHandler, getGlobal, setGlobal } from '../../index';
import {
addActionHandler,
getGlobal,
setGlobal,
} from '../../index';
import {
addMessages,
addViewportId,
@ -659,6 +663,15 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
break;
}
case 'deleteParticipantHistory': {
const { chatId, peerId } = update;
global = getGlobal();
deleteParticipantHistory(global, chatId, peerId, actions);
break;
}
case 'updateCommonBoxMessages': {
const { ids, messageUpdate } = update;
@ -1081,6 +1094,25 @@ function findLastMessage<T extends GlobalState>(global: T, chatId: string, threa
return undefined;
}
export function deleteParticipantHistory<T extends GlobalState>(
global: T,
chatId: string,
peerId: string,
actions: RequiredGlobalActions,
) {
const byId = selectChatMessages(global, chatId);
const messageIds = Object.values(byId).filter((message) => {
return message.senderId === peerId;
}).map((message) => message.id);
if (!messageIds.length) {
return;
}
deleteMessages(global, chatId, messageIds, actions);
}
export function deleteThread<T extends GlobalState>(
global: T,
chatId: string,

View File

@ -1037,17 +1037,16 @@ function copyTextForMessages(global: GlobalState, chatId: string, messageIds: nu
addActionHandler('openDeleteMessageModal', (global, actions, payload): ActionReturnType => {
const {
message, isSchedule, album,
chatId, messageIds, isSchedule,
tabId = getCurrentTabId(),
} = payload;
global = getGlobal();
global = updateTabState(global, {
deleteMessageModal: {
chatId,
messageIds,
isSchedule,
album,
message,
},
}, tabId);
setGlobal(global);

View File

@ -410,18 +410,13 @@ export function selectSender<T extends GlobalState>(global: T, message: ApiMessa
export function getSendersFromSelectedMessages<T extends GlobalState>(
global: T,
chat: ApiChat | undefined,
...[tabId = getCurrentTabId()]: TabArgs<T>
chatId: string,
messageIds: number[],
) {
const { messageIds: selectedMessageIds } = selectTabState(global, tabId).selectedMessages || {};
if (!chat?.id || !selectedMessageIds) {
return undefined;
}
return selectedMessageIds.map((id) => {
const message = selectChatMessage(global, chat.id, id);
return messageIds.map((id) => {
const message = selectChatMessage(global, chatId, id);
return message && selectSender(global, message);
});
}).filter(Boolean);
}
export function selectSenderFromMessage<T extends GlobalState>(
@ -782,21 +777,19 @@ export function selectAllowedMessageActionsSlow<T extends GlobalState>(
};
}
// This selector always returns a new object which can not be safely used in shallow-equal checks
export function selectCanDeleteSelectedMessages<T extends GlobalState>(
export function selectCanDeleteMessages<T extends GlobalState>(
global: T,
...[tabId = getCurrentTabId()]: TabArgs<T>
chatId: string,
threadId: ThreadId,
messageIds: number[],
) {
const { messageIds: selectedMessageIds } = selectTabState(global, tabId).selectedMessages || {};
const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {};
const chatMessages = chatId && selectChatMessages(global, chatId);
if (!chatMessages || !selectedMessageIds || !threadId) {
const chatMessages = selectChatMessages(global, chatId);
if (messageIds.length > API_GENERAL_ID_LIMIT) {
return {};
}
if (selectedMessageIds.length > API_GENERAL_ID_LIMIT) return {};
const messageActions = selectedMessageIds
const messageActions = messageIds
.map((id) => chatMessages[id] && selectAllowedMessageActionsSlow(global, chatMessages[id], threadId))
.filter(Boolean);
@ -806,6 +799,21 @@ export function selectCanDeleteSelectedMessages<T extends GlobalState>(
};
}
export function selectCanDeleteSelectedMessages<T extends GlobalState>(
global: T,
messageIds?: number[],
...[tabId = getCurrentTabId()]: TabArgs<T>
) {
const { messageIds: selectedMessageIds } = selectTabState(global, tabId).selectedMessages || {};
const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {};
const messageIdList = messageIds?.length ? messageIds : selectedMessageIds;
if (!chatId || !threadId || !messageIdList) {
return {};
}
return selectCanDeleteMessages(global, chatId, threadId, messageIdList);
}
export function selectCanReportSelectedMessages<T extends GlobalState>(
global: T,
...[tabId = getCurrentTabId()]: TabArgs<T>

View File

@ -61,7 +61,6 @@ import type {
ConfettiParams,
GiftProfileFilterOptions,
GlobalSearchContent,
IAlbum,
IAnchorPosition,
ISettings,
IThemeSettings,
@ -475,6 +474,10 @@ export interface ActionPayloads {
messageIds: number[];
shouldDeleteForAll?: boolean;
} & WithTabId;
deleteParticipantHistory: {
peerId: string;
chatId: string;
} & WithTabId;
markMessageListRead: {
maxId: number;
} & WithTabId;
@ -649,7 +652,11 @@ export interface ActionPayloads {
replyToNextMessage: {
targetIndexDelta: number;
} & WithTabId;
deleteChatUser: { chatId: string; userId: string } & WithTabId;
deleteChatUser: {
chatId: string;
userId: string;
shouldRevokeHistory?: boolean;
} & WithTabId;
deleteChat: { chatId: string } & WithTabId;
// chat creation
@ -2286,9 +2293,9 @@ export interface ActionPayloads {
closePaidReactionModal: WithTabId | undefined;
openDeleteMessageModal: ({
message?: ApiMessage;
chatId: string;
messageIds: number[];
isSchedule?: boolean;
album?: IAlbum;
onConfirm?: NoneToVoidFunction;
} & WithTabId);
closeDeleteMessageModal: WithTabId | undefined;

View File

@ -60,7 +60,6 @@ import type {
FocusDirection,
GiftProfileFilterOptions,
GlobalSearchContent,
IAlbum,
IAnchorPosition,
InlineBotSettings,
ManagementProgress,
@ -581,9 +580,9 @@ export type TabState = {
};
deleteMessageModal?: {
message?: ApiMessage;
chatId: string;
messageIds: number[];
isSchedule?: boolean;
album?: IAlbum;
onConfirm?: NoneToVoidFunction;
};

View File

@ -1668,6 +1668,7 @@ channels.togglePreHistoryHidden#eabbb94c channel:InputChannel enabled:Bool = Upd
channels.getGroupsForDiscussion#f5dad378 = messages.Chats;
channels.setDiscussionGroup#40582bb2 broadcast:InputChannel group:InputChannel = Bool;
channels.getSendAs#dc770ee peer:InputPeer = channels.SendAsPeers;
channels.deleteParticipantHistory#367544db channel:InputChannel participant:InputPeer = messages.AffectedHistory;
channels.toggleJoinToSend#e4cb9580 channel:InputChannel enabled:Bool = Updates;
channels.toggleJoinRequest#4c2985b6 channel:InputChannel enabled:Bool = Updates;
channels.reorderUsernames#b45ced1d channel:InputChannel order:Vector<string> = Bool;

View File

@ -240,6 +240,7 @@
"help.getPremiumPromo",
"channels.readHistory",
"channels.deleteMessages",
"channels.deleteParticipantHistory",
"channels.getMessages",
"channels.getParticipants",
"channels.getParticipant",