/* * https://github.com/morethanwords/tweb * Copyright (C) 2019-2021 Eduard Kuzmenko * https://github.com/morethanwords/tweb/blob/master/LICENSE */ import type { MyDocument } from "../../lib/appManagers/appDocsManager"; import type Chat from "./chat"; import IS_TOUCH_SUPPORTED from "../../environment/touchSupport"; import ButtonMenu, { ButtonMenuItemOptions } from "../buttonMenu"; import PopupDeleteMessages from "../popups/deleteMessages"; import PopupForward from "../popups/forward"; import PopupPinMessage from "../popups/unpinMessage"; import { copyTextToClipboard } from "../../helpers/clipboard"; import PopupSendNow from "../popups/sendNow"; import { toast } from "../toast"; import I18n, { i18n, LangPackKey } from "../../lib/langPack"; import findUpClassName from "../../helpers/dom/findUpClassName"; import cancelEvent from "../../helpers/dom/cancelEvent"; import { attachClickEvent, simulateClickEvent } from "../../helpers/dom/clickEvent"; import isSelectionEmpty from "../../helpers/dom/isSelectionEmpty"; import { Message, Poll, Chat as MTChat, MessageMedia, AvailableReaction } from "../../layer"; import PopupReportMessages from "../popups/reportMessages"; import assumeType from "../../helpers/assumeType"; import PopupSponsored from "../popups/sponsored"; import ListenerSetter from "../../helpers/listenerSetter"; import { getMiddleware } from "../../helpers/middleware"; import PeerTitle from "../peerTitle"; import StackedAvatars from "../stackedAvatars"; import { IS_APPLE } from "../../environment/userAgent"; import PopupReactedList from "../popups/reactedList"; import { ChatReactionsMenu, REACTION_CONTAINER_SIZE } from "./reactionsMenu"; import getPeerId from "../../lib/appManagers/utils/peers/getPeerId"; import getServerMessageId from "../../lib/appManagers/utils/messageId/getServerMessageId"; import { AppManagers } from "../../lib/appManagers/managers"; import positionMenu, { MenuPositionPadding } from "../../helpers/positionMenu"; import contextMenuController from "../../helpers/contextMenuController"; import { attachContextMenuListener } from "../../helpers/dom/attachContextMenuListener"; import filterAsync from "../../helpers/array/filterAsync"; import appDownloadManager from "../../lib/appManagers/appDownloadManager"; import { SERVICE_PEER_ID } from "../../lib/mtproto/mtproto_config"; export default class ChatContextMenu { private buttons: (ButtonMenuItemOptions & {verify: () => boolean | Promise, notDirect?: () => boolean, withSelection?: true, isSponsored?: true})[]; private element: HTMLElement; private isSelectable: boolean; private isSelected: boolean; private target: HTMLElement; private isTargetAGroupedItem: boolean; private isTextSelected: boolean; private isAnchorTarget: boolean; private isUsernameTarget: boolean; private isSponsored: boolean; private isOverBubble: boolean; private peerId: PeerId; private mid: number; private message: Message.message | Message.messageService; private noForwards: boolean; private reactionsMenu: ChatReactionsMenu; private listenerSetter: ListenerSetter; private attachListenerSetter: ListenerSetter; private viewerPeerId: PeerId; private middleware: ReturnType; private canOpenReactedList: boolean; constructor( private chat: Chat, private managers: AppManagers ) { this.listenerSetter = new ListenerSetter(); this.attachListenerSetter = new ListenerSetter(); this.middleware = getMiddleware(); } public attachTo(element: HTMLElement) { this.attachListenerSetter.removeAll(); if(IS_TOUCH_SUPPORTED/* && false */) { attachClickEvent(element, (e) => { if(this.chat.selection.isSelecting) { return; } this.chat.log('touchend', e); const badSelectors = [ '.name', '.peer-title', '.reply', '.document', 'audio-element', 'avatar-element', 'a', '.bubble-beside-button', 'replies-element', '[data-saved-from]:not(.bubble)', 'poll-element', 'attachment' ]; let good = !(e.target as HTMLElement).closest(badSelectors.join(', ')); if(good) { cancelEvent(e); //onContextMenu((e as TouchEvent).changedTouches[0]); // onContextMenu((e as TouchEvent).changedTouches ? (e as TouchEvent).changedTouches[0] : e as MouseEvent); this.onContextMenu(e); } }, {listenerSetter: this.attachListenerSetter}); } else attachContextMenuListener(element, this.onContextMenu, this.attachListenerSetter); } private onContextMenu = (e: MouseEvent | Touch | TouchEvent) => { let bubble: HTMLElement, contentWrapper: HTMLElement; try { contentWrapper = findUpClassName(e.target, 'bubble-content-wrapper'); bubble = contentWrapper ? contentWrapper.parentElement : findUpClassName(e.target, 'bubble'); } catch(e) {} // ! context menu click by date bubble (there is no pointer-events) if(!bubble || bubble.classList.contains('bubble-first')) return; let element = this.element; if(e instanceof MouseEvent || e.hasOwnProperty('preventDefault')) (e as any).preventDefault(); if(element && element.classList.contains('active')) { return false; } if(e instanceof MouseEvent || e.hasOwnProperty('cancelBubble')) (e as any).cancelBubble = true; let mid = +bubble.dataset.mid; if(!mid) return; const r = async() => { const isSponsored = this.isSponsored = mid < 0; this.isSelectable = this.chat.selection.canSelectBubble(bubble); this.peerId = this.chat.peerId; //this.msgID = msgID; this.target = e.target as HTMLElement; this.isTextSelected = !isSelectionEmpty(); this.isAnchorTarget = this.target.tagName === 'A' && ( (this.target as HTMLAnchorElement).target === '_blank' || this.target.classList.contains('anchor-url') ); this.isUsernameTarget = this.target.tagName === 'A' && this.target.classList.contains('mention'); // * если открыть контекстное меню для альбома не по бабблу, и последний элемент не выбран, чтобы показать остальные пункты if(this.chat.selection.isSelecting && !contentWrapper) { if(isSponsored) { return; } const mids = await this.chat.getMidsByMid(mid); if(mids.length > 1) { const selectedMid = this.chat.selection.isMidSelected(this.peerId, mid) ? mid : mids.find((mid) => this.chat.selection.isMidSelected(this.peerId, mid)); if(selectedMid) { mid = selectedMid; } } } this.isOverBubble = !!contentWrapper; const groupedItem = findUpClassName(this.target, 'grouped-item'); this.isTargetAGroupedItem = !!groupedItem; if(groupedItem) { this.mid = +groupedItem.dataset.mid; } else { this.mid = mid; } this.isSelected = this.chat.selection.isMidSelected(this.peerId, this.mid); this.message = await this.chat.getMessage(this.mid); this.noForwards = !isSponsored && !(await this.managers.appMessagesManager.canForward(this.message)); this.viewerPeerId = undefined; this.canOpenReactedList = undefined; const initResult = await this.init(); if(!initResult) { return; } element = initResult.element; const {cleanup, destroy, menuPadding, reactionsMenu, reactionsMenuPosition} = initResult; let isReactionsMenuVisible = false; if(reactionsMenu) { const className = 'is-visible'; isReactionsMenuVisible = reactionsMenu.container.classList.contains(className); if(isReactionsMenuVisible) reactionsMenu.container.classList.remove(className); if(reactionsMenuPosition === 'horizontal') { const offsetSize = element[/* reactionsMenuPosition === 'vertical' ? 'offsetHeight' : */'offsetWidth']; // if(reactionsMenu.scrollable.container.scrollWidth > offsetWidth) { const INNER_CONTAINER_PADDING = 8; const visibleLength = (offsetSize - INNER_CONTAINER_PADDING) / REACTION_CONTAINER_SIZE; const nextVisiblePart = visibleLength % 1; const MIN_NEXT_VISIBLE_PART = 0.65; if(nextVisiblePart < MIN_NEXT_VISIBLE_PART) { const minSize = (offsetSize + (MIN_NEXT_VISIBLE_PART - nextVisiblePart) * REACTION_CONTAINER_SIZE) | 0; element.style[/* reactionsMenuPosition === 'vertical' ? 'minHeight' : */'minWidth'] = minSize + 'px'; } // } } } const side: 'left' | 'right' = bubble.classList.contains('is-in') ? 'left' : 'right'; //bubble.parentElement.append(element); //appImManager.log('contextmenu', e, bubble, side); positionMenu((e as TouchEvent).touches ? (e as TouchEvent).touches[0] : e as MouseEvent, element, side, menuPadding); if(reactionsMenu) { reactionsMenu.widthContainer.style.top = element.style.top; reactionsMenu.widthContainer.style.left = element.style.left; reactionsMenu.widthContainer.style.setProperty('--menu-width', element[reactionsMenuPosition === 'vertical' ? 'offsetHeight' : 'offsetWidth'] + 'px'); element.parentElement.append(reactionsMenu.widthContainer); if(isReactionsMenuVisible) void reactionsMenu.container.offsetLeft; // reflow } contextMenuController.openBtnMenu(element, () => { if(reactionsMenu) { reactionsMenu.container.classList.remove('is-visible'); } this.mid = 0; this.peerId = undefined; this.target = null; this.viewerPeerId = undefined; this.canOpenReactedList = undefined; cleanup(); setTimeout(() => { destroy(); }, 300); }); if(isReactionsMenuVisible) { reactionsMenu.container.classList.add('is-visible'); } }; r(); }; public cleanup() { this.listenerSetter.removeAll(); this.reactionsMenu && this.reactionsMenu.cleanup(); this.middleware.clean(); } public destroy() { this.cleanup(); this.attachListenerSetter.removeAll(); } private async filterButtons(buttons: ChatContextMenu['buttons']) { if(this.isSponsored) { return buttons.filter((button) => { return button.isSponsored; }); } else { return filterAsync(buttons, async(button) => { let good: boolean; //if((appImManager.chatSelection.isSelecting && !button.withSelection) || (button.withSelection && !appImManager.chatSelection.isSelecting)) { if(this.chat.selection.isSelecting && !button.withSelection) { good = false; } else { good = this.isOverBubble || IS_TOUCH_SUPPORTED || true ? await button.verify() : button.notDirect && await button.verify() && button.notDirect(); } return !!good; }); } } private setButtons() { this.buttons = [{ icon: 'send2', text: 'MessageScheduleSend', onClick: this.onSendScheduledClick, verify: () => this.chat.type === 'scheduled' && !this.message.pFlags.is_outgoing }, { icon: 'send2', text: 'Message.Context.Selection.SendNow', onClick: this.onSendScheduledClick, verify: () => this.chat.type === 'scheduled' && this.isSelected && !this.chat.selection.selectionSendNowBtn.hasAttribute('disabled'), notDirect: () => true, withSelection: true }, { icon: 'schedule', text: 'MessageScheduleEditTime', onClick: () => { this.chat.input.scheduleSending(() => { assumeType(this.message); this.managers.appMessagesManager.editMessage(this.message, this.message.message, { scheduleDate: this.chat.input.scheduleDate, entities: this.message.entities }); this.chat.input.onMessageSent(false, false); }, new Date(this.message.date * 1000)); }, verify: () => this.chat.type === 'scheduled' }, { icon: 'reply', text: 'Reply', onClick: this.onReplyClick, verify: async() => await this.chat.canSend() && !this.message.pFlags.is_outgoing && !!this.chat.input.messageInput && this.chat.type !== 'scheduled'/* , cancelEvent: true */ }, { icon: 'edit', text: 'Edit', onClick: this.onEditClick, verify: async() => (await this.managers.appMessagesManager.canEditMessage(this.message, 'text')) && !!this.chat.input.messageInput }, { icon: 'copy', text: 'Copy', onClick: this.onCopyClick, verify: () => !this.noForwards && !!(this.message as Message.message).message && !this.isTextSelected && (!this.isAnchorTarget || (this.message as Message.message).message !== this.target.innerText) }, { icon: 'copy', text: 'Chat.CopySelectedText', onClick: this.onCopyClick, verify: () => !this.noForwards && !!(this.message as Message.message).message && this.isTextSelected }, { icon: 'copy', text: 'Message.Context.Selection.Copy', onClick: this.onCopyClick, verify: async() => { if(!this.isSelected || this.noForwards) { return false; } for(const [peerId, mids] of this.chat.selection.selectedMids) { for(const mid of mids) { const message = (await this.managers.appMessagesManager.getMessageByPeer(peerId, mid)) as Message.message; if(!!message.message) { return true; } } } return false; }, notDirect: () => true, withSelection: true }, { icon: 'copy', text: 'CopyLink', onClick: this.onCopyAnchorLinkClick, verify: () => this.isAnchorTarget, withSelection: true }, { icon: 'copy', text: 'Text.Context.Copy.Username', onClick: () => { copyTextToClipboard(this.target.innerHTML); }, verify: () => this.isUsernameTarget, withSelection: true }, { icon: 'copy', text: 'Text.Context.Copy.Hashtag', onClick: () => { copyTextToClipboard(this.target.innerHTML); }, verify: () => this.target.classList.contains('anchor-hashtag'), withSelection: true }, { icon: 'link', text: 'MessageContext.CopyMessageLink1', onClick: this.onCopyLinkClick, verify: async() => await this.managers.appPeersManager.isChannel(this.peerId) && !this.message.pFlags.is_outgoing }, { icon: 'pin', text: 'Message.Context.Pin', onClick: this.onPinClick, verify: async() => !this.message.pFlags.is_outgoing && this.message._ !== 'messageService' && !this.message.pFlags.pinned && await this.managers.appPeersManager.canPinMessage(this.peerId) && this.chat.type !== 'scheduled', }, { icon: 'unpin', text: 'Message.Context.Unpin', onClick: this.onUnpinClick, verify: async() => (this.message as Message.message).pFlags.pinned && await this.managers.appPeersManager.canPinMessage(this.peerId), }, { icon: 'download', text: 'MediaViewer.Context.Download', onClick: () => { appDownloadManager.downloadToDisc({media: (this.message as any).media.document}); }, verify: () => { if(this.message.pFlags.is_outgoing) { return false; } const doc: MyDocument = ((this.message as Message.message).media as MessageMedia.messageMediaDocument)?.document as any; if(!doc) return false; let hasTarget = !!IS_TOUCH_SUPPORTED; const isGoodType = !doc.type || !(['gif', 'video', 'sticker'] as MyDocument['type'][]).includes(doc.type); if(isGoodType) hasTarget = hasTarget || !!findUpClassName(this.target, 'document') || !!findUpClassName(this.target, 'audio'); return isGoodType && hasTarget; } }, { icon: 'checkretract', text: 'Chat.Poll.Unvote', onClick: this.onRetractVote, verify: () => { const poll = (this.message as any).media?.poll as Poll; return poll && poll.chosenIndexes.length && !poll.pFlags.closed && !poll.pFlags.quiz; }/* , cancelEvent: true */ }, { icon: 'stop', text: 'Chat.Poll.Stop', onClick: this.onStopPoll, verify: async() => { const poll = (this.message as any).media?.poll; return await this.managers.appMessagesManager.canEditMessage(this.message, 'poll') && poll && !poll.pFlags.closed && !this.message.pFlags.is_outgoing; }/* , cancelEvent: true */ }, { icon: 'forward', text: 'Forward', onClick: this.onForwardClick, // let forward the message if it's outgoing but not ours (like a changelog) verify: () => !this.noForwards && this.chat.type !== 'scheduled' && (!this.message.pFlags.is_outgoing || this.message.fromId === SERVICE_PEER_ID) && this.message._ !== 'messageService' }, { icon: 'forward', text: 'Message.Context.Selection.Forward', onClick: this.onForwardClick, verify: () => this.chat.selection.selectionForwardBtn && this.isSelected && !this.chat.selection.selectionForwardBtn.hasAttribute('disabled'), notDirect: () => true, withSelection: true }, { icon: 'flag', text: 'ReportChat', onClick: () => { new PopupReportMessages(this.peerId, [this.mid]); }, verify: async() => !this.message.pFlags.out && this.message._ === 'message' && !this.message.pFlags.is_outgoing && await this.managers.appPeersManager.isChannel(this.peerId), notDirect: () => true, withSelection: true }, { icon: 'select', text: 'Message.Context.Select', onClick: this.onSelectClick, verify: () => !(this.message as Message.messageService).action && !this.isSelected && this.isSelectable, notDirect: () => true, withSelection: true }, { icon: 'select', text: 'Message.Context.Selection.Clear', onClick: this.onClearSelectionClick, verify: () => this.isSelected, notDirect: () => true, withSelection: true }, { onClick: () => { if(this.viewerPeerId) { this.chat.appImManager.setInnerPeer({ peerId: this.viewerPeerId }); } else if(this.canOpenReactedList) { new PopupReactedList(this.message as Message.message); } else { return false; } }, verify: async() => !this.peerId.isUser() && (!!(this.message as Message.message).reactions?.recent_reactions?.length || await this.managers.appMessagesManager.canViewMessageReadParticipants(this.message)), notDirect: () => true }, { icon: 'delete danger', text: 'Delete', onClick: this.onDeleteClick, verify: async() => this.managers.appMessagesManager.canDeleteMessage(this.message) }, { icon: 'delete danger', text: 'Message.Context.Selection.Delete', onClick: this.onDeleteClick, verify: () => this.isSelected && !this.chat.selection.selectionDeleteBtn.hasAttribute('disabled'), notDirect: () => true, withSelection: true }, { icon: 'info', text: 'Chat.Message.Sponsored.What', onClick: () => { new PopupSponsored(); }, verify: () => false, isSponsored: true }]; } private async init() { this.cleanup(); this.setButtons(); const filteredButtons = await this.filterButtons(this.buttons); if(!filteredButtons.length) { return; } const element = this.element = ButtonMenu(filteredButtons, this.listenerSetter); element.id = 'bubble-contextmenu'; element.classList.add('contextmenu'); const viewsButton = filteredButtons.find((button) => !button.icon); if(viewsButton) { const reactions = (this.message as Message.message).reactions; const recentReactions = reactions?.recent_reactions; const isViewingReactions = !!recentReactions?.length; const participantsCount = await this.managers.appMessagesManager.canViewMessageReadParticipants(this.message) ? ((await this.managers.appPeersManager.getPeer(this.peerId)) as MTChat.chat).participants_count : undefined; const reactedLength = reactions ? reactions.results.reduce((acc, r) => acc + r.count, 0) : undefined; viewsButton.element.classList.add('tgico-' + (isViewingReactions ? 'reactions' : 'checks')); const i18nElem = new I18n.IntlElement({ key: isViewingReactions ? ( participantsCount === undefined ? 'Chat.Context.ReactedFast' : 'Chat.Context.Reacted' ) : 'NobodyViewed', args: isViewingReactions ? ( participantsCount === undefined ? [reactedLength] : [participantsCount, participantsCount] ) : undefined, element: viewsButton.textElement }); let fakeText: HTMLElement; if(isViewingReactions) { if(participantsCount === undefined) { fakeText = i18n('Chat.Context.ReactedFast', [reactedLength]); } else { fakeText = i18n( recentReactions.length === participantsCount ? 'Chat.Context.ReactedFast' : 'Chat.Context.Reacted', [recentReactions.length, participantsCount] ); } } else { fakeText = i18n('Loading'); } fakeText.classList.add('btn-menu-item-text-fake'); viewsButton.element.append(fakeText); const AVATAR_SIZE = 22; const MAX_AVATARS = 3; const PADDING_PER_AVATAR = 1.125; i18nElem.element.style.visibility = 'hidden'; i18nElem.element.style.paddingRight = isViewingReactions ? PADDING_PER_AVATAR * Math.min(MAX_AVATARS, recentReactions.length) + 'rem' : '1rem'; const middleware = this.middleware.get(); this.managers.appMessagesManager.getMessageReactionsListAndReadParticipants(this.message as Message.message).then((result) => { if(!middleware()) { return; } if(fakeText) { fakeText.remove(); } const reactions = result.combined; const reactedLength = participantsCount === undefined ? result.reactionsCount : ( isViewingReactions ? reactions.filter((reaction) => reaction.reaction).length : reactions.length ); let fakeElem: HTMLElement; if(reactions.length === 1) { fakeElem = new PeerTitle({ peerId: reactions[0].peerId, onlyFirstName: true, dialog: false, }).element; if(!isViewingReactions || result.readParticipants.length <= 1) { this.viewerPeerId = reactions[0].peerId; } } else if(isViewingReactions) { const isFull = reactedLength === reactions.length || participantsCount === undefined; fakeElem = i18n( isFull ? 'Chat.Context.ReactedFast' : 'Chat.Context.Reacted', isFull ? [reactedLength] : [reactedLength, reactions.length] ); } else { if(!reactions.length) { i18nElem.element.style.visibility = ''; } else { fakeElem = i18n('MessageSeen', [reactions.length]); } } if(fakeElem) { fakeElem.style.paddingRight = PADDING_PER_AVATAR * Math.min(MAX_AVATARS, reactedLength) + 'rem'; fakeElem.classList.add('btn-menu-item-text-fake'); viewsButton.element.append(fakeElem); } if(reactions.length) { const avatars = new StackedAvatars({avatarSize: AVATAR_SIZE}); avatars.render(recentReactions ? recentReactions.map((r) => getPeerId(r.peer_id)) : reactions.map((reaction) => reaction.peerId)); viewsButton.element.append(avatars.container); // if(reactions.length > 1) { // if(isViewingReactions) { this.canOpenReactedList = true; // } } }); } let menuPadding: MenuPositionPadding; let reactionsMenu: ChatReactionsMenu; let reactionsMenuPosition: 'horizontal' | 'vertical'; if(this.message._ === 'message' && !this.chat.selection.isSelecting && !this.message.pFlags.is_outgoing && !this.message.pFlags.is_scheduled) { reactionsMenuPosition = (IS_APPLE || IS_TOUCH_SUPPORTED)/* && false */ ? 'horizontal' : 'vertical'; reactionsMenu = this.reactionsMenu = new ChatReactionsMenu(this.managers, reactionsMenuPosition, this.middleware); reactionsMenu.init(await this.managers.appMessagesManager.getGroupsFirstMessage(this.message)); // element.prepend(reactionsMenu.widthContainer); const size = 36; const margin = 8; const totalSize = size + margin; const paddingLeft = 0, paddingRight = 0; if(reactionsMenuPosition === 'vertical') { menuPadding = { top: paddingLeft, // bottom: 36, // positionMenu will detect it itself somehow left: totalSize }; } else { menuPadding = { top: totalSize, right: paddingRight, left: paddingLeft }; } } this.chat.container.append(element); return { element, cleanup: () => { this.cleanup(); reactionsMenu && reactionsMenu.cleanup(); }, destroy: () => { element.remove(); reactionsMenu && reactionsMenu.widthContainer.remove(); }, menuPadding, reactionsMenu, reactionsMenuPosition }; } private onSendScheduledClick = async() => { if(this.chat.selection.isSelecting) { simulateClickEvent(this.chat.selection.selectionSendNowBtn); } else { new PopupSendNow(this.peerId, await this.chat.getMidsByMid(this.mid)); } }; private onReplyClick = () => { this.chat.input.initMessageReply(this.mid); }; private onEditClick = () => { this.chat.input.initMessageEditing(this.mid); }; private onCopyClick = async() => { if(isSelectionEmpty()) { const mids = this.chat.selection.isSelecting ? [...this.chat.selection.selectedMids.get(this.peerId)].sort((a, b) => a - b) : [this.mid]; const parts: string[] = await Promise.all(mids.map(async(mid) => { const message = (await this.chat.getMessage(mid)) as Message.message; return message?.message ? message.message + '\n' : ''; })); const str = parts.join(''); copyTextToClipboard(str); } else { document.execCommand('copy'); //cancelSelection(); } }; private onCopyAnchorLinkClick = () => { copyTextToClipboard((this.target as HTMLAnchorElement).href); }; private onCopyLinkClick = async() => { let threadMessage: Message.message; const {peerId, mid} = this; const threadId = this.chat.threadId; if(this.chat.type === 'discussion') { threadMessage = (await this.managers.appMessagesManager.getMessageByPeer(peerId, threadId)) as Message.message; } const username = await this.managers.appPeersManager.getPeerUsername(threadMessage ? threadMessage.fromId : peerId); const msgId = getServerMessageId(mid); let url = 'https://t.me/'; let key: LangPackKey; if(username) { url += username + '/' + (threadMessage ? getServerMessageId(threadMessage.fwd_from.channel_post) : msgId); if(threadMessage) url += '?comment=' + msgId; key = 'LinkCopied'; } else { url += 'c/' + peerId.toChatId() + '/' + msgId; if(threadMessage) url += '?thread=' + getServerMessageId(threadMessage.mid); key = 'LinkCopiedPrivateInfo'; } toast(I18n.format(key, true)); copyTextToClipboard(url); }; private onPinClick = () => { new PopupPinMessage(this.peerId, this.mid); }; private onUnpinClick = () => { new PopupPinMessage(this.peerId, this.mid, true); }; private onRetractVote = () => { this.managers.appPollsManager.sendVote(this.message, []); }; private onStopPoll = () => { this.managers.appPollsManager.stopPoll(this.message); }; private onForwardClick = async() => { if(this.chat.selection.isSelecting) { simulateClickEvent(this.chat.selection.selectionForwardBtn); } else { const peerId = this.peerId; const mids = this.isTargetAGroupedItem ? [this.mid] : await this.chat.getMidsByMid(this.mid); new PopupForward({ [peerId]: mids }); } }; private onSelectClick = () => { this.chat.selection.toggleByElement(findUpClassName(this.target, 'grouped-item') || findUpClassName(this.target, 'bubble')); }; private onClearSelectionClick = () => { this.chat.selection.cancelSelection(); }; private onDeleteClick = async() => { if(this.chat.selection.isSelecting) { simulateClickEvent(this.chat.selection.selectionDeleteBtn); } else { new PopupDeleteMessages(this.peerId, this.isTargetAGroupedItem ? [this.mid] : await this.chat.getMidsByMid(this.mid), this.chat.type); } }; }