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({ export function deleteChatUser({
chat, user, chat, user, shouldRevokeHistory,
}: { }: {
chat: ApiChat; user: ApiUser; chat: ApiChat; user: ApiUser; shouldRevokeHistory?: boolean;
}) { }) {
if (chat.type !== 'chatTypeBasicGroup') return undefined; if (chat.type !== 'chatTypeBasicGroup') return undefined;
return invokeRequest(new GramJs.messages.DeleteChatUser({ return invokeRequest(new GramJs.messages.DeleteChatUser({
chatId: buildInputEntity(chat.id, chat.accessHash) as BigInt.BigInteger, chatId: buildInputEntity(chat.id, chat.accessHash) as BigInt.BigInteger,
userId: buildInputEntity(user.id, user.accessHash) as GramJs.InputUser, userId: buildInputEntity(user.id, user.accessHash) as GramJs.InputUser,
revokeHistory: shouldRevokeHistory || undefined,
}), { }), {
shouldReturnTrue: true, 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({ export function deleteScheduledMessages({
chat, messageIds, chat, messageIds,
}: { }: {

View File

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

View File

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

View File

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

View File

@ -2,6 +2,7 @@ import type { FC } from '../../lib/teact/teact';
import React, { memo, useMemo } from '../../lib/teact/teact'; import React, { memo, useMemo } from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global'; import { getActions, withGlobal } from '../../global';
import type { ApiChat } from '../../api/types';
import type { ActiveDownloads, MediaViewerOrigin, MessageListType } from '../../types'; import type { ActiveDownloads, MediaViewerOrigin, MessageListType } from '../../types';
import type { IconName } from '../../types/icons'; import type { IconName } from '../../types/icons';
import type { MenuItemProps } from '../ui/MenuItem'; import type { MenuItemProps } from '../ui/MenuItem';
@ -16,7 +17,7 @@ import {
} from '../../global/helpers'; } from '../../global/helpers';
import { import {
selectActiveDownloads, selectActiveDownloads,
selectAllowedMessageActionsSlow, selectAllowedMessageActionsSlow, selectCurrentChat,
selectCurrentMessageList, selectCurrentMessageList,
selectIsChatProtected, selectIsChatProtected,
selectIsMessageProtected, selectIsMessageProtected,
@ -45,6 +46,7 @@ type StateProps = {
isProtected?: boolean; isProtected?: boolean;
isChatProtected?: boolean; isChatProtected?: boolean;
canDelete?: boolean; canDelete?: boolean;
chat?: ApiChat;
canUpdate?: boolean; canUpdate?: boolean;
messageListType?: MessageListType; messageListType?: MessageListType;
origin?: MediaViewerOrigin; origin?: MediaViewerOrigin;
@ -67,6 +69,7 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
item, item,
mediaData, mediaData,
isVideo, isVideo,
chat,
isChatProtected, isChatProtected,
isProtected, isProtected,
canReportAvatar, canReportAvatar,
@ -211,10 +214,11 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
} }
const openDeleteModalHandler = useLastCallback(() => { const openDeleteModalHandler = useLastCallback(() => {
if (item?.type === 'message') { if (item?.type === 'message' && chat) {
openDeleteMessageModal({ openDeleteMessageModal({
chatId: chat?.id,
messageIds: [item.message.id],
isSchedule: messageListType === 'scheduled', isSchedule: messageListType === 'scheduled',
message: item.message,
onConfirm: onBeforeDelete, onConfirm: onBeforeDelete,
}); });
} else { } else {
@ -394,6 +398,7 @@ export default memo(withGlobal<OwnProps>(
const avatarOwner = item?.type === 'avatar' ? item.avatarOwner : undefined; const avatarOwner = item?.type === 'avatar' ? item.avatarOwner : undefined;
const avatarPhoto = item?.type === 'avatar' && item.profilePhotos.photos[item.mediaIndex]; const avatarPhoto = item?.type === 'avatar' && item.profilePhotos.photos[item.mediaIndex];
const chat = selectCurrentChat(global);
const currentMessageList = selectCurrentMessageList(global); const currentMessageList = selectCurrentMessageList(global);
const { threadId } = selectCurrentMessageList(global) || {}; const { threadId } = selectCurrentMessageList(global) || {};
const isProtected = selectIsMessageProtected(global, message); const isProtected = selectIsMessageProtected(global, message);
@ -410,6 +415,7 @@ export default memo(withGlobal<OwnProps>(
return { return {
activeDownloads, activeDownloads,
isProtected, isProtected,
chat,
isChatProtected, isChatProtected,
canDelete, canDelete,
canUpdate, 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 buildClassName from '../../util/buildClassName';
import captureKeyboardListeners from '../../util/captureKeyboardListeners'; import captureKeyboardListeners from '../../util/captureKeyboardListeners';
import useFlag from '../../hooks/useFlag';
import useLastCallback from '../../hooks/useLastCallback'; import useLastCallback from '../../hooks/useLastCallback';
import useOldLang from '../../hooks/useOldLang'; import useOldLang from '../../hooks/useOldLang';
import usePreviousDeprecated from '../../hooks/usePreviousDeprecated'; import usePreviousDeprecated from '../../hooks/usePreviousDeprecated';
@ -26,7 +25,6 @@ import useCopySelectedMessages from './hooks/useCopySelectedMessages';
import Icon from '../common/icons/Icon'; import Icon from '../common/icons/Icon';
import Button from '../ui/Button'; import Button from '../ui/Button';
import DeleteSelectedMessageModal from './DeleteSelectedMessageModal';
import './MessageSelectToolbar.scss'; import './MessageSelectToolbar.scss';
@ -71,27 +69,35 @@ const MessageSelectToolbar: FC<OwnProps & StateProps> = ({
copySelectedMessages, copySelectedMessages,
showNotification, showNotification,
reportMessages, reportMessages,
openDeleteMessageModal,
} = getActions(); } = getActions();
const lang = useOldLang(); const lang = useOldLang();
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag();
useCopySelectedMessages(isActive); useCopySelectedMessages(isActive);
const handleExitMessageSelectMode = useLastCallback(() => { const handleExitMessageSelectMode = useLastCallback(() => {
exitMessageSelectMode(); exitMessageSelectMode();
}); });
const handleDelete = useLastCallback(() => {
if (!selectedMessageIds || !chat) return;
openDeleteMessageModal({
chatId: chat.id,
messageIds: selectedMessageIds,
isSchedule,
});
});
useEffect(() => { useEffect(() => {
return isActive && !isDeleteModalOpen && !isAnyModalOpen return isActive && !isAnyModalOpen
? captureKeyboardListeners({ ? captureKeyboardListeners({
onBackspace: canDeleteMessages ? openDeleteModal : undefined, onBackspace: canDeleteMessages ? handleDelete : undefined,
onDelete: canDeleteMessages ? openDeleteModal : undefined, onDelete: canDeleteMessages ? handleDelete : undefined,
onEsc: handleExitMessageSelectMode, onEsc: handleExitMessageSelectMode,
}) })
: undefined; : undefined;
}, [ }, [
isActive, isDeleteModalOpen, openDeleteModal, handleExitMessageSelectMode, isAnyModalOpen, isActive, handleDelete, handleExitMessageSelectMode, isAnyModalOpen,
canDeleteMessages, canDeleteMessages,
]); ]);
@ -181,18 +187,11 @@ const MessageSelectToolbar: FC<OwnProps & StateProps> = ({
renderButton('copy', lang('lng_context_copy_selected_items'), handleCopy) renderButton('copy', lang('lng_context_copy_selected_items'), handleCopy)
)} )}
{canDeleteMessages && ( {canDeleteMessages && (
renderButton('delete', lang('EditAdminGroupDeleteMessages'), openDeleteModal, true) renderButton('delete', lang('EditAdminGroupDeleteMessages'), handleDelete, true)
)} )}
</div> </div>
)} )}
</div> </div>
{canDeleteMessages && (
<DeleteSelectedMessageModal
isOpen={isDeleteModalOpen}
isSchedule={isSchedule}
onClose={closeDeleteModal}
/>
)}
</div> </div>
); );
}; };
@ -211,7 +210,8 @@ export default memo(withGlobal<OwnProps>(
const canForward = !isSchedule && chatId ? selectCanForwardMessages(global, chatId, selectedMessageIds) : false; const canForward = !isSchedule && chatId ? selectCanForwardMessages(global, chatId, selectedMessageIds) : false;
const isShareMessageModalOpen = tabState.isShareMessageModalShown; const isShareMessageModalOpen = tabState.isShareMessageModalShown;
const isAnyModalOpen = Boolean(isShareMessageModalOpen || tabState.requestedDraft const isAnyModalOpen = Boolean(isShareMessageModalOpen || tabState.requestedDraft
|| tabState.requestedAttachBotInChat || tabState.requestedAttachBotInstall || tabState.reportModal); || tabState.requestedAttachBotInChat || tabState.requestedAttachBotInstall || tabState.reportModal
|| tabState.deleteMessageModal);
return { return {
chat, chat,

View File

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

View File

@ -379,7 +379,14 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
const handleDelete = useLastCallback(() => { const handleDelete = useLastCallback(() => {
setIsMenuOpen(false); setIsMenuOpen(false);
closeMenu(); 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(() => { const closePinModal = useLastCallback(() => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -182,7 +182,12 @@ export const SCROLL_SHORT_TRANSITION_MAX_DISTANCE = 300; // px
// Average duration of message sending animation // Average duration of message sending animation
export const API_UPDATE_THROTTLE = Math.round((SCROLL_MIN_DURATION + SCROLL_MAX_DURATION) / 2); export const API_UPDATE_THROTTLE = Math.round((SCROLL_MIN_DURATION + SCROLL_MAX_DURATION) / 2);
export const API_THROTTLE_RESET_UPDATES = new Set([ 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; 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 => { 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 chat = selectChat(global, chatId);
const user = selectUser(global, userId); const user = selectUser(global, userId);
if (!chat || !user) { if (!chat || !user) {
@ -759,7 +761,7 @@ addActionHandler('deleteChatUser', (global, actions, payload): ActionReturnType
actions.openChat({ id: undefined, tabId }); actions.openChat({ id: undefined, tabId });
} }
void callApi('deleteChatUser', { chat, user }); void callApi('deleteChatUser', { chat, user, shouldRevokeHistory });
}); });
addActionHandler('deleteChat', (global, actions, payload): ActionReturnType => { 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 => { addActionHandler('deleteScheduledMessages', (global, actions, payload): ActionReturnType => {
const { messageIds, tabId = getCurrentTabId() } = payload; const { messageIds, tabId = getCurrentTabId() } = payload;
const currentMessageList = selectCurrentMessageList(global, tabId); const currentMessageList = selectCurrentMessageList(global, tabId);

View File

@ -25,7 +25,11 @@ import {
isMessageLocal, isUserId, isMessageLocal, isUserId,
} from '../../helpers'; } from '../../helpers';
import { getMessageReplyInfo, getStoryReplyInfo } from '../../helpers/replies'; import { getMessageReplyInfo, getStoryReplyInfo } from '../../helpers/replies';
import { addActionHandler, getGlobal, setGlobal } from '../../index'; import {
addActionHandler,
getGlobal,
setGlobal,
} from '../../index';
import { import {
addMessages, addMessages,
addViewportId, addViewportId,
@ -659,6 +663,15 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
break; break;
} }
case 'deleteParticipantHistory': {
const { chatId, peerId } = update;
global = getGlobal();
deleteParticipantHistory(global, chatId, peerId, actions);
break;
}
case 'updateCommonBoxMessages': { case 'updateCommonBoxMessages': {
const { ids, messageUpdate } = update; const { ids, messageUpdate } = update;
@ -1081,6 +1094,25 @@ function findLastMessage<T extends GlobalState>(global: T, chatId: string, threa
return undefined; 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>( export function deleteThread<T extends GlobalState>(
global: T, global: T,
chatId: string, chatId: string,

View File

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

View File

@ -410,18 +410,13 @@ export function selectSender<T extends GlobalState>(global: T, message: ApiMessa
export function getSendersFromSelectedMessages<T extends GlobalState>( export function getSendersFromSelectedMessages<T extends GlobalState>(
global: T, global: T,
chat: ApiChat | undefined, chatId: string,
...[tabId = getCurrentTabId()]: TabArgs<T> messageIds: number[],
) { ) {
const { messageIds: selectedMessageIds } = selectTabState(global, tabId).selectedMessages || {}; return messageIds.map((id) => {
if (!chat?.id || !selectedMessageIds) { const message = selectChatMessage(global, chatId, id);
return undefined;
}
return selectedMessageIds.map((id) => {
const message = selectChatMessage(global, chat.id, id);
return message && selectSender(global, message); return message && selectSender(global, message);
}); }).filter(Boolean);
} }
export function selectSenderFromMessage<T extends GlobalState>( 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 selectCanDeleteMessages<T extends GlobalState>(
export function selectCanDeleteSelectedMessages<T extends GlobalState>(
global: T, global: T,
...[tabId = getCurrentTabId()]: TabArgs<T> chatId: string,
threadId: ThreadId,
messageIds: number[],
) { ) {
const { messageIds: selectedMessageIds } = selectTabState(global, tabId).selectedMessages || {}; const chatMessages = selectChatMessages(global, chatId);
const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {};
const chatMessages = chatId && selectChatMessages(global, chatId); if (messageIds.length > API_GENERAL_ID_LIMIT) {
if (!chatMessages || !selectedMessageIds || !threadId) {
return {}; return {};
} }
if (selectedMessageIds.length > API_GENERAL_ID_LIMIT) return {}; const messageActions = messageIds
const messageActions = selectedMessageIds
.map((id) => chatMessages[id] && selectAllowedMessageActionsSlow(global, chatMessages[id], threadId)) .map((id) => chatMessages[id] && selectAllowedMessageActionsSlow(global, chatMessages[id], threadId))
.filter(Boolean); .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>( export function selectCanReportSelectedMessages<T extends GlobalState>(
global: T, global: T,
...[tabId = getCurrentTabId()]: TabArgs<T> ...[tabId = getCurrentTabId()]: TabArgs<T>

View File

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

View File

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

View File

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

View File

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