Delete Message Modal: Add linked chat moderation (#5519)
This commit is contained in:
parent
45fc7aac7c
commit
e90bd8ba9a
@ -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,
|
||||
});
|
||||
|
||||
@ -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,
|
||||
}: {
|
||||
|
||||
@ -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 |
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -1,37 +1,41 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useEffect,
|
||||
memo,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from '../../lib/teact/teact';
|
||||
import { getActions, getGlobal, withGlobal } from '../../global';
|
||||
|
||||
import type {
|
||||
ApiChat, ApiChatMember, ApiMessage, ApiPeer,
|
||||
} from '../../api/types';
|
||||
import type { IAlbum } from '../../types';
|
||||
import type { ApiChat, ApiChatMember } from '../../api/types';
|
||||
import type { IRadioOption } from '../ui/CheckboxGroup';
|
||||
|
||||
import {
|
||||
getHasAdminRight,
|
||||
getPeerTitle,
|
||||
getPrivateChatUserId,
|
||||
getUserFirstOrLastName, getUserFullName,
|
||||
isChatBasicGroup,
|
||||
isChatSuperGroup, isOwnMessage,
|
||||
getUserFirstOrLastName, isChatBasicGroup,
|
||||
isChatChannel,
|
||||
isChatSuperGroup,
|
||||
isSystemBot,
|
||||
isUserId,
|
||||
} from '../../global/helpers';
|
||||
import {
|
||||
selectAllowedMessageActionsSlow,
|
||||
getSendersFromSelectedMessages,
|
||||
selectBot,
|
||||
selectChat, selectChatFullInfo, selectCurrentMessageIds,
|
||||
selectCurrentMessageList, selectSender, selectSenderFromMessage, selectTabState,
|
||||
selectCanDeleteSelectedMessages,
|
||||
selectChat,
|
||||
selectChatFullInfo, selectIsChatWithBot,
|
||||
selectSenderFromMessage,
|
||||
selectTabState,
|
||||
selectUser,
|
||||
} from '../../global/selectors';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { buildCollectionByCallback, unique } from '../../util/iteratees';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
|
||||
import renderText from './helpers/renderText';
|
||||
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
import usePreviousDeprecated from '../../hooks/usePreviousDeprecated';
|
||||
@ -39,10 +43,12 @@ import useManagePermissions from '../right/hooks/useManagePermissions';
|
||||
|
||||
import PermissionCheckboxList from '../main/PermissionCheckboxList';
|
||||
import Button from '../ui/Button';
|
||||
import Checkbox from '../ui/Checkbox';
|
||||
import CheckboxGroup from '../ui/CheckboxGroup';
|
||||
import ListItem from '../ui/ListItem';
|
||||
import Modal from '../ui/Modal';
|
||||
import Avatar from './Avatar';
|
||||
import AvatarList from './AvatarList';
|
||||
import Icon from './icons/Icon';
|
||||
|
||||
import styles from './DeleteMessageModal.module.scss';
|
||||
@ -54,146 +60,199 @@ export type OwnProps = {
|
||||
type StateProps = {
|
||||
chat?: ApiChat;
|
||||
isGroup?: boolean;
|
||||
isChannel?: boolean;
|
||||
isSuperGroup?: boolean;
|
||||
sender: ApiPeer | undefined;
|
||||
currentUserId?: string;
|
||||
messageIds?: number[];
|
||||
canDeleteForAll?: boolean;
|
||||
contactName?: string;
|
||||
currentUserId?: string;
|
||||
willDeleteForCurrentUserOnly?: boolean;
|
||||
willDeleteForAll?: boolean;
|
||||
messageIdList: number[] | undefined;
|
||||
adminMembersById?: Record<string, ApiChatMember>;
|
||||
chatBot?: boolean;
|
||||
isSchedule?: boolean;
|
||||
message?: ApiMessage;
|
||||
album?: IAlbum;
|
||||
onConfirm?: NoneToVoidFunction;
|
||||
isOwn?: boolean;
|
||||
canBanUsers?: boolean;
|
||||
isCreator?: boolean;
|
||||
linkedChatId?: string;
|
||||
};
|
||||
|
||||
const DeleteMessageModal: FC<OwnProps & StateProps> = ({
|
||||
isOpen,
|
||||
chat,
|
||||
isGroup,
|
||||
isChannel,
|
||||
isSuperGroup,
|
||||
sender,
|
||||
currentUserId,
|
||||
messageIdList,
|
||||
isSchedule,
|
||||
message,
|
||||
album,
|
||||
currentUserId,
|
||||
messageIds,
|
||||
isCreator,
|
||||
canDeleteForAll,
|
||||
contactName,
|
||||
willDeleteForCurrentUserOnly,
|
||||
willDeleteForAll,
|
||||
onConfirm,
|
||||
adminMembersById,
|
||||
chatBot,
|
||||
isOwn,
|
||||
adminMembersById,
|
||||
canBanUsers,
|
||||
linkedChatId,
|
||||
}) => {
|
||||
const {
|
||||
closeDeleteMessageModal,
|
||||
deleteMessages,
|
||||
deleteScheduledMessages,
|
||||
reportChannelSpam,
|
||||
deleteChatMember,
|
||||
deleteScheduledMessages,
|
||||
exitMessageSelectMode,
|
||||
updateChatMemberBannedRights,
|
||||
closeDeleteMessageModal,
|
||||
deleteParticipantHistory,
|
||||
} = getActions();
|
||||
|
||||
const prevIsOpen = usePreviousDeprecated(isOpen);
|
||||
|
||||
const lang = useOldLang();
|
||||
const oldLang = useOldLang();
|
||||
const lang = useLang();
|
||||
|
||||
const {
|
||||
permissions, havePermissionChanged, handlePermissionChange, resetPermissions,
|
||||
} = useManagePermissions(chat?.defaultBannedRights);
|
||||
|
||||
const [chosenDeleteOption, setChosenDeleteOption] = useState<string[] | undefined>(undefined);
|
||||
const [chosenBanOption, setChosenBanOptions] = useState<string[] | undefined>(undefined);
|
||||
const [chosenSpanOption, setChosenSpanOptions] = useState<string[] | undefined>(undefined);
|
||||
const [peerIdsToDeleteAll, setPeerIdsToDeleteAll] = useState<string[] | undefined>(undefined);
|
||||
const [peerIdsToBan, setPeerIdsToBan] = useState<string[] | undefined>(undefined);
|
||||
const [peerIdsToReportSpam, setPeerIdsToReportSpam] = useState<string[] | undefined>(undefined);
|
||||
const [isMediaDropdownOpen, setIsMediaDropdownOpen] = useState(false);
|
||||
const [isAdditionalOptionsVisible, setIsAdditionalOptionsVisible] = useState(false);
|
||||
const isSenderOwner = useMemo(() => {
|
||||
return sender && adminMembersById && adminMembersById[sender.id] && adminMembersById[sender.id].isOwner;
|
||||
}, [sender, adminMembersById]);
|
||||
const [shouldDeleteForAll, setShouldDeleteForAll] = useState(true);
|
||||
|
||||
const user = useMemo(() => {
|
||||
const usersById = getGlobal().users.byId;
|
||||
if (!sender || isSchedule) return undefined;
|
||||
|
||||
return usersById[sender.id];
|
||||
}, [isSchedule, sender]);
|
||||
|
||||
const shouldShowAdditionalOptions = useMemo(() => {
|
||||
return user && user.id !== currentUserId;
|
||||
}, [user, currentUserId]);
|
||||
|
||||
const shouldShowOptions = shouldShowAdditionalOptions && !canDeleteForAll && !isSchedule && (isGroup || isSuperGroup);
|
||||
|
||||
const userName = useMemo(() => {
|
||||
const usersById = getGlobal().users.byId;
|
||||
if (!sender || isSchedule) return '';
|
||||
|
||||
return getUserFullName(usersById[sender.id]);
|
||||
}, [isSchedule, sender]);
|
||||
|
||||
const ACTION_SPAM_OPTION: IRadioOption[] = useMemo(() => {
|
||||
if (!user) {
|
||||
return [];
|
||||
const peerList = useMemo(() => {
|
||||
if (isChannel || !messageIds || !chat) {
|
||||
return MEMO_EMPTY_ARRAY;
|
||||
}
|
||||
const global = getGlobal();
|
||||
const senderArray = getSendersFromSelectedMessages(global, chat.id, messageIds);
|
||||
return senderArray ? unique(senderArray).filter((peer) => peer?.id !== chat?.id && peer?.id !== linkedChatId) : MEMO_EMPTY_ARRAY;
|
||||
}, [chat, isChannel, messageIds]);
|
||||
|
||||
return [
|
||||
{
|
||||
value: user.id,
|
||||
label: lang('ReportSpamTitle'),
|
||||
},
|
||||
];
|
||||
}, [lang, user]);
|
||||
|
||||
const ACTION_DELETE_OPTION: IRadioOption[] = useMemo(() => {
|
||||
if (!user) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
value: user.id,
|
||||
label: lang('DeleteAllFrom', userName),
|
||||
},
|
||||
];
|
||||
}, [lang, user, userName]);
|
||||
|
||||
const ACTION_BAN_OPTION: IRadioOption[] = useMemo(() => {
|
||||
if (!user) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
value: user.id,
|
||||
label: (message && isAdditionalOptionsVisible ? lang('KickFromSupergroup') : lang('DeleteBan', userName)),
|
||||
},
|
||||
];
|
||||
}, [isAdditionalOptionsVisible, lang, message, user, userName]);
|
||||
|
||||
const toggleAdditionalOptions = useLastCallback(() => {
|
||||
setIsAdditionalOptionsVisible(!isAdditionalOptionsVisible);
|
||||
const buildNestedOptionListWithAvatars = useLastCallback(() => {
|
||||
return peerList.map((member) => {
|
||||
return {
|
||||
value: `${member.id}`,
|
||||
label: getPeerTitle(lang, member) || '',
|
||||
leftElement: <Avatar size="small" peer={member} />,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const filterMessageIdByUserId = useLastCallback((userIds: string[], selectedMessageIdList: number[]) => {
|
||||
const peerListToDeleteAll = useMemo(() => {
|
||||
return peerList.filter((peer) => peer.id !== linkedChatId && peer.id !== currentUserId);
|
||||
}, [peerList, currentUserId, linkedChatId]);
|
||||
|
||||
const peerListToReportSpam = useMemo(() => {
|
||||
return peerList.filter((peer) => peer.id !== currentUserId && peer.id !== linkedChatId);
|
||||
}, [peerList, currentUserId, linkedChatId]);
|
||||
|
||||
const peerListToBan = useMemo(() => {
|
||||
const isCurrentUserInList = peerList.some((peer) => peer.id === currentUserId);
|
||||
const shouldReturnEmpty = !canBanUsers || isCurrentUserInList;
|
||||
|
||||
if (shouldReturnEmpty) {
|
||||
return MEMO_EMPTY_ARRAY;
|
||||
}
|
||||
|
||||
return peerList.filter((peer) => {
|
||||
const isAdmin = adminMembersById?.[peer.id];
|
||||
return isCreator || !isAdmin;
|
||||
});
|
||||
}, [peerList, isCreator, currentUserId, canBanUsers, adminMembersById]);
|
||||
|
||||
const shouldShowAdditionalOptions = useMemo(() => {
|
||||
return Boolean(peerListToDeleteAll.length || peerListToReportSpam.length || peerListToBan.length);
|
||||
}, [peerListToDeleteAll, peerListToReportSpam, peerListToBan]);
|
||||
|
||||
const shouldShowOption = shouldShowAdditionalOptions
|
||||
&& !canDeleteForAll && !isSchedule && isSuperGroup;
|
||||
|
||||
const peerNames = useMemo(() => {
|
||||
if (!peerList || isSchedule) return {};
|
||||
return buildCollectionByCallback(peerList, (peer) => [peer.id, getPeerTitle(lang, peer)]);
|
||||
}, [isSchedule, lang, peerList]);
|
||||
|
||||
const ACTION_SPAM_OPTION: IRadioOption[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
value: messageIds && peerList.length >= 2 ? 'spam' : peerList?.[0]?.id,
|
||||
label: oldLang('ReportSpamTitle'),
|
||||
nestedOptions: messageIds && peerList.length >= 2 ? [
|
||||
...buildNestedOptionListWithAvatars().filter((opt) => opt.value !== linkedChatId
|
||||
&& opt.value !== currentUserId),
|
||||
] : undefined,
|
||||
},
|
||||
];
|
||||
}, [messageIds, peerList, oldLang, linkedChatId, currentUserId]);
|
||||
|
||||
const ACTION_DELETE_OPTION: IRadioOption[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
value: messageIds && peerList.length >= 2 ? 'delete_all' : peerList?.[0]?.id,
|
||||
label: messageIds && peerList.length >= 2
|
||||
? oldLang('DeleteAllFromUsers')
|
||||
: oldLang('DeleteAllFrom', Object.values(peerNames)[0]),
|
||||
nestedOptions: messageIds && peerList.length >= 2 ? [
|
||||
...buildNestedOptionListWithAvatars().filter((opt) => opt.value !== linkedChatId
|
||||
&& opt.value !== currentUserId),
|
||||
] : undefined,
|
||||
},
|
||||
];
|
||||
}, [messageIds, peerList, oldLang, peerNames, linkedChatId, currentUserId]);
|
||||
|
||||
const ACTION_BAN_OPTION: IRadioOption[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
value: messageIds && peerList.length >= 2 ? 'ban' : peerList?.[0]?.id,
|
||||
label: messageIds && peerList.length >= 2
|
||||
? (isAdditionalOptionsVisible ? oldLang('DeleteRestrictUsers') : oldLang('DeleteBanUsers'))
|
||||
: (isAdditionalOptionsVisible ? oldLang('KickFromSupergroup')
|
||||
: oldLang('DeleteBan', Object.values(peerNames)[0])),
|
||||
nestedOptions: messageIds && peerList.length >= 2 ? [
|
||||
...buildNestedOptionListWithAvatars(),
|
||||
] : undefined,
|
||||
},
|
||||
];
|
||||
}, [isAdditionalOptionsVisible, oldLang, messageIds, peerList, peerNames]);
|
||||
|
||||
const toggleAdditionalOptions = useLastCallback(() => {
|
||||
setIsAdditionalOptionsVisible((prev) => !prev);
|
||||
});
|
||||
|
||||
const filterMessageIdByPeerId = useLastCallback((peerIds: string[], selectedMessageIdList: number[]) => {
|
||||
if (!chat) return MEMO_EMPTY_ARRAY;
|
||||
const global = getGlobal();
|
||||
return selectedMessageIdList.filter((msgId) => {
|
||||
const senderPeer = selectSenderFromMessage(getGlobal(), chat.id, msgId);
|
||||
return senderPeer && userIds.includes(senderPeer.id);
|
||||
const sender = selectSenderFromMessage(global, chat.id, msgId);
|
||||
return sender && peerIds.includes(sender.id);
|
||||
});
|
||||
});
|
||||
|
||||
const handleReportSpam = useLastCallback((userMessagesMap: Record<string, number[]>) => {
|
||||
Object.entries(userMessagesMap).forEach(([userId, messageIdList]) => {
|
||||
if (messageIdList.length) {
|
||||
reportChannelSpam({
|
||||
participantId: userId,
|
||||
chatId: chat!.id,
|
||||
messageIds: messageIdList,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const handleDeleteMessages = useLastCallback((filteredMessageIdList: number[]) => {
|
||||
if (filteredMessageIdList?.length) {
|
||||
deleteMessages({ messageIds: filteredMessageIdList, shouldDeleteForAll: true });
|
||||
}
|
||||
deleteMessages({ messageIds: filteredMessageIdList, shouldDeleteForAll: true });
|
||||
});
|
||||
|
||||
const handleDeleteAllPeerMessages = useLastCallback((peerIdList: string[]) => {
|
||||
if (!chat) return;
|
||||
peerIdList.forEach((peerId) => {
|
||||
deleteParticipantHistory({ peerId, chatId: chat.id });
|
||||
});
|
||||
});
|
||||
|
||||
const handleDeleteMember = useLastCallback((filteredUserIdList: string[]) => {
|
||||
@ -212,86 +271,68 @@ const DeleteMessageModal: FC<OwnProps & StateProps> = ({
|
||||
});
|
||||
});
|
||||
|
||||
const handleDeleteMessageForAll = useLastCallback(() => {
|
||||
onConfirm?.();
|
||||
const messageIds = album?.messages
|
||||
? album.messages.map(({ id }) => id)
|
||||
: [message!.id];
|
||||
deleteMessages({ messageIds, shouldDeleteForAll: true });
|
||||
closeDeleteMessageModal();
|
||||
});
|
||||
|
||||
const handleDeleteMessageForSelf = useLastCallback(() => {
|
||||
if (!chat) return;
|
||||
const handleDeleteMessageList = useLastCallback(() => {
|
||||
if (!chat || !messageIds) return;
|
||||
|
||||
onConfirm?.();
|
||||
const messageIds = album?.messages
|
||||
? album.messages.map(({ id }) => id)
|
||||
: [message!.id];
|
||||
if (isSchedule) {
|
||||
deleteScheduledMessages({ messageIds });
|
||||
} else if (!isOwn && (chosenSpanOption || chosenDeleteOption || chosenBanOption) && (isGroup || isSuperGroup)) {
|
||||
if (chosenSpanOption && sender) {
|
||||
const filteredMessageIdList = filterMessageIdByUserId(chosenSpanOption, messageIdList!);
|
||||
if (filteredMessageIdList && filteredMessageIdList.length) {
|
||||
reportChannelSpam({ participantId: sender.id, chatId: chat.id, messageIds: filteredMessageIdList });
|
||||
}
|
||||
} else if (shouldShowOption) {
|
||||
if (peerIdsToReportSpam) {
|
||||
const global = getGlobal();
|
||||
const peerIdList = peerIdsToReportSpam.filter((option) => !Number.isNaN(Number(option)));
|
||||
const messageList = messageIds!.reduce<Record<string, number[]>>((acc, msgId) => {
|
||||
const peer = selectSenderFromMessage(global, chat.id, msgId);
|
||||
if (peer && peerIdList.includes(peer.id)) {
|
||||
if (!acc[peer.id]) {
|
||||
acc[peer.id] = [];
|
||||
}
|
||||
acc[peer.id].push(Number(msgId));
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
handleReportSpam(messageList);
|
||||
}
|
||||
|
||||
if (chosenDeleteOption) {
|
||||
const filteredMessageIdList = filterMessageIdByUserId(chosenDeleteOption, messageIdList!);
|
||||
if (peerIdsToDeleteAll) {
|
||||
const peerIdList = peerIdsToDeleteAll.filter((option) => !Number.isNaN(Number(option)));
|
||||
handleDeleteAllPeerMessages(peerIdList);
|
||||
}
|
||||
|
||||
if (peerIdsToBan && !havePermissionChanged) {
|
||||
const peerIdList = peerIdsToBan.filter((option) => !Number.isNaN(Number(option)));
|
||||
handleDeleteMember(peerIdList);
|
||||
const filteredMessageIdList = filterMessageIdByPeerId(peerIdList, messageIds!);
|
||||
handleDeleteMessages(filteredMessageIdList);
|
||||
}
|
||||
|
||||
if (chosenBanOption && !havePermissionChanged && message) {
|
||||
const filteredUserIdList = chosenBanOption.filter((userId) => messageIds?.some((msgId) => {
|
||||
const senderPeer = selectSenderFromMessage(getGlobal(), chat.id, msgId);
|
||||
return senderPeer && senderPeer.id === userId;
|
||||
}));
|
||||
handleDeleteMember(filteredUserIdList);
|
||||
deleteMessages({
|
||||
messageIds: [message.id],
|
||||
shouldDeleteForAll: false,
|
||||
});
|
||||
if (peerIdsToBan && havePermissionChanged) {
|
||||
const peerIdList = peerIdsToBan.filter((option) => !Number.isNaN(Number(option)));
|
||||
handleUpdateChatMemberBannedRights(peerIdList);
|
||||
}
|
||||
|
||||
if (chosenBanOption && havePermissionChanged) {
|
||||
const filteredUserIdList = chosenBanOption.filter((userId) => messageIds?.some((msgId) => {
|
||||
const senderPeer = selectSenderFromMessage(getGlobal(), chat.id, msgId);
|
||||
return senderPeer && senderPeer.id === userId;
|
||||
}));
|
||||
handleUpdateChatMemberBannedRights(filteredUserIdList);
|
||||
if (!peerIdsToReportSpam || !peerIdsToDeleteAll || !peerIdsToBan) {
|
||||
deleteMessages({ messageIds, shouldDeleteForAll });
|
||||
}
|
||||
} else {
|
||||
deleteMessages({
|
||||
messageIds,
|
||||
shouldDeleteForAll: false,
|
||||
});
|
||||
deleteMessages({ messageIds, shouldDeleteForAll });
|
||||
}
|
||||
|
||||
closeDeleteMessageModal();
|
||||
exitMessageSelectMode();
|
||||
});
|
||||
|
||||
const handleDeleteOptionChange = useLastCallback((options: string[]) => {
|
||||
setChosenDeleteOption(options);
|
||||
});
|
||||
|
||||
const handleBanOptionChange = useLastCallback((options: string[]) => {
|
||||
setChosenBanOptions(options);
|
||||
});
|
||||
|
||||
const handleSpanOptionChange = useLastCallback((options: string[]) => {
|
||||
setChosenSpanOptions(options);
|
||||
});
|
||||
|
||||
const handleClose = useLastCallback(() => {
|
||||
const onCloseHandler = useLastCallback(() => {
|
||||
closeDeleteMessageModal();
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen && prevIsOpen) {
|
||||
setChosenSpanOptions(undefined);
|
||||
setChosenDeleteOption(undefined);
|
||||
setChosenBanOptions(undefined);
|
||||
setPeerIdsToReportSpam(undefined);
|
||||
setPeerIdsToDeleteAll(undefined);
|
||||
setPeerIdsToBan(undefined);
|
||||
setShouldDeleteForAll(true);
|
||||
setIsMediaDropdownOpen(false);
|
||||
setIsAdditionalOptionsVisible(false);
|
||||
resetPermissions();
|
||||
@ -300,14 +341,19 @@ const DeleteMessageModal: FC<OwnProps & StateProps> = ({
|
||||
|
||||
function renderHeader() {
|
||||
return (
|
||||
<div className={shouldShowOptions && styles.container} dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{shouldShowOptions && (
|
||||
<Avatar
|
||||
<div
|
||||
className={shouldShowOption && styles.container}
|
||||
dir={oldLang.isRtl ? 'rtl' : undefined}
|
||||
>
|
||||
{shouldShowOption && (
|
||||
<AvatarList
|
||||
size="small"
|
||||
peer={user!}
|
||||
peers={peerList}
|
||||
/>
|
||||
)}
|
||||
<h3 className={shouldShowOptions ? styles.title : styles.singleTitle}>{lang('DeleteSingleMessagesTitle')}</h3>
|
||||
<h3 className={buildClassName(shouldShowOption ? styles.title : styles.singleTitle)}>
|
||||
{oldLang('Chat.DeleteMessagesConfirmation', messageIds?.length)}
|
||||
</h3>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -317,19 +363,24 @@ const DeleteMessageModal: FC<OwnProps & StateProps> = ({
|
||||
<div className={styles.options}>
|
||||
<CheckboxGroup
|
||||
options={ACTION_SPAM_OPTION}
|
||||
onChange={handleSpanOptionChange}
|
||||
selected={chosenSpanOption}
|
||||
onChange={setPeerIdsToReportSpam}
|
||||
selected={peerIdsToReportSpam}
|
||||
nestedCheckbox={messageIds && peerList.length >= 2}
|
||||
/>
|
||||
<CheckboxGroup
|
||||
options={ACTION_DELETE_OPTION}
|
||||
onChange={handleDeleteOptionChange}
|
||||
selected={chosenDeleteOption}
|
||||
/>
|
||||
{!isSenderOwner && canBanUsers && (
|
||||
{peerListToDeleteAll?.length > 0 && (
|
||||
<CheckboxGroup
|
||||
options={ACTION_DELETE_OPTION}
|
||||
onChange={setPeerIdsToDeleteAll}
|
||||
selected={peerIdsToDeleteAll}
|
||||
nestedCheckbox={messageIds && peerList.length >= 2}
|
||||
/>
|
||||
)}
|
||||
{peerListToBan?.length > 0 && (
|
||||
<CheckboxGroup
|
||||
options={ACTION_BAN_OPTION}
|
||||
onChange={handleBanOptionChange}
|
||||
selected={chosenBanOption}
|
||||
onChange={setPeerIdsToBan}
|
||||
selected={peerIdsToBan}
|
||||
nestedCheckbox={messageIds && peerList.length >= 2}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@ -342,11 +393,10 @@ const DeleteMessageModal: FC<OwnProps & StateProps> = ({
|
||||
isAdditionalOptionsVisible && styles.restrictionContainerOpen)}
|
||||
>
|
||||
<h3 className={buildClassName(styles.actionTitle, styles.restrictionTitle)}>
|
||||
{lang('UserRestrictionsCanDoUsers', 1)}
|
||||
{oldLang('UserRestrictionsCanDoUsers', peerList.length)}
|
||||
</h3>
|
||||
<PermissionCheckboxList
|
||||
withCheckbox
|
||||
permissionGroup
|
||||
chatId={chat?.id}
|
||||
isMediaDropdownOpen={isMediaDropdownOpen}
|
||||
setIsMediaDropdownOpen={setIsMediaDropdownOpen}
|
||||
@ -361,29 +411,32 @@ const DeleteMessageModal: FC<OwnProps & StateProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (!messageIds) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={handleClose}
|
||||
onEnter={canDeleteForAll ? undefined : handleDeleteMessageForSelf}
|
||||
className="delete"
|
||||
onClose={onCloseHandler}
|
||||
onEnter={canDeleteForAll ? undefined : handleDeleteMessageList}
|
||||
className={styles.root}
|
||||
>
|
||||
<div className={buildClassName(styles.mainContainer, 'custom-scroll')}>
|
||||
<div className={styles.main}>
|
||||
{renderHeader()}
|
||||
{shouldShowOptions && (
|
||||
{shouldShowOption && (
|
||||
<>
|
||||
<p className={styles.actionTitle}>{lang('DeleteAdditionalActions')}</p>
|
||||
<p className={styles.actionTitle}>{oldLang('DeleteAdditionalActions')}</p>
|
||||
{renderAdditionalActionOptions()}
|
||||
{renderPartiallyRestrictedUser()}
|
||||
{
|
||||
chosenBanOption && canBanUsers && chosenBanOption?.length ? (
|
||||
peerIdsToBan && canBanUsers ? (
|
||||
<ListItem
|
||||
narrow
|
||||
className={styles.listItemButton}
|
||||
buttonClassName={styles.button}
|
||||
onClick={toggleAdditionalOptions}
|
||||
>
|
||||
{lang(isAdditionalOptionsVisible ? 'DeleteToggleBanUsers' : 'DeleteToggleRestrictUsers')}
|
||||
{oldLang(isAdditionalOptionsVisible ? 'DeleteToggleBanUsers' : 'DeleteToggleRestrictUsers')}
|
||||
<Icon
|
||||
name={isAdditionalOptionsVisible ? 'up' : 'down'}
|
||||
className={buildClassName(styles.button, 'ml-2')}
|
||||
@ -393,35 +446,35 @@ const DeleteMessageModal: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
</>
|
||||
)}
|
||||
{(chatBot || !shouldShowAdditionalOptions) && (
|
||||
{(canDeleteForAll || chatBot || !shouldShowOption) && (
|
||||
<>
|
||||
<p>{lang('AreYouSureDeleteSingleMessage')}</p>
|
||||
<p>{messageIds.length > 1
|
||||
? lang('AreYouSureDeleteFewMessages') : lang('AreYouSureDeleteSingleMessage')}
|
||||
</p>
|
||||
{willDeleteForCurrentUserOnly && (
|
||||
<p>{lang('lng_delete_for_me_chat_hint', 1, 'i')}</p>
|
||||
<p>{oldLang('lng_delete_for_me_chat_hint', 1, 'i')}</p>
|
||||
)}
|
||||
{willDeleteForAll && (
|
||||
<p>{lang('lng_delete_for_everyone_hint', 1, 'i')}</p>
|
||||
<p>{oldLang('lng_delete_for_everyone_hint', 1, 'i')}</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div className={canDeleteForAll ? 'dialog-buttons-column'
|
||||
: buildClassName('dialog-buttons', isAdditionalOptionsVisible && styles.dialogButtons)}
|
||||
{canDeleteForAll && (
|
||||
<Checkbox
|
||||
className="dialog-checkbox"
|
||||
label={contactName ? renderText(oldLang('DeleteMessagesOptionAlso', contactName))
|
||||
: oldLang('Conversation.DeleteMessagesForEveryone')}
|
||||
checked={shouldDeleteForAll}
|
||||
onCheck={setShouldDeleteForAll}
|
||||
/>
|
||||
)}
|
||||
<div className={buildClassName('dialog-buttons',
|
||||
isMediaDropdownOpen ? styles.dialogButtons : styles.proceedButtons)}
|
||||
>
|
||||
{canDeleteForAll && (
|
||||
<Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteMessageForAll}>
|
||||
{contactName && renderText(lang('Conversation.DeleteMessagesFor', contactName))}
|
||||
{!contactName && lang('Conversation.DeleteMessagesForEveryone')}
|
||||
</Button>
|
||||
)}
|
||||
<Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteMessageForSelf}>
|
||||
{lang(canDeleteForAll ? 'ChatList.DeleteForCurrentUser' : 'Delete')}
|
||||
</Button>
|
||||
<Button
|
||||
className="confirm-dialog-button"
|
||||
isText
|
||||
onClick={handleClose}
|
||||
>{lang('Cancel')}
|
||||
<Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteMessageList}>
|
||||
{shouldShowOption ? oldLang('DeleteProceedBtn') : lang('Delete')}
|
||||
</Button>
|
||||
<Button className="confirm-dialog-button" isText onClick={onCloseHandler}>{oldLang('Cancel')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
@ -433,48 +486,44 @@ export default memo(withGlobal<OwnProps>(
|
||||
const {
|
||||
deleteMessageModal,
|
||||
} = selectTabState(global);
|
||||
const chatId = deleteMessageModal && deleteMessageModal.message?.chatId;
|
||||
const messageIds = deleteMessageModal?.messageIds;
|
||||
const chatId = deleteMessageModal?.chatId;
|
||||
const { canDeleteForAll } = selectCanDeleteSelectedMessages(global, messageIds);
|
||||
const chat = chatId ? selectChat(global, chatId) : undefined;
|
||||
const chatFullInfo = chat && selectChatFullInfo(global, chat.id);
|
||||
const { threadId, type } = selectCurrentMessageList(global) || {};
|
||||
const { canDeleteForAll } = (deleteMessageModal && deleteMessageModal.message && threadId
|
||||
&& selectAllowedMessageActionsSlow(global, deleteMessageModal.message, threadId)) || {};
|
||||
const adminMembersById = chatFullInfo && chatFullInfo?.adminMembersById;
|
||||
const messageIdList = chat && selectCurrentMessageIds(global, chat.id, threadId!, type!);
|
||||
const isGroup = Boolean(chat) && isChatBasicGroup(chat);
|
||||
const linkedChatId = chatFullInfo?.linkedChatId;
|
||||
const isChannel = Boolean(chat) && isChatChannel(chat);
|
||||
const isSuperGroup = Boolean(chat) && isChatSuperGroup(chat);
|
||||
const sender = deleteMessageModal?.message && selectSender(global, deleteMessageModal.message);
|
||||
const isSchedule = deleteMessageModal?.isSchedule;
|
||||
const onConfirm = deleteMessageModal?.onConfirm;
|
||||
const contactName = chat && isUserId(chat.id)
|
||||
? getUserFirstOrLastName(selectUser(global, getPrivateChatUserId(chat)!))
|
||||
: undefined;
|
||||
const isChatWithBot = Boolean(deleteMessageModal && deleteMessageModal.message
|
||||
&& selectBot(global, deleteMessageModal.message.chatId));
|
||||
const chatBot = Boolean(chat && !isSystemBot(chat.id) && selectBot(global, chat.id));
|
||||
const canBanUsers = chat && (chat.isCreator || getHasAdminRight(chat, 'banUsers'));
|
||||
const isOwn = deleteMessageModal && deleteMessageModal.message && isOwnMessage(deleteMessageModal.message);
|
||||
|
||||
const adminMembersById = chatFullInfo?.adminMembersById;
|
||||
const canBanUsers = chat && getHasAdminRight(chat, 'banUsers');
|
||||
const isCreator = chat?.isCreator;
|
||||
const isChatWithBot = chat ? selectIsChatWithBot(global, chat) : undefined;
|
||||
const willDeleteForCurrentUserOnly = (chat && isChatBasicGroup(chat) && !canDeleteForAll) || isChatWithBot;
|
||||
const willDeleteForAll = chat && isChatSuperGroup(chat);
|
||||
const willDeleteForAll = chat && (isChatSuperGroup(chat) || isChannel);
|
||||
|
||||
return {
|
||||
chat,
|
||||
isGroup,
|
||||
isChannel,
|
||||
isSuperGroup,
|
||||
messageIds,
|
||||
currentUserId: global.currentUserId,
|
||||
sender,
|
||||
messageIdList,
|
||||
canDeleteForAll: deleteMessageModal && !deleteMessageModal.isSchedule && canDeleteForAll,
|
||||
canDeleteForAll: !isSchedule && canDeleteForAll,
|
||||
contactName,
|
||||
willDeleteForCurrentUserOnly,
|
||||
willDeleteForAll,
|
||||
adminMembersById,
|
||||
chatBot,
|
||||
isSchedule: deleteMessageModal && deleteMessageModal.isSchedule,
|
||||
message: deleteMessageModal && deleteMessageModal.message,
|
||||
album: deleteMessageModal && deleteMessageModal.album,
|
||||
onConfirm: deleteMessageModal && deleteMessageModal.onConfirm,
|
||||
isOwn,
|
||||
canBanUsers,
|
||||
linkedChatId,
|
||||
isSchedule,
|
||||
isCreator,
|
||||
onConfirm,
|
||||
};
|
||||
},
|
||||
)(DeleteMessageModal));
|
||||
|
||||
@ -2,6 +2,7 @@ import type { FC } from '../../lib/teact/teact';
|
||||
import React, { memo, useMemo } from '../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import type { ApiChat } from '../../api/types';
|
||||
import type { ActiveDownloads, MediaViewerOrigin, MessageListType } from '../../types';
|
||||
import type { IconName } from '../../types/icons';
|
||||
import type { MenuItemProps } from '../ui/MenuItem';
|
||||
@ -16,7 +17,7 @@ import {
|
||||
} from '../../global/helpers';
|
||||
import {
|
||||
selectActiveDownloads,
|
||||
selectAllowedMessageActionsSlow,
|
||||
selectAllowedMessageActionsSlow, selectCurrentChat,
|
||||
selectCurrentMessageList,
|
||||
selectIsChatProtected,
|
||||
selectIsMessageProtected,
|
||||
@ -45,6 +46,7 @@ type StateProps = {
|
||||
isProtected?: boolean;
|
||||
isChatProtected?: boolean;
|
||||
canDelete?: boolean;
|
||||
chat?: ApiChat;
|
||||
canUpdate?: boolean;
|
||||
messageListType?: MessageListType;
|
||||
origin?: MediaViewerOrigin;
|
||||
@ -67,6 +69,7 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
|
||||
item,
|
||||
mediaData,
|
||||
isVideo,
|
||||
chat,
|
||||
isChatProtected,
|
||||
isProtected,
|
||||
canReportAvatar,
|
||||
@ -211,10 +214,11 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
|
||||
const openDeleteModalHandler = useLastCallback(() => {
|
||||
if (item?.type === 'message') {
|
||||
if (item?.type === 'message' && chat) {
|
||||
openDeleteMessageModal({
|
||||
chatId: chat?.id,
|
||||
messageIds: [item.message.id],
|
||||
isSchedule: messageListType === 'scheduled',
|
||||
message: item.message,
|
||||
onConfirm: onBeforeDelete,
|
||||
});
|
||||
} else {
|
||||
@ -394,6 +398,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
const avatarOwner = item?.type === 'avatar' ? item.avatarOwner : undefined;
|
||||
const avatarPhoto = item?.type === 'avatar' && item.profilePhotos.photos[item.mediaIndex];
|
||||
|
||||
const chat = selectCurrentChat(global);
|
||||
const currentMessageList = selectCurrentMessageList(global);
|
||||
const { threadId } = selectCurrentMessageList(global) || {};
|
||||
const isProtected = selectIsMessageProtected(global, message);
|
||||
@ -410,6 +415,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
return {
|
||||
activeDownloads,
|
||||
isProtected,
|
||||
chat,
|
||||
isChatProtected,
|
||||
canDelete,
|
||||
canUpdate,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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));
|
||||
@ -18,7 +18,6 @@ import {
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import captureKeyboardListeners from '../../util/captureKeyboardListeners';
|
||||
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
import usePreviousDeprecated from '../../hooks/usePreviousDeprecated';
|
||||
@ -26,7 +25,6 @@ import useCopySelectedMessages from './hooks/useCopySelectedMessages';
|
||||
|
||||
import Icon from '../common/icons/Icon';
|
||||
import Button from '../ui/Button';
|
||||
import DeleteSelectedMessageModal from './DeleteSelectedMessageModal';
|
||||
|
||||
import './MessageSelectToolbar.scss';
|
||||
|
||||
@ -71,27 +69,35 @@ const MessageSelectToolbar: FC<OwnProps & StateProps> = ({
|
||||
copySelectedMessages,
|
||||
showNotification,
|
||||
reportMessages,
|
||||
openDeleteMessageModal,
|
||||
} = getActions();
|
||||
const lang = useOldLang();
|
||||
|
||||
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag();
|
||||
|
||||
useCopySelectedMessages(isActive);
|
||||
|
||||
const handleExitMessageSelectMode = useLastCallback(() => {
|
||||
exitMessageSelectMode();
|
||||
});
|
||||
|
||||
const handleDelete = useLastCallback(() => {
|
||||
if (!selectedMessageIds || !chat) return;
|
||||
openDeleteMessageModal({
|
||||
chatId: chat.id,
|
||||
messageIds: selectedMessageIds,
|
||||
isSchedule,
|
||||
});
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
return isActive && !isDeleteModalOpen && !isAnyModalOpen
|
||||
return isActive && !isAnyModalOpen
|
||||
? captureKeyboardListeners({
|
||||
onBackspace: canDeleteMessages ? openDeleteModal : undefined,
|
||||
onDelete: canDeleteMessages ? openDeleteModal : undefined,
|
||||
onBackspace: canDeleteMessages ? handleDelete : undefined,
|
||||
onDelete: canDeleteMessages ? handleDelete : undefined,
|
||||
onEsc: handleExitMessageSelectMode,
|
||||
})
|
||||
: undefined;
|
||||
}, [
|
||||
isActive, isDeleteModalOpen, openDeleteModal, handleExitMessageSelectMode, isAnyModalOpen,
|
||||
isActive, handleDelete, handleExitMessageSelectMode, isAnyModalOpen,
|
||||
canDeleteMessages,
|
||||
]);
|
||||
|
||||
@ -181,18 +187,11 @@ const MessageSelectToolbar: FC<OwnProps & StateProps> = ({
|
||||
renderButton('copy', lang('lng_context_copy_selected_items'), handleCopy)
|
||||
)}
|
||||
{canDeleteMessages && (
|
||||
renderButton('delete', lang('EditAdminGroupDeleteMessages'), openDeleteModal, true)
|
||||
renderButton('delete', lang('EditAdminGroupDeleteMessages'), handleDelete, true)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{canDeleteMessages && (
|
||||
<DeleteSelectedMessageModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
isSchedule={isSchedule}
|
||||
onClose={closeDeleteModal}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -211,7 +210,8 @@ export default memo(withGlobal<OwnProps>(
|
||||
const canForward = !isSchedule && chatId ? selectCanForwardMessages(global, chatId, selectedMessageIds) : false;
|
||||
const isShareMessageModalOpen = tabState.isShareMessageModalShown;
|
||||
const isAnyModalOpen = Boolean(isShareMessageModalOpen || tabState.requestedDraft
|
||||
|| tabState.requestedAttachBotInChat || tabState.requestedAttachBotInstall || tabState.reportModal);
|
||||
|| tabState.requestedAttachBotInChat || tabState.requestedAttachBotInstall || tabState.reportModal
|
||||
|| tabState.deleteMessageModal);
|
||||
|
||||
return {
|
||||
chat,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -379,7 +379,14 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
const handleDelete = useLastCallback(() => {
|
||||
setIsMenuOpen(false);
|
||||
closeMenu();
|
||||
openDeleteMessageModal({ isSchedule: messageListType === 'scheduled', album, message });
|
||||
const messageIds = album?.messages
|
||||
? album.messages.map(({ id }) => id)
|
||||
: [message.id];
|
||||
openDeleteMessageModal({
|
||||
chatId: message.chatId,
|
||||
messageIds,
|
||||
isSchedule: messageListType === 'scheduled',
|
||||
});
|
||||
});
|
||||
|
||||
const closePinModal = useLastCallback(() => {
|
||||
|
||||
@ -173,7 +173,7 @@ const ChatReportPane: FC<OwnProps & StateProps> = ({
|
||||
<Icon name="close" />
|
||||
</Button>
|
||||
<ConfirmDialog
|
||||
isOpen={isBlockUserModalOpen}
|
||||
isOpen={true}
|
||||
onClose={closeBlockUserModal}
|
||||
title={lang('BlockUserTitle', user ? getUserFirstOrLastName(user) : getChatTitle(lang, chat!))}
|
||||
text={user
|
||||
|
||||
@ -36,7 +36,7 @@ type StateProps = {
|
||||
members?: ApiChatMember[];
|
||||
};
|
||||
|
||||
const ITEM_HEIGHT = 24 + 20;
|
||||
const ITEM_HEIGHT = 48;
|
||||
const BEFORE_ITEMS_COUNT = 2;
|
||||
const ITEMS_COUNT = 9;
|
||||
|
||||
|
||||
@ -40,7 +40,7 @@ type StateProps = {
|
||||
isFormFullyDisabled?: boolean;
|
||||
};
|
||||
|
||||
const ITEM_HEIGHT = 24 + 32;
|
||||
const ITEM_HEIGHT = 48;
|
||||
const SHIFT_HEIGHT_MINUS = 1;
|
||||
const BEFORE_ITEMS_COUNT = 2;
|
||||
const BEFORE_USER_INFO_HEIGHT = 96;
|
||||
|
||||
@ -269,11 +269,11 @@
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
left: 0.125rem;
|
||||
padding: 0 0.5rem 0 3.375rem;
|
||||
padding: 0 0.5rem 0 3.5rem;
|
||||
background: var(--color-background);
|
||||
|
||||
&--open {
|
||||
transform: translateY(0.25rem);
|
||||
transform: translateY(0.025rem);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -166,7 +166,7 @@
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 3.5rem;
|
||||
left: 1.125rem;
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
}
|
||||
@ -217,7 +217,7 @@
|
||||
align-items: center;
|
||||
gap: 0.3125rem;
|
||||
margin-block: 0;
|
||||
padding: 0 0 0 3.5rem;
|
||||
padding: 0 0 0 4.1875rem;
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
@ -253,7 +253,8 @@
|
||||
}
|
||||
|
||||
&.avatar {
|
||||
padding-inline-start: 5.5rem;
|
||||
padding-inline-start: 3.5rem;
|
||||
margin-inline-start: 3.125rem;
|
||||
}
|
||||
|
||||
.button {
|
||||
|
||||
@ -76,7 +76,7 @@ const CheckboxGroup: FC<OwnProps> = ({
|
||||
});
|
||||
const getCheckedNestedCount = useLastCallback((nestedOptions: IRadioOption[]) => {
|
||||
const checkedCount = nestedOptions?.filter((nestedOption) => values.includes(nestedOption.value)).length;
|
||||
return checkedCount > 0 ? checkedCount : undefined;
|
||||
return checkedCount > 0 ? checkedCount : nestedOptions.length;
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@ -200,7 +200,7 @@
|
||||
}
|
||||
|
||||
.dialog-checkbox {
|
||||
margin: 1rem -1rem;
|
||||
margin: 1rem -1.125rem;
|
||||
}
|
||||
|
||||
.confirm-dialog-button {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -745,7 +745,9 @@ addActionHandler('joinChannel', async (global, actions, payload): Promise<void>
|
||||
});
|
||||
|
||||
addActionHandler('deleteChatUser', (global, actions, payload): ActionReturnType => {
|
||||
const { chatId, userId, tabId = getCurrentTabId() } = payload;
|
||||
const {
|
||||
chatId, userId, shouldRevokeHistory, tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
const chat = selectChat(global, chatId);
|
||||
const user = selectUser(global, userId);
|
||||
if (!chat || !user) {
|
||||
@ -759,7 +761,7 @@ addActionHandler('deleteChatUser', (global, actions, payload): ActionReturnType
|
||||
actions.openChat({ id: undefined, tabId });
|
||||
}
|
||||
|
||||
void callApi('deleteChatUser', { chat, user });
|
||||
void callApi('deleteChatUser', { chat, user, shouldRevokeHistory });
|
||||
});
|
||||
|
||||
addActionHandler('deleteChat', (global, actions, payload): ActionReturnType => {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -25,7 +25,11 @@ import {
|
||||
isMessageLocal, isUserId,
|
||||
} from '../../helpers';
|
||||
import { getMessageReplyInfo, getStoryReplyInfo } from '../../helpers/replies';
|
||||
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
||||
import {
|
||||
addActionHandler,
|
||||
getGlobal,
|
||||
setGlobal,
|
||||
} from '../../index';
|
||||
import {
|
||||
addMessages,
|
||||
addViewportId,
|
||||
@ -659,6 +663,15 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
break;
|
||||
}
|
||||
|
||||
case 'deleteParticipantHistory': {
|
||||
const { chatId, peerId } = update;
|
||||
|
||||
global = getGlobal();
|
||||
deleteParticipantHistory(global, chatId, peerId, actions);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'updateCommonBoxMessages': {
|
||||
const { ids, messageUpdate } = update;
|
||||
|
||||
@ -1081,6 +1094,25 @@ function findLastMessage<T extends GlobalState>(global: T, chatId: string, threa
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function deleteParticipantHistory<T extends GlobalState>(
|
||||
global: T,
|
||||
chatId: string,
|
||||
peerId: string,
|
||||
actions: RequiredGlobalActions,
|
||||
) {
|
||||
const byId = selectChatMessages(global, chatId);
|
||||
|
||||
const messageIds = Object.values(byId).filter((message) => {
|
||||
return message.senderId === peerId;
|
||||
}).map((message) => message.id);
|
||||
|
||||
if (!messageIds.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
deleteMessages(global, chatId, messageIds, actions);
|
||||
}
|
||||
|
||||
export function deleteThread<T extends GlobalState>(
|
||||
global: T,
|
||||
chatId: string,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -410,18 +410,13 @@ export function selectSender<T extends GlobalState>(global: T, message: ApiMessa
|
||||
|
||||
export function getSendersFromSelectedMessages<T extends GlobalState>(
|
||||
global: T,
|
||||
chat: ApiChat | undefined,
|
||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
chatId: string,
|
||||
messageIds: number[],
|
||||
) {
|
||||
const { messageIds: selectedMessageIds } = selectTabState(global, tabId).selectedMessages || {};
|
||||
if (!chat?.id || !selectedMessageIds) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return selectedMessageIds.map((id) => {
|
||||
const message = selectChatMessage(global, chat.id, id);
|
||||
return messageIds.map((id) => {
|
||||
const message = selectChatMessage(global, chatId, id);
|
||||
return message && selectSender(global, message);
|
||||
});
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
export function selectSenderFromMessage<T extends GlobalState>(
|
||||
@ -782,21 +777,19 @@ export function selectAllowedMessageActionsSlow<T extends GlobalState>(
|
||||
};
|
||||
}
|
||||
|
||||
// This selector always returns a new object which can not be safely used in shallow-equal checks
|
||||
export function selectCanDeleteSelectedMessages<T extends GlobalState>(
|
||||
export function selectCanDeleteMessages<T extends GlobalState>(
|
||||
global: T,
|
||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
chatId: string,
|
||||
threadId: ThreadId,
|
||||
messageIds: number[],
|
||||
) {
|
||||
const { messageIds: selectedMessageIds } = selectTabState(global, tabId).selectedMessages || {};
|
||||
const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {};
|
||||
const chatMessages = chatId && selectChatMessages(global, chatId);
|
||||
if (!chatMessages || !selectedMessageIds || !threadId) {
|
||||
const chatMessages = selectChatMessages(global, chatId);
|
||||
|
||||
if (messageIds.length > API_GENERAL_ID_LIMIT) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (selectedMessageIds.length > API_GENERAL_ID_LIMIT) return {};
|
||||
|
||||
const messageActions = selectedMessageIds
|
||||
const messageActions = messageIds
|
||||
.map((id) => chatMessages[id] && selectAllowedMessageActionsSlow(global, chatMessages[id], threadId))
|
||||
.filter(Boolean);
|
||||
|
||||
@ -806,6 +799,21 @@ export function selectCanDeleteSelectedMessages<T extends GlobalState>(
|
||||
};
|
||||
}
|
||||
|
||||
export function selectCanDeleteSelectedMessages<T extends GlobalState>(
|
||||
global: T,
|
||||
messageIds?: number[],
|
||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
) {
|
||||
const { messageIds: selectedMessageIds } = selectTabState(global, tabId).selectedMessages || {};
|
||||
const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {};
|
||||
const messageIdList = messageIds?.length ? messageIds : selectedMessageIds;
|
||||
if (!chatId || !threadId || !messageIdList) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return selectCanDeleteMessages(global, chatId, threadId, messageIdList);
|
||||
}
|
||||
|
||||
export function selectCanReportSelectedMessages<T extends GlobalState>(
|
||||
global: T,
|
||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
|
||||
@ -1668,6 +1668,7 @@ channels.togglePreHistoryHidden#eabbb94c channel:InputChannel enabled:Bool = Upd
|
||||
channels.getGroupsForDiscussion#f5dad378 = messages.Chats;
|
||||
channels.setDiscussionGroup#40582bb2 broadcast:InputChannel group:InputChannel = Bool;
|
||||
channels.getSendAs#dc770ee peer:InputPeer = channels.SendAsPeers;
|
||||
channels.deleteParticipantHistory#367544db channel:InputChannel participant:InputPeer = messages.AffectedHistory;
|
||||
channels.toggleJoinToSend#e4cb9580 channel:InputChannel enabled:Bool = Updates;
|
||||
channels.toggleJoinRequest#4c2985b6 channel:InputChannel enabled:Bool = Updates;
|
||||
channels.reorderUsernames#b45ced1d channel:InputChannel order:Vector<string> = Bool;
|
||||
|
||||
@ -240,6 +240,7 @@
|
||||
"help.getPremiumPromo",
|
||||
"channels.readHistory",
|
||||
"channels.deleteMessages",
|
||||
"channels.deleteParticipantHistory",
|
||||
"channels.getMessages",
|
||||
"channels.getParticipants",
|
||||
"channels.getParticipant",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user