tweb/src/components/chat/selection.ts
morethanwords ca1213c32f Support noforwards
Fix chat date blinking
Fix displaying sent messages to new dialog
Scroll to date bubble if message is bigger than viewport
Fix releasing keyboard by inline helper
Fix clearing self user
Fix displaying sent public poll
Update contacts counter in dialogs placeholder
Improve multiselect animation
Disable lottie icon animations if they're disabled
Fix changing mtproto transport during authorization
2022-01-08 16:52:14 +04:00

973 lines
34 KiB
TypeScript

/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import type { AppMessagesManager, MessagesStorage } from "../../lib/appManagers/appMessagesManager";
import type ChatBubbles from "./bubbles";
import type ChatInput from "./input";
import type Chat from "./chat";
import { IS_TOUCH_SUPPORTED } from "../../environment/touchSupport";
import Button from "../button";
import ButtonIcon from "../buttonIcon";
import CheckboxField from "../checkboxField";
import PopupDeleteMessages from "../popups/deleteMessages";
import PopupForward from "../popups/forward";
import { toast } from "../toast";
import SetTransition from "../singleTransition";
import ListenerSetter from "../../helpers/listenerSetter";
import PopupSendNow from "../popups/sendNow";
import appNavigationController, { NavigationItem } from "../appNavigationController";
import { IS_MOBILE_SAFARI } from "../../environment/userAgent";
import I18n, { i18n, _i18n } from "../../lib/langPack";
import findUpClassName from "../../helpers/dom/findUpClassName";
import blurActiveElement from "../../helpers/dom/blurActiveElement";
import { cancelEvent } from "../../helpers/dom/cancelEvent";
import cancelSelection from "../../helpers/dom/cancelSelection";
import getSelectedText from "../../helpers/dom/getSelectedText";
import rootScope from "../../lib/rootScope";
import { safeAssign } from "../../helpers/object";
import { fastRaf } from "../../helpers/schedulers";
import replaceContent from "../../helpers/dom/replaceContent";
import AppSearchSuper from "../appSearchSuper.";
import isInDOM from "../../helpers/dom/isInDOM";
import { randomLong } from "../../helpers/random";
import { attachContextMenuListener } from "../misc";
import { attachClickEvent, AttachClickOptions } from "../../helpers/dom/clickEvent";
import findUpAsChild from "../../helpers/dom/findUpAsChild";
const accumulateMapSet = (map: Map<any, Set<number>>) => {
return [...map.values()].reduce((acc, v) => acc + v.size, 0);
};
//const MIN_CLICK_MOVE = 32; // minimum bubble height
class AppSelection {
public selectedMids: Map<PeerId, Set<number>> = new Map();
public isSelecting = false;
public selectedText: string;
protected listenerSetter: ListenerSetter;
protected appMessagesManager: AppMessagesManager;
protected isScheduled: boolean;
protected listenElement: HTMLElement;
protected onToggleSelection: (forwards: boolean) => void;
protected onUpdateContainer: (cantForward: boolean, cantDelete: boolean, cantSend: boolean) => void;
protected onCancelSelection: () => void;
protected toggleByMid: (peerId: PeerId, mid: number) => void;
protected toggleByElement: (bubble: HTMLElement) => void;
protected navigationType: NavigationItem['type'];
protected getElementFromTarget: (target: HTMLElement) => HTMLElement;
protected verifyTarget: (e: MouseEvent, target: HTMLElement) => boolean;
protected verifyMouseMoveTarget: (e: MouseEvent, element: HTMLElement, selecting: boolean) => boolean;
protected verifyTouchLongPress: () => boolean;
protected targetLookupClassName: string;
protected lookupBetweenParentClassName: string;
protected lookupBetweenElementsQuery: string;
constructor(options: {
appMessagesManager: AppMessagesManager,
listenElement: HTMLElement,
listenerSetter: ListenerSetter,
getElementFromTarget: AppSelection['getElementFromTarget'],
verifyTarget?: AppSelection['verifyTarget'],
verifyMouseMoveTarget?: AppSelection['verifyMouseMoveTarget'],
verifyTouchLongPress?: AppSelection['verifyTouchLongPress'],
targetLookupClassName: string,
lookupBetweenParentClassName: string,
lookupBetweenElementsQuery: string,
isScheduled?: AppSelection['isScheduled']
}) {
safeAssign(this, options);
this.navigationType = 'multiselect-' + randomLong() as any;
if(IS_TOUCH_SUPPORTED) {
this.listenerSetter.add(this.listenElement)('touchend', () => {
if(!this.isSelecting) return;
this.selectedText = getSelectedText();
});
attachContextMenuListener(this.listenElement, (e) => {
if(this.isSelecting || (this.verifyTouchLongPress && !this.verifyTouchLongPress())) return;
// * these two lines will fix instant text selection on iOS Safari
document.body.classList.add('no-select'); // * need no-select on body because chat-input transforms in channels
this.listenElement.addEventListener('touchend', (e) => {
cancelEvent(e); // ! this one will fix propagation to document loader button, etc
document.body.classList.remove('no-select');
//this.chat.bubbles.onBubblesClick(e);
}, {once: true, capture: true});
cancelSelection();
//cancelEvent(e as any);
const element = this.getElementFromTarget(e.target as HTMLElement);
if(element) {
this.toggleByElement(element);
}
}, this.listenerSetter);
return;
}
const getElementsBetween = (first: HTMLElement, last: HTMLElement) => {
if(first === last) {
return [];
}
const firstRect = first.getBoundingClientRect();
const lastRect = last.getBoundingClientRect();
const difference = (firstRect.top - lastRect.top) || (firstRect.left - lastRect.left);
const isHigher = difference < 0;
const parent = findUpClassName(first, this.lookupBetweenParentClassName);
if(!parent) {
return [];
}
const elements = Array.from(parent.querySelectorAll(this.lookupBetweenElementsQuery)) as HTMLElement[];
let firstIndex = elements.indexOf(first);
let lastIndex = elements.indexOf(last);
if(!isHigher) {
[lastIndex, firstIndex] = [firstIndex, lastIndex];
}
const slice = elements.slice(firstIndex + 1, lastIndex);
// console.log('getElementsBetween', first, last, slice, firstIndex, lastIndex, isHigher);
return slice;
};
this.listenerSetter.add(this.listenElement)('mousedown', (e) => {
//console.log('selection mousedown', e);
const element = findUpClassName(e.target, this.targetLookupClassName);
if(e.button !== 0) {
return;
}
if(this.verifyTarget && !this.verifyTarget(e, element)) {
return;
}
const seen: AppSelection['selectedMids'] = new Map();
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});
} */
let firstTarget = element;
const processElement = (element: HTMLElement, checkBetween = true) => {
const mid = +element.dataset.mid;
if(!mid || !element.dataset.peerId) return;
const peerId = element.dataset.peerId.toPeerId();
if(!isInDOM(firstTarget)) {
firstTarget = element;
}
let seenSet = seen.get(peerId);
if(!seenSet) {
seen.set(peerId, seenSet = new Set());
}
if(!seenSet.has(mid)) {
const isSelected = this.isMidSelected(peerId, mid);
if(selecting === undefined) {
//bubblesContainer.classList.add('no-select');
selecting = !isSelected;
}
seenSet.add(mid);
if((selecting && !isSelected) || (!selecting && isSelected)) {
const seenLength = accumulateMapSet(seen);
if(this.toggleByElement && checkBetween) {
if(seenLength < 2) {
if(findUpAsChild(element, firstTarget)) {
firstTarget = element;
}
}
const elementsBetween = getElementsBetween(firstTarget, element);
// console.log(elementsBetween);
if(elementsBetween.length) {
elementsBetween.forEach(element => {
processElement(element, false);
});
}
}
if(!this.selectedMids.size) {
if(seenLength === 2 && this.toggleByMid) {
for(const [peerId, mids] of seen) {
for(const mid of mids) {
this.toggleByMid(peerId, mid);
}
}
}
} else if(this.toggleByElement) {
this.toggleByElement(element);
}
}
}
};
//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 element = this.getElementFromTarget(e.target as HTMLElement);
if(!element) {
//console.error('found no bubble', e);
return;
}
if(this.verifyMouseMoveTarget && !this.verifyMouseMoveTarget(e, element, selecting)) {
this.listenerSetter.removeManual(this.listenElement, 'mousemove', onMouseMove);
this.listenerSetter.removeManual(document, 'mouseup', onMouseUp, documentListenerOptions);
return;
}
processElement(element);
};
const onMouseUp = (e: MouseEvent) => {
if(seen.size) {
attachClickEvent(window, cancelEvent, {capture: true, once: true, passive: false});
}
this.listenerSetter.removeManual(this.listenElement, 'mousemove', onMouseMove);
//bubblesContainer.classList.remove('no-select');
// ! CANCEL USER SELECTION !
cancelSelection();
};
const documentListenerOptions = {once: true};
this.listenerSetter.add(this.listenElement)('mousemove', onMouseMove);
this.listenerSetter.add(document)('mouseup', onMouseUp, documentListenerOptions);
});
}
protected isElementShouldBeSelected(element: HTMLElement) {
return this.isMidSelected(element.dataset.peerId.toPeerId(), +element.dataset.mid);
}
protected appendCheckbox(element: HTMLElement, checkboxField: CheckboxField) {
element.prepend(checkboxField.label);
}
public toggleElementCheckbox(element: HTMLElement, show: boolean) {
const hasCheckbox = !!this.getCheckboxInputFromElement(element);
if(show) {
if(hasCheckbox) {
return false;
}
const checkboxField = new CheckboxField({
name: element.dataset.mid,
round: true
});
// * if it is a render of new message
if(this.isSelecting) { // ! avoid breaking animation on start
if(this.isElementShouldBeSelected(element)) {
checkboxField.input.checked = true;
element.classList.add('is-selected');
}
}
this.appendCheckbox(element, checkboxField);
} else if(hasCheckbox) {
this.getCheckboxInputFromElement(element).parentElement.remove();
}
return true;
}
protected getCheckboxInputFromElement(element: HTMLElement): HTMLInputElement {
return element.firstElementChild?.tagName === 'LABEL' &&
element.firstElementChild.firstElementChild as HTMLInputElement;
}
protected updateContainer(forceSelection = false) {
const size = this.selectedMids.size;
if(!size && !forceSelection) return;
let cantForward = !size,
cantDelete = !size,
cantSend = !size;
for(const [peerId, mids] of this.selectedMids) {
const storage = this.isScheduled ? this.appMessagesManager.getScheduledMessagesStorage(peerId) : this.appMessagesManager.getMessagesStorage(peerId);
for(const mid of mids) {
const message = this.appMessagesManager.getMessageFromStorage(storage, mid);
if(!cantForward) {
cantForward = !this.appMessagesManager.canForward(message);
}
if(!cantDelete) {
cantDelete = !this.appMessagesManager.canDeleteMessage(message);
}
if(cantForward && cantDelete) break;
}
if(cantForward && cantDelete) break;
}
this.onUpdateContainer && this.onUpdateContainer(cantForward, cantDelete, cantSend);
}
public toggleSelection(toggleCheckboxes = true, forceSelection = false) {
const wasSelecting = this.isSelecting;
const size = this.selectedMids.size;
this.isSelecting = !!size || forceSelection;
if(wasSelecting === this.isSelecting) return false;
// const bubblesContainer = this.bubbles.bubblesContainer;
//bubblesContainer.classList.toggle('is-selecting', !!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(!IS_TOUCH_SUPPORTED) {
this.listenElement.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();
const forwards = !!size || forceSelection;
this.onToggleSelection && this.onToggleSelection(forwards);
if(!IS_MOBILE_SAFARI) {
if(forwards) {
appNavigationController.pushItem({
type: this.navigationType,
onPop: () => {
this.cancelSelection();
}
});
} else {
appNavigationController.removeByType(this.navigationType);
}
}
if(forceSelection) {
this.updateContainer(forceSelection);
}
return true;
}
public cancelSelection = () => {
this.onCancelSelection && this.onCancelSelection();
this.selectedMids.clear();
this.toggleSelection();
cancelSelection();
};
public cleanup() {
this.selectedMids.clear();
this.toggleSelection(false);
}
protected updateElementSelection(element: HTMLElement, isSelected: boolean) {
this.toggleElementCheckbox(element, true);
const input = this.getCheckboxInputFromElement(element);
input.checked = isSelected;
this.toggleSelection();
this.updateContainer();
SetTransition(element, 'is-selected', isSelected, 200);
}
public isMidSelected(peerId: PeerId, mid: number) {
const set = this.selectedMids.get(peerId);
return set?.has(mid);
}
public length() {
return accumulateMapSet(this.selectedMids);
}
protected toggleMid(peerId: PeerId, mid: number, unselect?: boolean) {
let set = this.selectedMids.get(peerId);
if(unselect || (unselect === undefined && set?.has(mid))) {
if(set) {
set.delete(mid);
if(!set.size) {
this.selectedMids.delete(peerId);
}
}
} else {
const diff = rootScope.config.forwarded_count_max - this.length() - 1;
if(diff < 0) {
toast(I18n.format('Chat.Selection.LimitToast', true));
return false;
/* 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); */
}
if(!set) {
set = new Set();
this.selectedMids.set(peerId, set);
}
set.add(mid);
}
return true;
}
/**
* ! Call this method only to handle deleted messages
*/
public deleteSelectedMids(peerId: PeerId, mids: number[]) {
const set = this.selectedMids.get(peerId);
if(!set) {
return;
}
mids.forEach(mid => {
set.delete(mid);
});
if(!set.size) {
this.selectedMids.delete(peerId);
}
this.updateContainer();
this.toggleSelection();
}
}
export class SearchSelection extends AppSelection {
protected selectionContainer: HTMLElement;
protected selectionCountEl: HTMLElement;
public selectionForwardBtn: HTMLElement;
public selectionDeleteBtn: HTMLElement;
public selectionGotoBtn: HTMLElement;
private isPrivate: boolean;
constructor(private searchSuper: AppSearchSuper, appMessagesManager: AppMessagesManager) {
super({
appMessagesManager,
listenElement: searchSuper.container,
listenerSetter: new ListenerSetter(),
verifyTarget: (e, target) => !!target && this.isSelecting,
getElementFromTarget: (target) => findUpClassName(target, 'search-super-item'),
targetLookupClassName: 'search-super-item',
lookupBetweenParentClassName: 'tabs-tab',
lookupBetweenElementsQuery: '.search-super-item'
});
this.isPrivate = !searchSuper.showSender;
}
/* public appendCheckbox(element: HTMLElement, checkboxField: CheckboxField) {
checkboxField.label.classList.add('bubble-select-checkbox');
if(element.classList.contains('document') || element.tagName === 'AUDIO-ELEMENT') {
element.querySelector('.document, audio-element').append(checkboxField.label);
} else {
super.appendCheckbox(bubble, checkboxField);
}
} */
public toggleSelection(toggleCheckboxes = true, forceSelection = false) {
const ret = super.toggleSelection(toggleCheckboxes, forceSelection);
if(ret && toggleCheckboxes) {
const elements = Array.from(this.searchSuper.tabsContainer.querySelectorAll('.search-super-item')) as HTMLElement[];
elements.forEach(element => {
this.toggleElementCheckbox(element, this.isSelecting);
});
}
return ret;
}
public toggleByElement = (element: HTMLElement) => {
const mid = +element.dataset.mid;
const peerId = element.dataset.peerId.toPeerId();
if(!this.toggleMid(peerId, mid)) {
return;
}
this.updateElementSelection(element, this.isMidSelected(peerId, mid));
};
public toggleByMid = (peerId: PeerId, mid: number) => {
const element = this.searchSuper.mediaTab.contentTab.querySelector(`.search-super-item[data-peer-id="${peerId}"][data-mid="${mid}"]`) as HTMLElement;
this.toggleByElement(element);
};
protected onUpdateContainer = (cantForward: boolean, cantDelete: boolean, cantSend: boolean) => {
const length = this.length();
replaceContent(this.selectionCountEl, i18n('messages', [length]));
this.selectionGotoBtn.classList.toggle('hide', length !== 1);
this.selectionForwardBtn.classList.toggle('hide', cantForward);
this.selectionDeleteBtn && this.selectionDeleteBtn.classList.toggle('hide', cantDelete);
};
protected onToggleSelection = (forwards: boolean) => {
SetTransition(this.searchSuper.navScrollableContainer, 'is-selecting', forwards, 200, () => {
if(!this.isSelecting) {
this.selectionContainer.remove();
this.selectionContainer =
this.selectionForwardBtn =
this.selectionDeleteBtn =
null;
this.selectedText = undefined;
}
});
SetTransition(this.searchSuper.container, 'is-selecting', forwards, 200);
if(this.isSelecting) {
if(!this.selectionContainer) {
const BASE_CLASS = 'search-super-selection';
this.selectionContainer = document.createElement('div');
this.selectionContainer.classList.add(BASE_CLASS + '-container');
const btnCancel = ButtonIcon(`close ${BASE_CLASS}-cancel`, {noRipple: true});
this.listenerSetter.add(btnCancel)('click', this.cancelSelection, {once: true});
this.selectionCountEl = document.createElement('div');
this.selectionCountEl.classList.add(BASE_CLASS + '-count');
this.selectionGotoBtn = ButtonIcon(`message ${BASE_CLASS}-goto`);
const attachClickOptions: AttachClickOptions = {listenerSetter: this.listenerSetter};
attachClickEvent(this.selectionGotoBtn, () => {
const peerId = [...this.selectedMids.keys()][0];
const mid = [...this.selectedMids.get(peerId)][0];
this.cancelSelection();
rootScope.dispatchEvent('history_focus', {
peerId,
mid
});
}, attachClickOptions);
this.selectionForwardBtn = ButtonIcon(`forward ${BASE_CLASS}-forward`);
attachClickEvent(this.selectionForwardBtn, () => {
const obj: {[fromPeerId: PeerId]: number[]} = {};
for(const [fromPeerId, mids] of this.selectedMids) {
obj[fromPeerId] = Array.from(mids).sort((a, b) => a - b);
}
new PopupForward(obj, () => {
this.cancelSelection();
});
}, attachClickOptions);
if(this.isPrivate) {
this.selectionDeleteBtn = ButtonIcon(`delete danger ${BASE_CLASS}-delete`);
attachClickEvent(this.selectionDeleteBtn, () => {
const peerId = [...this.selectedMids.keys()][0];
new PopupDeleteMessages(peerId, [...this.selectedMids.get(peerId)], 'chat', () => {
this.cancelSelection();
});
}, attachClickOptions);
}
this.selectionContainer.append(...[
btnCancel,
this.selectionCountEl,
this.selectionGotoBtn,
this.selectionForwardBtn,
this.selectionDeleteBtn
].filter(Boolean));
const transitionElement = this.selectionContainer;
transitionElement.style.opacity = '0';
this.searchSuper.navScrollableContainer.append(transitionElement);
void transitionElement.offsetLeft; // reflow
transitionElement.style.opacity = '';
}
}
};
}
export default class ChatSelection extends AppSelection {
protected selectionInputWrapper: HTMLElement;
protected selectionContainer: HTMLElement;
protected selectionCountEl: HTMLElement;
public selectionSendNowBtn: HTMLElement;
public selectionForwardBtn: HTMLElement;
public selectionDeleteBtn: HTMLElement;
selectionLeft: HTMLDivElement;
selectionRight: HTMLDivElement;
constructor(private chat: Chat, private bubbles: ChatBubbles, private input: ChatInput, appMessagesManager: AppMessagesManager) {
super({
appMessagesManager,
listenElement: bubbles.bubblesContainer,
listenerSetter: bubbles.listenerSetter,
getElementFromTarget: (target) => findUpClassName(target, 'grouped-item') || findUpClassName(target, 'bubble'),
verifyTarget: (e, target) => {
// LEFT BUTTON
// проверка внизу нужна для того, чтобы не активировать селект если target потомок .bubble
const bad = !this.selectedMids.size
&& !(e.target as HTMLElement).classList.contains('bubble')
&& !(e.target as HTMLElement).classList.contains('document-selection')
&& target;
return !bad;
},
verifyMouseMoveTarget: (e, element, selecting) => {
const bad = e.target !== element &&
!(e.target as HTMLElement).classList.contains('document-selection') &&
selecting === undefined &&
!this.selectedMids.size;
return !bad;
},
verifyTouchLongPress: () => !this.chat.input.recording,
targetLookupClassName: 'bubble',
lookupBetweenParentClassName: 'bubbles-inner',
lookupBetweenElementsQuery: '.bubble:not(.is-multiple-documents), .grouped-item',
isScheduled: chat.type === 'scheduled'
});
}
public appendCheckbox(bubble: HTMLElement, checkboxField: CheckboxField) {
checkboxField.label.classList.add('bubble-select-checkbox');
if(bubble.classList.contains('document-container')) {
bubble.querySelector('.document, audio-element').append(checkboxField.label);
} else {
super.appendCheckbox(bubble, checkboxField);
}
}
public toggleSelection(toggleCheckboxes = true, forceSelection = false) {
const ret = super.toggleSelection(toggleCheckboxes, forceSelection);
if(ret && toggleCheckboxes) {
for(const mid in this.bubbles.bubbles) {
const bubble = this.bubbles.bubbles[mid];
this.toggleElementCheckbox(bubble, this.isSelecting);
}
}
return ret;
}
public toggleElementCheckbox(bubble: HTMLElement, show: boolean) {
if(!this.canSelectBubble(bubble)) return;
const ret = super.toggleElementCheckbox(bubble, show);
if(ret) {
const isGrouped = bubble.classList.contains('is-grouped');
if(isGrouped) {
this.bubbles.getBubbleGroupedItems(bubble).forEach(item => this.toggleElementCheckbox(item, show));
}
}
return ret;
}
public toggleByElement = (bubble: HTMLElement) => {
if(!this.canSelectBubble(bubble)) return;
const mid = +bubble.dataset.mid;
const isGrouped = bubble.classList.contains('is-grouped');
if(isGrouped) {
if(!this.isGroupedBubbleSelected(bubble)) {
const set = this.selectedMids.get(this.bubbles.peerId);
if(set) {
const mids = this.chat.getMidsByMid(mid);
mids.forEach(mid => set.delete(mid));
}
}
this.bubbles.getBubbleGroupedItems(bubble).forEach(this.toggleByElement);
return;
}
if(!this.toggleMid(this.bubbles.peerId, mid)) {
return;
}
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.updateElementSelection(groupContainer, isGroupedMidsSelected);
}
}
this.updateElementSelection(bubble, this.isMidSelected(this.bubbles.peerId, mid));
};
protected toggleByMid = (peerId: PeerId, mid: number) => {
const mounted = this.bubbles.getMountedBubble(mid);
if(mounted) {
this.toggleByElement(mounted.bubble);
}
};
public isElementShouldBeSelected(element: HTMLElement) {
const isGrouped = element.classList.contains('is-grouped');
return super.isElementShouldBeSelected(element) && (!isGrouped || this.isGroupedMidsSelected(+element.dataset.mid));
}
protected isGroupedBubbleSelected(bubble: HTMLElement) {
const groupedCheckboxInput = this.getCheckboxInputFromElement(bubble);
return groupedCheckboxInput?.checked;
}
protected isGroupedMidsSelected(mid: number) {
const mids = this.chat.getMidsByMid(mid);
const selectedMids = mids.filter(mid => this.isMidSelected(this.bubbles.peerId, mid));
return mids.length === selectedMids.length;
}
protected getCheckboxInputFromElement(bubble: HTMLElement) {
/* 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') as HTMLInputElement :
super.getCheckboxInputFromElement(bubble);
}
public canSelectBubble(bubble: HTMLElement) {
return !bubble.classList.contains('service') && !bubble.classList.contains('is-sending') && !bubble.classList.contains('bubble-first');
}
protected onToggleSelection = (forwards: boolean) => {
let transform = '', borderRadius = '', needTranslateX: number;
// if(forwards) {
const p = this.input.rowsWrapper.parentElement;
const fakeSelectionWrapper = p.querySelector('.fake-selection-wrapper');
const fakeRowsWrapper = p.querySelector('.fake-rows-wrapper');
const fakeSelectionRect = fakeSelectionWrapper.getBoundingClientRect();
const fakeRowsRect = fakeRowsWrapper.getBoundingClientRect();
const widthFrom = fakeRowsRect.width;
const widthTo = fakeSelectionRect.width;
if(widthFrom !== widthTo) {
const scale = (widthTo/* - 8 */) / widthFrom;
const initTranslateX = (widthFrom - widthTo) / 2;
needTranslateX = fakeSelectionRect.left - fakeRowsRect.left - initTranslateX;
if(forwards) {
transform = `translateX(${needTranslateX}px) scaleX(${scale})`;
if(scale < 1) {
const br = 12;
borderRadius = '' + (br + br * (1 - scale)) + 'px';
}
}
//scale = widthTo / widthFrom;
}
// }
SetTransition(this.input.rowsWrapper, 'is-centering', forwards, 200);
this.input.rowsWrapper.style.transform = transform;
this.input.rowsWrapper.style.borderRadius = borderRadius;
SetTransition(this.listenElement, 'is-selecting', forwards, 200, () => {
if(!this.isSelecting) {
this.selectionInputWrapper.remove();
this.selectionInputWrapper =
this.selectionContainer =
this.selectionSendNowBtn =
this.selectionForwardBtn =
this.selectionDeleteBtn =
this.selectionLeft =
this.selectionRight =
null;
this.selectedText = undefined;
}
fastRaf(() => {
this.bubbles.onScroll();
});
});
//const chatInput = this.appImManager.chatInput;
if(this.isSelecting) {
if(!this.selectionContainer) {
this.selectionInputWrapper = document.createElement('div');
this.selectionInputWrapper.classList.add('chat-input-wrapper', 'selection-wrapper');
this.selectionContainer = document.createElement('div');
this.selectionContainer.classList.add('selection-container');
const attachClickOptions: AttachClickOptions = {listenerSetter: this.listenerSetter};
const btnCancel = ButtonIcon('close', {noRipple: true});
attachClickEvent(btnCancel, this.cancelSelection, {once: true, listenerSetter: this.listenerSetter});
this.selectionCountEl = document.createElement('div');
this.selectionCountEl.classList.add('selection-container-count');
if(this.chat.type === 'scheduled') {
this.selectionSendNowBtn = Button('btn-primary btn-transparent btn-short text-bold selection-container-send', {icon: 'send2'});
this.selectionSendNowBtn.append(i18n('MessageScheduleSend'));
attachClickEvent(this.selectionSendNowBtn, () => {
new PopupSendNow(this.bubbles.peerId, [...this.selectedMids.get(this.bubbles.peerId)], () => {
this.cancelSelection();
});
}, attachClickOptions);
} else {
this.selectionForwardBtn = Button('btn-primary btn-transparent text-bold selection-container-forward', {icon: 'forward'});
this.selectionForwardBtn.append(i18n('Forward'));
attachClickEvent(this.selectionForwardBtn, () => {
const obj: {[fromPeerId: PeerId]: number[]} = {};
for(const [fromPeerId, mids] of this.selectedMids) {
obj[fromPeerId] = Array.from(mids).sort((a, b) => a - b);
}
new PopupForward(obj, () => {
this.cancelSelection();
});
}, attachClickOptions);
}
this.selectionDeleteBtn = Button('btn-primary btn-transparent danger text-bold selection-container-delete', {icon: 'delete'});
this.selectionDeleteBtn.append(i18n('Delete'));
attachClickEvent(this.selectionDeleteBtn, () => {
new PopupDeleteMessages(this.bubbles.peerId, [...this.selectedMids.get(this.bubbles.peerId)], this.chat.type, () => {
this.cancelSelection();
});
}, attachClickOptions);
const left = this.selectionLeft = document.createElement('div');
left.classList.add('selection-container-left');
left.append(btnCancel, this.selectionCountEl);
const right = this.selectionRight = document.createElement('div');
right.classList.add('selection-container-right');
right.append(...[
this.selectionSendNowBtn,
this.selectionForwardBtn,
this.selectionDeleteBtn
].filter(Boolean))
left.style.transform = `translateX(-${needTranslateX * 2}px)`;
right.style.transform = `translateX(${needTranslateX * 2}px)`;
this.selectionContainer.append(left, right);
this.selectionInputWrapper.style.opacity = '0';
this.selectionInputWrapper.append(this.selectionContainer);
this.input.rowsWrapper.parentElement.append(this.selectionInputWrapper);
void this.selectionInputWrapper.offsetLeft; // reflow
this.selectionInputWrapper.style.opacity = '';
left.style.transform = '';
right.style.transform = '';
}
} else {
this.selectionLeft.style.transform = `translateX(-${needTranslateX * 2}px)`;
this.selectionRight.style.transform = `translateX(${needTranslateX * 2}px)`;
}
};
protected onUpdateContainer = (cantForward: boolean, cantDelete: boolean, cantSend: boolean) => {
replaceContent(this.selectionCountEl, i18n('messages', [this.length()]));
this.selectionSendNowBtn && this.selectionSendNowBtn.toggleAttribute('disabled', cantSend);
this.selectionForwardBtn && this.selectionForwardBtn.toggleAttribute('disabled', cantForward);
this.selectionDeleteBtn.toggleAttribute('disabled', cantDelete);
};
protected onCancelSelection = () => {
for(const [peerId, mids] of this.selectedMids) {
for(const mid of mids) {
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.toggleByElement(mounted.bubble);
}
/* const bubble = this.appImManager.bubbles[mid];
if(bubble) {
this.toggleByBubble(bubble);
} */
}
}
};
}