From e90bd8ba9a5e636194c7f19998026a7d431d5490 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Thu, 13 Feb 2025 14:27:54 +0100 Subject: [PATCH] Delete Message Modal: Add linked chat moderation (#5519) --- src/api/gramjs/methods/chats.ts | 5 +- src/api/gramjs/methods/messages.ts | 31 ++ src/api/types/updates.ts | 11 +- .../common/DeleteMessageModal.module.scss | 34 +- src/components/common/DeleteMessageModal.tsx | 501 +++++++++-------- .../mediaViewer/MediaViewerActions.tsx | 12 +- .../DeleteSelectedMessageModal.module.scss | 68 --- .../middle/DeleteSelectedMessageModal.tsx | 508 ------------------ .../middle/MessageSelectToolbar.tsx | 34 +- .../middle/composer/hooks/useEditing.ts | 6 +- .../middle/message/ContextMenuContainer.tsx | 9 +- .../middle/panes/ChatReportPane.tsx | 2 +- .../management/ManageGroupPermissions.tsx | 2 +- .../management/ManageGroupUserPermissions.tsx | 2 +- .../right/management/Management.scss | 4 +- src/components/ui/Checkbox.scss | 7 +- src/components/ui/CheckboxGroup.tsx | 2 +- src/components/ui/Modal.scss | 2 +- src/config.ts | 7 +- src/global/actions/api/chats.ts | 6 +- src/global/actions/api/messages.ts | 10 + src/global/actions/apiUpdaters/messages.ts | 34 +- src/global/actions/ui/messages.ts | 7 +- src/global/selectors/messages.ts | 48 +- src/global/types/actions.ts | 15 +- src/global/types/tabState.ts | 5 +- src/lib/gramjs/tl/apiTl.ts | 1 + src/lib/gramjs/tl/static/api.json | 1 + 28 files changed, 488 insertions(+), 886 deletions(-) delete mode 100644 src/components/middle/DeleteSelectedMessageModal.module.scss delete mode 100644 src/components/middle/DeleteSelectedMessageModal.tsx diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts index 4a06997c4..baea62719 100644 --- a/src/api/gramjs/methods/chats.ts +++ b/src/api/gramjs/methods/chats.ts @@ -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, }); diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index 231d03efc..4781deee8 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -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, }: { diff --git a/src/api/types/updates.ts b/src/api/types/updates.ts index 656475000..2650830c4 100644 --- a/src/api/types/updates.ts +++ b/src/api/types/updates.ts @@ -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 | diff --git a/src/components/common/DeleteMessageModal.module.scss b/src/components/common/DeleteMessageModal.module.scss index b6fb4c6a3..aeab720aa 100644 --- a/src/components/common/DeleteMessageModal.module.scss +++ b/src/components/common/DeleteMessageModal.module.scss @@ -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; diff --git a/src/components/common/DeleteMessageModal.tsx b/src/components/common/DeleteMessageModal.tsx index 9e1d74a4a..b80f82ddc 100644 --- a/src/components/common/DeleteMessageModal.tsx +++ b/src/components/common/DeleteMessageModal.tsx @@ -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; chatBot?: boolean; isSchedule?: boolean; - message?: ApiMessage; - album?: IAlbum; onConfirm?: NoneToVoidFunction; - isOwn?: boolean; canBanUsers?: boolean; + isCreator?: boolean; + linkedChatId?: string; }; const DeleteMessageModal: FC = ({ 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(undefined); - const [chosenBanOption, setChosenBanOptions] = useState(undefined); - const [chosenSpanOption, setChosenSpanOptions] = useState(undefined); + const [peerIdsToDeleteAll, setPeerIdsToDeleteAll] = useState(undefined); + const [peerIdsToBan, setPeerIdsToBan] = useState(undefined); + const [peerIdsToReportSpam, setPeerIdsToReportSpam] = useState(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: , + }; + }); }); - 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) => { + 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 = ({ }); }); - 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>((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 = ({ function renderHeader() { return ( -
- {shouldShowOptions && ( - + {shouldShowOption && ( + )} -

{lang('DeleteSingleMessagesTitle')}

+

+ {oldLang('Chat.DeleteMessagesConfirmation', messageIds?.length)} +

); } @@ -317,19 +363,24 @@ const DeleteMessageModal: FC = ({
= 2} /> - - {!isSenderOwner && canBanUsers && ( + {peerListToDeleteAll?.length > 0 && ( + = 2} + /> + )} + {peerListToBan?.length > 0 && ( = 2} /> )}
@@ -342,11 +393,10 @@ const DeleteMessageModal: FC = ({ isAdditionalOptionsVisible && styles.restrictionContainerOpen)} >

- {lang('UserRestrictionsCanDoUsers', 1)} + {oldLang('UserRestrictionsCanDoUsers', peerList.length)}

= ({ ); } + if (!messageIds) { + return undefined; + } + return ( -
+
{renderHeader()} - {shouldShowOptions && ( + {shouldShowOption && ( <> -

{lang('DeleteAdditionalActions')}

+

{oldLang('DeleteAdditionalActions')}

{renderAdditionalActionOptions()} {renderPartiallyRestrictedUser()} { - chosenBanOption && canBanUsers && chosenBanOption?.length ? ( + peerIdsToBan && canBanUsers ? ( - {lang(isAdditionalOptionsVisible ? 'DeleteToggleBanUsers' : 'DeleteToggleRestrictUsers')} + {oldLang(isAdditionalOptionsVisible ? 'DeleteToggleBanUsers' : 'DeleteToggleRestrictUsers')} = ({ } )} - {(chatBot || !shouldShowAdditionalOptions) && ( + {(canDeleteForAll || chatBot || !shouldShowOption) && ( <> -

{lang('AreYouSureDeleteSingleMessage')}

+

{messageIds.length > 1 + ? lang('AreYouSureDeleteFewMessages') : lang('AreYouSureDeleteSingleMessage')} +

{willDeleteForCurrentUserOnly && ( -

{lang('lng_delete_for_me_chat_hint', 1, 'i')}

+

{oldLang('lng_delete_for_me_chat_hint', 1, 'i')}

)} {willDeleteForAll && ( -

{lang('lng_delete_for_everyone_hint', 1, 'i')}

+

{oldLang('lng_delete_for_everyone_hint', 1, 'i')}

)} )} -
+ )} +
- {canDeleteForAll && ( - - )} - - +
@@ -433,48 +486,44 @@ export default memo(withGlobal( 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)); diff --git a/src/components/mediaViewer/MediaViewerActions.tsx b/src/components/mediaViewer/MediaViewerActions.tsx index 382c5cd00..9fbe31047 100644 --- a/src/components/mediaViewer/MediaViewerActions.tsx +++ b/src/components/mediaViewer/MediaViewerActions.tsx @@ -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 = ({ item, mediaData, isVideo, + chat, isChatProtected, isProtected, canReportAvatar, @@ -211,10 +214,11 @@ const MediaViewerActions: FC = ({ } 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( 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( return { activeDownloads, isProtected, + chat, isChatProtected, canDelete, canUpdate, diff --git a/src/components/middle/DeleteSelectedMessageModal.module.scss b/src/components/middle/DeleteSelectedMessageModal.module.scss deleted file mode 100644 index 90ee43163..000000000 --- a/src/components/middle/DeleteSelectedMessageModal.module.scss +++ /dev/null @@ -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; -} diff --git a/src/components/middle/DeleteSelectedMessageModal.tsx b/src/components/middle/DeleteSelectedMessageModal.tsx deleted file mode 100644 index e06bbb606..000000000 --- a/src/components/middle/DeleteSelectedMessageModal.tsx +++ /dev/null @@ -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; - canBanUsers?: boolean; -}; - -const DeleteSelectedMessageModal: FC = ({ - 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(undefined); - const [chosenBanOption, setChosenBanOptions] = useState(undefined); - const [chosenSpanOption, setChosenSpanOptions] = useState(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: , - })); - }); - - 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) => { - 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>((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 ( -
- {shouldShowOptions && ( - - )} -

- {oldLang('Chat.DeleteMessagesConfirmation', selectedMessageIds?.length)} -

-
- ); - } - - function renderAdditionalActionOptions() { - return ( -
- = 2} - /> - = 2} - /> - {!isSenderOwner && canBanUsers && ( - = 2} - /> - )} -
- ); - } - - function renderPartiallyRestrictedUser() { - return ( -
-

- {oldLang('UserRestrictionsCanDoUsers', userList.length)} -

- -
- ); - } - - if (!selectedMessageIds) { - return undefined; - } - - return ( - -
- {renderHeader()} - {!showAdditionalOptions &&

{lang('AreYouSureDeleteFewMessages')}

} - {shouldShowOptions && ( - <> -

{oldLang('DeleteAdditionalActions')}

- {renderAdditionalActionOptions()} - {renderPartiallyRestrictedUser()} - { - chosenBanOption && canBanUsers && chosenBanOption?.length ? ( - - {oldLang(isAdditionalOptionsVisible ? 'DeleteToggleBanUsers' : 'DeleteToggleRestrictUsers')} - - - ) : setIsAdditionalOptionsVisible(false) - } - - )} - {willDeleteForCurrentUserOnly && lang('DeleteForMeDescription')} - {(willDeleteForAll && !showAdditionalOptions) && lang('DeleteForEveryoneDescription')} -
- {canDeleteForAll && ( - - )} - - -
-
-
- ); -}; - -export default memo(withGlobal( - (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)); diff --git a/src/components/middle/MessageSelectToolbar.tsx b/src/components/middle/MessageSelectToolbar.tsx index c124f0a9d..a4089484c 100644 --- a/src/components/middle/MessageSelectToolbar.tsx +++ b/src/components/middle/MessageSelectToolbar.tsx @@ -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 = ({ 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 = ({ renderButton('copy', lang('lng_context_copy_selected_items'), handleCopy) )} {canDeleteMessages && ( - renderButton('delete', lang('EditAdminGroupDeleteMessages'), openDeleteModal, true) + renderButton('delete', lang('EditAdminGroupDeleteMessages'), handleDelete, true) )}
)}
- {canDeleteMessages && ( - - )} ); }; @@ -211,7 +210,8 @@ export default memo(withGlobal( 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, diff --git a/src/components/middle/composer/hooks/useEditing.ts b/src/components/middle/composer/hooks/useEditing.ts index 11a8d08ac..a34c72aac 100644 --- a/src/components/middle/composer/hooks/useEditing.ts +++ b/src/components/middle/composer/hooks/useEditing.ts @@ -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; } diff --git a/src/components/middle/message/ContextMenuContainer.tsx b/src/components/middle/message/ContextMenuContainer.tsx index 0d31788c3..d0b86cb52 100644 --- a/src/components/middle/message/ContextMenuContainer.tsx +++ b/src/components/middle/message/ContextMenuContainer.tsx @@ -379,7 +379,14 @@ const ContextMenuContainer: FC = ({ 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(() => { diff --git a/src/components/middle/panes/ChatReportPane.tsx b/src/components/middle/panes/ChatReportPane.tsx index 215b154ef..aa74097ae 100644 --- a/src/components/middle/panes/ChatReportPane.tsx +++ b/src/components/middle/panes/ChatReportPane.tsx @@ -173,7 +173,7 @@ const ChatReportPane: FC = ({ = ({ }); 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 ( diff --git a/src/components/ui/Modal.scss b/src/components/ui/Modal.scss index 2f340b519..5e4a7d75b 100644 --- a/src/components/ui/Modal.scss +++ b/src/components/ui/Modal.scss @@ -200,7 +200,7 @@ } .dialog-checkbox { - margin: 1rem -1rem; + margin: 1rem -1.125rem; } .confirm-dialog-button { diff --git a/src/config.ts b/src/config.ts index d64b7576f..f780ece7b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -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; diff --git a/src/global/actions/api/chats.ts b/src/global/actions/api/chats.ts index c39973c45..efdce280e 100644 --- a/src/global/actions/api/chats.ts +++ b/src/global/actions/api/chats.ts @@ -745,7 +745,9 @@ addActionHandler('joinChannel', async (global, actions, payload): Promise }); 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 => { diff --git a/src/global/actions/api/messages.ts b/src/global/actions/api/messages.ts index 8b96690ed..9763ba7fc 100644 --- a/src/global/actions/api/messages.ts +++ b/src/global/actions/api/messages.ts @@ -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); diff --git a/src/global/actions/apiUpdaters/messages.ts b/src/global/actions/apiUpdaters/messages.ts index 2103e310b..a8399a310 100644 --- a/src/global/actions/apiUpdaters/messages.ts +++ b/src/global/actions/apiUpdaters/messages.ts @@ -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(global: T, chatId: string, threa return undefined; } +export function deleteParticipantHistory( + 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( global: T, chatId: string, diff --git a/src/global/actions/ui/messages.ts b/src/global/actions/ui/messages.ts index ccd3c5a23..f665284a3 100644 --- a/src/global/actions/ui/messages.ts +++ b/src/global/actions/ui/messages.ts @@ -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); diff --git a/src/global/selectors/messages.ts b/src/global/selectors/messages.ts index b057c2e82..10a135586 100644 --- a/src/global/selectors/messages.ts +++ b/src/global/selectors/messages.ts @@ -410,18 +410,13 @@ export function selectSender(global: T, message: ApiMessa export function getSendersFromSelectedMessages( global: T, - chat: ApiChat | undefined, - ...[tabId = getCurrentTabId()]: TabArgs + 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( @@ -782,21 +777,19 @@ export function selectAllowedMessageActionsSlow( }; } -// This selector always returns a new object which can not be safely used in shallow-equal checks -export function selectCanDeleteSelectedMessages( +export function selectCanDeleteMessages( global: T, - ...[tabId = getCurrentTabId()]: TabArgs + 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( }; } +export function selectCanDeleteSelectedMessages( + global: T, + messageIds?: number[], + ...[tabId = getCurrentTabId()]: TabArgs +) { + 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( global: T, ...[tabId = getCurrentTabId()]: TabArgs diff --git a/src/global/types/actions.ts b/src/global/types/actions.ts index d30a55461..ae56d91c0 100644 --- a/src/global/types/actions.ts +++ b/src/global/types/actions.ts @@ -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; diff --git a/src/global/types/tabState.ts b/src/global/types/tabState.ts index 9654888a9..9a8ce804c 100644 --- a/src/global/types/tabState.ts +++ b/src/global/types/tabState.ts @@ -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; }; diff --git a/src/lib/gramjs/tl/apiTl.ts b/src/lib/gramjs/tl/apiTl.ts index bc69afbd8..a129b1b2f 100644 --- a/src/lib/gramjs/tl/apiTl.ts +++ b/src/lib/gramjs/tl/apiTl.ts @@ -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 = Bool; diff --git a/src/lib/gramjs/tl/static/api.json b/src/lib/gramjs/tl/static/api.json index 5d247db2a..01759d0c1 100644 --- a/src/lib/gramjs/tl/static/api.json +++ b/src/lib/gramjs/tl/static/api.json @@ -240,6 +240,7 @@ "help.getPremiumPromo", "channels.readHistory", "channels.deleteMessages", + "channels.deleteParticipantHistory", "channels.getMessages", "channels.getParticipants", "channels.getParticipant",