Telegram Web K with changes to work inside I2P
https://web.telegram.i2p/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
427 lines
15 KiB
427 lines
15 KiB
import type { AppMessagesManager } from "../../lib/appManagers/appMessagesManager"; |
|
import type ChatBubbles from "./bubbles"; |
|
import type ChatInput from "./input"; |
|
import { isTouchSupported } from "../../helpers/touchSupport"; |
|
import { blurActiveElement, 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"; |
|
import SetTransition from "../singleTransition"; |
|
import ListenerSetter from "../../helpers/listenerSetter"; |
|
|
|
const MAX_SELECTION_LENGTH = 100; |
|
//const MIN_CLICK_MOVE = 32; // minimum bubble height |
|
|
|
export default class ChatSelection { |
|
public selectedMids: Set<number> = new Set(); |
|
public isSelecting = false; |
|
|
|
private selectionContainer: HTMLElement; |
|
private selectionCountEl: HTMLElement; |
|
public selectionForwardBtn: HTMLElement; |
|
public selectionDeleteBtn: HTMLElement; |
|
|
|
public selectedText: string; |
|
|
|
private listenerSetter: ListenerSetter; |
|
|
|
constructor(private bubbles: ChatBubbles, private input: ChatInput, private appMessagesManager: AppMessagesManager) { |
|
const bubblesContainer = bubbles.bubblesContainer; |
|
this.listenerSetter = bubbles.listenerSetter; |
|
|
|
if(isTouchSupported) { |
|
this.listenerSetter.add(bubblesContainer, 'touchend', (e) => { |
|
if(!this.isSelecting) return; |
|
this.selectedText = getSelectedText(); |
|
}); |
|
return; |
|
} |
|
|
|
this.listenerSetter.add(bubblesContainer, 'mousedown', (e) => { |
|
//console.log('selection mousedown', e); |
|
const bubble = findUpClassName(e.target, 'bubble'); |
|
// LEFT BUTTON |
|
// проверка внизу нужна для того, чтобы не активировать селект если target потомок .bubble |
|
if(e.button != 0 |
|
|| ( |
|
!this.selectedMids.size |
|
&& !(e.target as HTMLElement).classList.contains('bubble') |
|
&& !(e.target as HTMLElement).classList.contains('document-selection') |
|
&& bubble |
|
) |
|
) { |
|
return; |
|
} |
|
|
|
const seen: Set<number> = 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<HTMLElement, true> = new Map(); |
|
let canceledSelection = false; |
|
const onMouseMove = (e: MouseEvent) => { |
|
if(!canceledSelection) { |
|
cancelSelection(); |
|
canceledSelection = true; |
|
} |
|
/* 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, 'grouped-item') || findUpClassName(e.target, 'bubble'); |
|
if(!bubble) { |
|
//console.error('found no bubble', e); |
|
return; |
|
} |
|
|
|
const mid = +bubble.dataset.mid; |
|
if(!mid) return; |
|
|
|
// * cancel selecting if selecting message text |
|
if(e.target != bubble && !(e.target as HTMLElement).classList.contains('document-selection') && selecting === undefined && !this.selectedMids.size) { |
|
this.listenerSetter.removeManual(bubblesContainer, 'mousemove', onMouseMove); |
|
this.listenerSetter.removeManual(document, 'mouseup', onMouseUp, documentListenerOptions); |
|
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.bubbles.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}); |
|
} |
|
|
|
this.listenerSetter.removeManual(bubblesContainer, 'mousemove', onMouseMove); |
|
//bubblesContainer.classList.remove('no-select'); |
|
|
|
// ! CANCEL USER SELECTION ! |
|
cancelSelection(); |
|
}; |
|
|
|
const documentListenerOptions = {once: true}; |
|
this.listenerSetter.add(bubblesContainer, 'mousemove', onMouseMove); |
|
this.listenerSetter.add(document, 'mouseup', onMouseUp, documentListenerOptions); |
|
}); |
|
} |
|
|
|
public toggleBubbleCheckbox(bubble: HTMLElement, show: boolean) { |
|
const hasCheckbox = !!this.getCheckboxInputFromBubble(bubble); |
|
const isGrouped = bubble.classList.contains('is-grouped'); |
|
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) && (!isGrouped || this.isGroupedMidsSelected(mid))) { |
|
checkboxField.input.checked = true; |
|
bubble.classList.add('is-selected'); |
|
} |
|
|
|
if(bubble.classList.contains('document-container')) { |
|
bubble.querySelector('.document, audio-element').append(checkboxField.label); |
|
} else { |
|
bubble.prepend(checkboxField.label); |
|
} |
|
} else if(hasCheckbox) { |
|
this.getCheckboxInputFromBubble(bubble).parentElement.remove(); |
|
} |
|
|
|
if(isGrouped) { |
|
this.bubbles.getBubbleGroupedItems(bubble).forEach(item => this.toggleBubbleCheckbox(item, show)); |
|
} |
|
} |
|
|
|
public getCheckboxInputFromBubble(bubble: HTMLElement): HTMLInputElement { |
|
/* let perf = performance.now(); |
|
let checkbox = bubble.firstElementChild.tagName == 'LABEL' && bubble.firstElementChild.firstElementChild as HTMLInputElement; |
|
console.log('getCheckboxInputFromBubble firstElementChild time:', performance.now() - perf); |
|
|
|
perf = performance.now(); |
|
checkbox = bubble.querySelector('label input'); |
|
console.log('getCheckboxInputFromBubble querySelector time:', performance.now() - perf); */ |
|
/* let perf = performance.now(); |
|
let contains = bubble.classList.contains('document-container'); |
|
console.log('getCheckboxInputFromBubble classList time:', performance.now() - perf); |
|
|
|
perf = performance.now(); |
|
contains = bubble.className.includes('document-container'); |
|
console.log('getCheckboxInputFromBubble className time:', performance.now() - perf); */ |
|
|
|
return bubble.classList.contains('document-container') ? |
|
bubble.querySelector('label input') : |
|
bubble.firstElementChild.tagName == 'LABEL' && bubble.firstElementChild.firstElementChild as HTMLInputElement; |
|
} |
|
|
|
public updateForwardContainer(forceSelection = false) { |
|
if(!this.selectedMids.size && !forceSelection) return; |
|
this.selectionCountEl.innerText = this.selectedMids.size + ' Message' + (this.selectedMids.size == 1 ? '' : 's'); |
|
|
|
let cantForward = !this.selectedMids.size, cantDelete = !this.selectedMids.size; |
|
for(const mid of this.selectedMids.values()) { |
|
const message = this.appMessagesManager.getMessageByPeer(this.bubbles.peerId, mid); |
|
if(!cantForward) { |
|
if(message.action) { |
|
cantForward = true; |
|
} |
|
} |
|
|
|
|
|
if(!cantDelete) { |
|
const canDelete = this.appMessagesManager.canDeleteMessage(this.bubbles.peerId, mid); |
|
if(!canDelete) { |
|
cantDelete = true; |
|
} |
|
} |
|
|
|
if(cantForward && cantDelete) break; |
|
} |
|
|
|
this.selectionForwardBtn.toggleAttribute('disabled', cantForward); |
|
this.selectionDeleteBtn.toggleAttribute('disabled', cantDelete); |
|
} |
|
|
|
public toggleSelection(toggleCheckboxes = true, forceSelection = false) { |
|
const wasSelecting = this.isSelecting; |
|
this.isSelecting = this.selectedMids.size > 0 || forceSelection; |
|
|
|
if(wasSelecting == this.isSelecting) return; |
|
|
|
const bubblesContainer = this.bubbles.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(); |
|
} |
|
}/* else { |
|
if(!wasSelecting) { |
|
bubblesContainer.classList.add('no-select'); |
|
setTimeout(() => { |
|
cancelSelection(); |
|
bubblesContainer.classList.remove('no-select'); |
|
cancelSelection(); |
|
}, 100); |
|
} |
|
} */ |
|
|
|
blurActiveElement(); // * for mobile keyboards |
|
|
|
const forwards = !!this.selectedMids.size || forceSelection; |
|
SetTransition(this.input.rowsWrapper, 'is-centering', forwards, 200); |
|
SetTransition(bubblesContainer, 'is-selecting', forwards, 200, () => { |
|
if(!this.isSelecting) { |
|
this.selectionContainer.remove(); |
|
this.selectionContainer = this.selectionForwardBtn = this.selectionDeleteBtn = null; |
|
this.selectedText = undefined; |
|
} |
|
|
|
window.requestAnimationFrame(() => { |
|
this.bubbles.onScroll(); |
|
}); |
|
}); |
|
|
|
//const chatInput = this.appImManager.chatInput; |
|
|
|
if(this.isSelecting) { |
|
if(!this.selectionContainer) { |
|
this.selectionContainer = document.createElement('div'); |
|
this.selectionContainer.classList.add('selection-container'); |
|
|
|
const btnCancel = ButtonIcon('close', {noRipple: true}); |
|
this.listenerSetter.add(btnCancel, '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.listenerSetter.add(this.selectionForwardBtn, 'click', () => { |
|
new PopupForward(this.bubbles.peerId, [...this.selectedMids], () => { |
|
this.cancelSelection(); |
|
}); |
|
}); |
|
|
|
this.selectionDeleteBtn = Button('btn-primary btn-transparent danger selection-container-delete', {icon: 'delete'}); |
|
this.selectionDeleteBtn.append('Delete'); |
|
this.listenerSetter.add(this.selectionDeleteBtn, 'click', () => { |
|
new PopupDeleteMessages(this.bubbles.peerId, [...this.selectedMids], () => { |
|
this.cancelSelection(); |
|
}); |
|
}); |
|
|
|
this.selectionContainer.append(btnCancel, this.selectionCountEl, this.selectionForwardBtn, this.selectionDeleteBtn); |
|
|
|
this.input.rowsWrapper.append(this.selectionContainer); |
|
} |
|
} |
|
|
|
if(toggleCheckboxes) { |
|
for(const mid in this.bubbles.bubbles) { |
|
const bubble = this.bubbles.bubbles[mid]; |
|
this.toggleBubbleCheckbox(bubble, this.isSelecting); |
|
} |
|
} |
|
|
|
if(forceSelection) { |
|
this.updateForwardContainer(forceSelection); |
|
} |
|
} |
|
|
|
public cancelSelection = () => { |
|
for(const mid of this.selectedMids) { |
|
const mounted = this.bubbles.getMountedBubble(mid); |
|
if(mounted) { |
|
//this.toggleByBubble(mounted.message.grouped_id ? mounted.bubble.querySelector(`.grouped-item[data-mid="${mid}"]`) : mounted.bubble); |
|
this.toggleByBubble(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 isGroupedBubbleSelected(bubble: HTMLElement) { |
|
const groupedCheckboxInput = this.getCheckboxInputFromBubble(bubble); |
|
return groupedCheckboxInput?.checked; |
|
} |
|
|
|
public isGroupedMidsSelected(mid: number) { |
|
const mids = this.appMessagesManager.getMidsByMid(this.bubbles.peerId, 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 isGrouped = bubble.classList.contains('is-grouped'); |
|
if(isGrouped) { |
|
if(!this.isGroupedBubbleSelected(bubble)) { |
|
const mids = this.appMessagesManager.getMidsByMid(this.bubbles.peerId, mid); |
|
mids.forEach(mid => this.selectedMids.delete(mid)); |
|
} |
|
|
|
this.bubbles.getBubbleGroupedItems(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 isGroupedItem = bubble.classList.contains('grouped-item'); |
|
if(isGroupedItem) { |
|
const groupContainer = findUpClassName(bubble, 'bubble'); |
|
const isGroupedSelected = this.isGroupedBubbleSelected(groupContainer); |
|
const isGroupedMidsSelected = this.isGroupedMidsSelected(mid); |
|
|
|
const willChange = isGroupedMidsSelected || isGroupedSelected; |
|
if(willChange) { |
|
this.updateBubbleSelection(groupContainer, isGroupedMidsSelected); |
|
} |
|
} |
|
|
|
this.updateBubbleSelection(bubble, !found); |
|
}; |
|
} |