import { isTouchSupported } from "../../helpers/touchSupport"; import type { AppImManager } from "../../lib/appManagers/appImManager"; import type { AppMessagesManager } from "../../lib/appManagers/appMessagesManager"; import { cancelEvent, cancelSelection, findUpClassName, getSelectedText } from "../../helpers/dom"; import Button from "../button"; import ButtonIcon from "../buttonIcon"; import CheckboxField from "../checkbox"; import PopupDeleteMessages from "../popupDeleteMessages"; import PopupForward from "../popupForward"; import { toast } from "../toast"; const SetTransition = (element: HTMLElement, className: string, forwards: boolean, duration: number, onTransitionEnd?: () => void) => { const timeout = element.dataset.timeout; if(timeout !== undefined) { clearTimeout(+timeout); } if(forwards) { element.classList.add(className); } element.classList.add('animating'); element.classList.toggle('backwards', !forwards); element.dataset.timeout = '' + setTimeout(() => { delete element.dataset.timeout; if(!forwards) { element.classList.remove('backwards', className); } element.classList.remove('animating'); onTransitionEnd && onTransitionEnd(); }, duration); }; const MAX_SELECTION_LENGTH = 100; //const MIN_CLICK_MOVE = 32; // minimum bubble height export default class ChatSelection { public selectedMids: Set = new Set(); public isSelecting = false; private selectionContainer: HTMLElement; private selectionCountEl: HTMLElement; public selectionForwardBtn: HTMLElement; public selectionDeleteBtn: HTMLElement; public selectedText: string; constructor(private appImManager: AppImManager, private appMessagesManager: AppMessagesManager) { const bubblesContainer = appImManager.bubblesContainer; if(isTouchSupported) { bubblesContainer.addEventListener('touchend', (e) => { if(!this.isSelecting) return; this.selectedText = getSelectedText(); }); return; } bubblesContainer.addEventListener('mousedown', (e) => { //console.log('selection mousedown', e); if(e.button != 0 || (!this.selectedMids.size && !(e.target as HTMLElement).classList.contains('bubble'))) { // LEFT BUTTON return; } const seen: Set = new Set(); let selecting: boolean; /* let good = false; const {x, y} = e; */ /* const bubbles = appImManager.bubbles; for(const mid in bubbles) { const bubble = bubbles[mid]; bubble.addEventListener('mouseover', () => { console.log('mouseover'); }, {once: true}); } */ //const foundTargets: Map = new Map(); const onMouseMove = (e: MouseEvent) => { /* if(!good) { if(Math.abs(e.x - x) > MIN_CLICK_MOVE || Math.abs(e.y - y) > MIN_CLICK_MOVE) { good = true; } else { return; } } */ /* if(foundTargets.has(e.target as HTMLElement)) return; foundTargets.set(e.target as HTMLElement, true); */ const bubble = findUpClassName(e.target, 'bubble'); if(!bubble) { console.error('found no bubble', e); return; } const mid = +bubble.dataset.mid; if(!mid) return; if(e.target != bubble && selecting === undefined) { bubblesContainer.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); return; } if(!seen.has(mid)) { const isBubbleSelected = this.selectedMids.has(mid); if(selecting === undefined) { //bubblesContainer.classList.add('no-select'); selecting = !isBubbleSelected; } seen.add(mid); if((selecting && !isBubbleSelected) || (!selecting && isBubbleSelected)) { if(!this.selectedMids.size) { if(seen.size == 2) { [...seen].forEach(mid => { const mounted = this.appImManager.getMountedBubble(mid); if(mounted) { this.toggleByBubble(mounted.bubble); } }) } } else { this.toggleByBubble(bubble); } } } //console.log('onMouseMove', target); }; const onMouseUp = (e: MouseEvent) => { if(seen.size) { window.addEventListener('click', (e) => { cancelEvent(e); }, {capture: true, once: true, passive: false}); } bubblesContainer.removeEventListener('mousemove', onMouseMove); //bubblesContainer.classList.remove('no-select'); // ! CANCEL USER SELECTION ! cancelSelection(); }; bubblesContainer.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp, {once: true}); }); } public toggleBubbleCheckbox(bubble: HTMLElement, show: boolean) { const hasCheckbox = !!this.getCheckboxInputFromBubble(bubble); const isAlbum = bubble.classList.contains('is-album'); if(show) { if(hasCheckbox) return; const checkboxField = CheckboxField('', bubble.dataset.mid, true); checkboxField.label.classList.add('bubble-select-checkbox'); // * if it is a render of new message const mid = +bubble.dataset.mid; if(this.selectedMids.has(mid) && (!isAlbum || this.isAlbumMidsSelected(mid))) { checkboxField.input.checked = true; bubble.classList.add('is-selected'); } bubble.prepend(checkboxField.label); } else if(hasCheckbox) { bubble.firstElementChild.remove(); } if(isAlbum) { this.appImManager.getBubbleAlbumItems(bubble).forEach(item => this.toggleBubbleCheckbox(item, show)); } } public getCheckboxInputFromBubble(bubble: HTMLElement) { return bubble.firstElementChild.tagName == 'LABEL' && bubble.firstElementChild.firstElementChild as HTMLInputElement; } public updateForwardContainer() { if(!this.selectedMids.size) return; this.selectionCountEl.innerText = this.selectedMids.size + ' Message' + (this.selectedMids.size == 1 ? '' : 's'); let cantForward = false, cantDelete = false; for(const mid of this.selectedMids.values()) { const message = this.appMessagesManager.getMessage(mid); if(!cantForward) { if(message.action) { cantForward = true; } } if(!cantDelete) { const canDelete = this.appMessagesManager.canDeleteMessage(mid); if(!canDelete) { cantDelete = true; } } if(cantForward && cantDelete) break; } this.selectionForwardBtn.toggleAttribute('disabled', cantForward); this.selectionDeleteBtn.toggleAttribute('disabled', cantDelete); } public toggleSelection(toggleCheckboxes = true) { const wasSelecting = this.isSelecting; this.isSelecting = this.selectedMids.size > 0; if(wasSelecting == this.isSelecting) return; const bubblesContainer = this.appImManager.bubblesContainer; //bubblesContainer.classList.toggle('is-selecting', !!this.selectedMids.size); /* if(bubblesContainer.classList.contains('is-chat-input-hidden')) { const scrollable = this.appImManager.scrollable; if(scrollable.isScrolledDown) { scrollable.scrollTo(scrollable.scrollHeight, 'top', true, true, 200); } } */ if(!isTouchSupported) { bubblesContainer.classList.toggle('no-select', this.isSelecting); if(wasSelecting) { // ! CANCEL USER SELECTION ! cancelSelection(); } } SetTransition(bubblesContainer, 'is-selecting', !!this.selectedMids.size, 200, () => { if(!this.isSelecting) { this.selectionContainer.remove(); this.selectionContainer = this.selectionForwardBtn = this.selectionDeleteBtn = null; this.selectedText = undefined; } window.requestAnimationFrame(() => { this.appImManager.onScroll(); }); }); //const chatInput = this.appImManager.chatInput; if(this.isSelecting) { if(!this.selectionContainer) { const inputMessageDiv = document.querySelector('.input-message'); this.selectionContainer = document.createElement('div'); this.selectionContainer.classList.add('selection-container'); const btnCancel = ButtonIcon('close', {noRipple: true}); btnCancel.addEventListener('click', this.cancelSelection, {once: true}); this.selectionCountEl = document.createElement('div'); this.selectionCountEl.classList.add('selection-container-count'); this.selectionForwardBtn = Button('btn-primary btn-transparent selection-container-forward', {icon: 'forward'}); this.selectionForwardBtn.append('Forward'); this.selectionForwardBtn.addEventListener('click', () => { new PopupForward([...this.selectedMids], () => { this.cancelSelection(); }); }); this.selectionDeleteBtn = Button('btn-primary btn-transparent danger selection-container-delete', {icon: 'delete'}); this.selectionDeleteBtn.append('Delete'); this.selectionDeleteBtn.addEventListener('click', () => { new PopupDeleteMessages([...this.selectedMids], () => { this.cancelSelection(); }); }); this.selectionContainer.append(btnCancel, this.selectionCountEl, this.selectionForwardBtn, this.selectionDeleteBtn); inputMessageDiv.append(this.selectionContainer); } } if(toggleCheckboxes) { for(const mid in this.appImManager.bubbles) { const bubble = this.appImManager.bubbles[mid]; this.toggleBubbleCheckbox(bubble, this.isSelecting); } } } public cancelSelection = () => { for(const mid of this.selectedMids) { const mounted = this.appImManager.getMountedBubble(mid); if(mounted) { this.toggleByBubble(mounted.message.grouped_id ? mounted.bubble.querySelector(`.album-item[data-mid="${mid}"]`) : mounted.bubble); } /* const bubble = this.appImManager.bubbles[mid]; if(bubble) { this.toggleByBubble(bubble); } */ } this.selectedMids.clear(); this.toggleSelection(); cancelSelection(); }; public cleanup() { this.selectedMids.clear(); this.toggleSelection(false); } private updateBubbleSelection(bubble: HTMLElement, isSelected: boolean) { this.toggleBubbleCheckbox(bubble, true); const input = this.getCheckboxInputFromBubble(bubble); input.checked = isSelected; this.toggleSelection(); this.updateForwardContainer(); SetTransition(bubble, 'is-selected', isSelected, 200); } public isAlbumBubbleSelected(bubble: HTMLElement) { const albumCheckboxInput = this.getCheckboxInputFromBubble(bubble); return albumCheckboxInput?.checked; } public isAlbumMidsSelected(mid: number) { const mids = this.appMessagesManager.getMidsByMid(mid); const selectedMids = mids.filter(mid => this.selectedMids.has(mid)); return mids.length == selectedMids.length; } public toggleByBubble = (bubble: HTMLElement) => { const mid = +bubble.dataset.mid; const isAlbum = bubble.classList.contains('is-album'); if(isAlbum) { if(!this.isAlbumBubbleSelected(bubble)) { const mids = this.appMessagesManager.getMidsByMid(mid); mids.forEach(mid => this.selectedMids.delete(mid)); } this.appImManager.getBubbleAlbumItems(bubble).forEach(this.toggleByBubble); return; } const found = this.selectedMids.has(mid); if(found) { this.selectedMids.delete(mid); } else { const diff = MAX_SELECTION_LENGTH - this.selectedMids.size - 1; if(diff < 0) { toast('Max selection count reached.'); return; /* const it = this.selectedMids.values(); do { const mid = it.next().value; const mounted = this.appImManager.getMountedBubble(mid); if(mounted) { this.toggleByBubble(mounted.bubble); } else { const mids = this.appMessagesManager.getMidsByMid(mid); for(const mid of mids) { this.selectedMids.delete(mid); } } } while(this.selectedMids.size > MAX_SELECTION_LENGTH); */ } this.selectedMids.add(mid); } const isAlbumItem = bubble.classList.contains('album-item'); if(isAlbumItem) { const albumContainer = findUpClassName(bubble, 'bubble'); const isAlbumSelected = this.isAlbumBubbleSelected(albumContainer); const isAlbumMidsSelected = this.isAlbumMidsSelected(mid); const willChange = isAlbumMidsSelected || isAlbumSelected; if(willChange) { this.updateBubbleSelection(albumContainer, isAlbumMidsSelected); } } this.updateBubbleSelection(bubble, !found); }; }