tweb/src/components/emoticonsDropdown/index.ts
Eduard Kuzmenko b485dce7f2 Fix moving avatars after chat jump
Handle instant peer changing correctly
Display unread badge only in chats
Delay opening on ESG hover
Lazy load sticker sets thumbs
Fix onchanging profile info flick
Hide shared media menu tabs
Scroll on click to shared media tab
Fix media tabs scroll position
Fix voice messages playback in search on iOS
Hover & play on waveform
Better input fields border animation
Don't lock scroll on horizontal scrollables if unnecessary
Restrict editing & deleting outgoing messages
Display avatars in notifications
Open chat on notification click
Don't close country selector on scrolling
Select single country by enter
Fix jumping text in context menus
Changed preloader color
2021-04-18 15:55:56 +04:00

439 lines
13 KiB
TypeScript

/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import { isTouchSupported } from "../../helpers/touchSupport";
import appChatsManager from "../../lib/appManagers/appChatsManager";
import appImManager from "../../lib/appManagers/appImManager";
import rootScope from "../../lib/rootScope";
import { attachClickEvent, blurActiveElement, whichChild } from "../../helpers/dom";
import animationIntersector from "../animationIntersector";
import { horizontalMenu } from "../horizontalMenu";
import LazyLoadQueue, { LazyLoadQueueIntersector } from "../lazyLoadQueue";
import Scrollable, { ScrollableX } from "../scrollable";
import appSidebarRight from "../sidebarRight";
import StickyIntersector from "../stickyIntersector";
import EmojiTab from "./tabs/emoji";
import GifsTab from "./tabs/gifs";
import StickersTab from "./tabs/stickers";
import { pause } from "../../helpers/schedulers";
import { MOUNT_CLASS_TO } from "../../config/debug";
import AppGifsTab from "../sidebarRight/tabs/gifs";
import AppStickersTab from "../sidebarRight/tabs/stickers";
import findUpClassName from "../../helpers/dom/findUpClassName";
import findUpTag from "../../helpers/dom/findUpTag";
import ListenerSetter from "../../helpers/listenerSetter";
export const EMOTICONSSTICKERGROUP = 'emoticons-dropdown';
export interface EmoticonsTab {
init: () => void,
onCloseAfterTimeout?: () => void
}
const KEEP_OPEN = false;
const TOGGLE_TIMEOUT = 200;
const ANIMATION_DURATION = 200;
export class EmoticonsDropdown {
public static lazyLoadQueue = new LazyLoadQueue();
private element: HTMLElement;
public emojiTab: EmojiTab;
public stickersTab: StickersTab;
public gifsTab: GifsTab;
private container: HTMLElement;
private tabsEl: HTMLElement;
private tabId = -1;
private tabs: {[id: number]: EmoticonsTab};
public searchButton: HTMLElement;
public deleteBtn: HTMLElement;
private displayTimeout: number;
public events: {
onClose: Array<() => void>,
onCloseAfter: Array<() => void>,
onOpen: Array<() => void>,
onOpenAfter: Array<() => void>
} = {
onClose: [],
onCloseAfter: [],
onOpen: [],
onOpenAfter: []
};
private selectTab: ReturnType<typeof horizontalMenu>;
private forceClose = false;
constructor() {
this.element = document.getElementById('emoji-dropdown') as HTMLDivElement;
}
public attachButtonListener(button: HTMLElement, listenerSetter: ListenerSetter) {
let firstTime = true;
if(isTouchSupported) {
attachClickEvent(button, () => {
if(firstTime) {
firstTime = false;
this.toggle(true);
} else {
this.toggle();
}
}, {listenerSetter});
} else {
listenerSetter.add(button, 'mouseover', (e) => {
//console.log('onmouseover button');
if(firstTime) {
listenerSetter.add(button, 'mouseout', this.onMouseOut);
firstTime = false;
}
clearTimeout(this.displayTimeout);
this.displayTimeout = window.setTimeout(() => {
this.toggle(true);
}, TOGGLE_TIMEOUT);
});
}
}
private onMouseOut = (e: MouseEvent) => {
if(KEEP_OPEN) return;
clearTimeout(this.displayTimeout);
if(!this.element.classList.contains('active')) return;
const toElement = (e as any).toElement as Element;
if(toElement && findUpClassName(toElement, 'emoji-dropdown')) {
return;
}
this.displayTimeout = window.setTimeout(() => {
this.toggle(false);
}, TOGGLE_TIMEOUT);
};
private init() {
this.emojiTab = new EmojiTab();
this.stickersTab = new StickersTab();
this.gifsTab = new GifsTab();
this.tabs = {
0: this.emojiTab,
1: this.stickersTab,
2: this.gifsTab
};
this.container = this.element.querySelector('.emoji-container .tabs-container') as HTMLDivElement;
this.tabsEl = this.element.querySelector('.emoji-tabs') as HTMLUListElement;
this.selectTab = horizontalMenu(this.tabsEl, this.container, this.onSelectTabClick, () => {
const tab = this.tabs[this.tabId];
if(tab.init) {
tab.init();
}
tab.onCloseAfterTimeout && tab.onCloseAfterTimeout();
animationIntersector.checkAnimations(false, EMOTICONSSTICKERGROUP);
});
this.searchButton = this.element.querySelector('.emoji-tabs-search');
this.searchButton.addEventListener('click', () => {
if(this.tabId === 1) {
if(!appSidebarRight.isTabExists(AppStickersTab)) {
new AppStickersTab(appSidebarRight).open();
}
} else {
if(!appSidebarRight.isTabExists(AppGifsTab)) {
new AppGifsTab(appSidebarRight).open();
}
}
});
this.deleteBtn = this.element.querySelector('.emoji-tabs-delete');
this.deleteBtn.addEventListener('click', () => {
const input = appImManager.chat.input.messageInput;
if((input.lastChild as any)?.tagName) {
input.lastElementChild.remove();
} else if(input.lastChild) {
if(!input.lastChild.textContent.length) {
input.lastChild.remove();
} else {
input.lastChild.textContent = input.lastChild.textContent.slice(0, -1);
}
}
const event = new Event('input', {bubbles: true, cancelable: true});
appImManager.chat.input.messageInput.dispatchEvent(event);
//appSidebarRight.stickersTab.init();
});
(this.tabsEl.children[1] as HTMLLIElement).click(); // set emoji tab
if(this.tabs[0].init) {
this.tabs[0].init(); // onTransitionEnd не вызовется, т.к. это первая открытая вкладка
}
rootScope.on('peer_changed', this.checkRights);
this.checkRights();
if(!isTouchSupported) {
this.element.onmouseout = this.onMouseOut;
this.element.onmouseover = (e) => {
if(this.forceClose) {
return;
}
//console.log('onmouseover element');
clearTimeout(this.displayTimeout);
};
}
}
private onSelectTabClick = (id: number) => {
if(this.tabId === id) {
return;
}
animationIntersector.checkAnimations(true, EMOTICONSSTICKERGROUP);
this.tabId = id;
this.searchButton.classList.toggle('hide', this.tabId === 0);
this.deleteBtn.classList.toggle('hide', this.tabId !== 0);
};
public checkRights = () => {
const peerId = appImManager.chat.peerId;
const children = this.tabsEl.children;
const tabsElements = Array.from(children) as HTMLElement[];
const canSendStickers = peerId > 0 || appChatsManager.hasRights(peerId, 'send_stickers');
tabsElements[2].toggleAttribute('disabled', !canSendStickers);
const canSendGifs = peerId > 0 || appChatsManager.hasRights(peerId, 'send_gifs');
tabsElements[3].toggleAttribute('disabled', !canSendGifs);
const active = this.tabsEl.querySelector('.active');
if(active && whichChild(active) !== 1 && (!canSendStickers || !canSendGifs)) {
this.selectTab(0, false);
}
};
public toggle = async(enable?: boolean) => {
//if(!this.element) return;
const willBeActive = (!!this.element.style.display && enable === undefined) || enable;
if(this.init) {
if(willBeActive) {
this.init();
this.init = null;
} else {
return;
}
}
if(isTouchSupported) {
if(willBeActive) {
//appImManager.chat.input.saveScroll();
if(blurActiveElement()) {
await pause(100);
}
}
}
if(this.element.parentElement !== appImManager.chat.input.chatInput) {
appImManager.chat.input.chatInput.append(this.element);
}
if((this.element.style.display && enable === undefined) || enable) {
this.events.onOpen.forEach(cb => cb());
EmoticonsDropdown.lazyLoadQueue.lock();
//EmoticonsDropdown.lazyLoadQueue.unlock();
animationIntersector.lockIntersectionGroup(EMOTICONSSTICKERGROUP);
this.element.style.display = '';
void this.element.offsetLeft; // reflow
this.element.classList.add('active');
clearTimeout(this.displayTimeout);
this.displayTimeout = window.setTimeout(() => {
animationIntersector.unlockIntersectionGroup(EMOTICONSSTICKERGROUP);
EmoticonsDropdown.lazyLoadQueue.unlock();
EmoticonsDropdown.lazyLoadQueue.refresh();
this.forceClose = false;
this.container.classList.remove('disable-hover');
this.events.onOpenAfter.forEach(cb => cb());
}, isTouchSupported ? 0 : ANIMATION_DURATION);
// ! can't use together with resizeObserver
/* if(isTouchSupported) {
const height = this.element.scrollHeight + appImManager.chat.input.inputContainer.scrollHeight - 10;
console.log('[ESG]: toggle: enable height', height);
appImManager.chat.bubbles.scrollable.scrollTop += height;
} */
/* if(touchSupport) {
this.restoreScroll();
} */
} else {
this.events.onClose.forEach(cb => cb());
EmoticonsDropdown.lazyLoadQueue.lock();
//EmoticonsDropdown.lazyLoadQueue.lock();
// нужно залочить группу и выключить стикеры
animationIntersector.lockIntersectionGroup(EMOTICONSSTICKERGROUP);
animationIntersector.checkAnimations(true, EMOTICONSSTICKERGROUP);
this.element.classList.remove('active');
clearTimeout(this.displayTimeout);
this.displayTimeout = window.setTimeout(() => {
this.element.style.display = 'none';
// теперь можно убрать visible, чтобы они не включились после фокуса
animationIntersector.unlockIntersectionGroup(EMOTICONSSTICKERGROUP);
EmoticonsDropdown.lazyLoadQueue.unlock();
EmoticonsDropdown.lazyLoadQueue.refresh();
this.forceClose = false;
this.container.classList.remove('disable-hover');
this.events.onCloseAfter.forEach(cb => cb());
}, isTouchSupported ? 0 : ANIMATION_DURATION);
/* if(isTouchSupported) {
const scrollHeight = this.container.scrollHeight;
if(scrollHeight) {
const height = this.container.scrollHeight + appImManager.chat.input.inputContainer.scrollHeight - 10;
appImManager.chat.bubbles.scrollable.scrollTop -= height;
}
} */
/* if(touchSupport) {
this.restoreScroll();
} */
}
//animationIntersector.checkAnimations(false, EMOTICONSSTICKERGROUP);
};
public static menuOnClick = (menu: HTMLElement, scroll: Scrollable, menuScroll?: ScrollableX) => {
let prevId = 0;
let jumpedTo = -1;
const setActive = (id: number) => {
if(id === prevId) {
return false;
}
menu.children[prevId].classList.remove('active');
menu.children[id].classList.add('active');
prevId = id;
return true;
};
const stickyIntersector = new StickyIntersector(scroll.container, (stuck, target) => {
//console.log('sticky scrollTOp', stuck, target, scroll.container.scrollTop);
if(Math.abs(jumpedTo - scroll.container.scrollTop) <= 1) {
return;
} else {
jumpedTo = -1;
}
const which = whichChild(target);
if(!stuck && which) { // * due to stickyIntersector
return;
}
setActive(which);
if(menuScroll) {
if(which < menu.childElementCount - 4) {
menuScroll.container.scrollLeft = (which - 3) * 47;
} else {
menuScroll.container.scrollLeft = which * 47;
}
}
});
menu.addEventListener('click', (e) => {
let target = e.target as HTMLElement;
target = findUpClassName(target, 'menu-horizontal-div-item');
if(!target) {
return;
}
const which = whichChild(target);
/* if(menuScroll) {
menuScroll.scrollIntoView(target, false, 0);
} */
if(!setActive(which)) {
return;
}
const element = (scroll.splitUp || scroll.container).children[which] as HTMLElement;
const offsetTop = element.offsetTop + 1; // * due to stickyIntersector
scroll.container.scrollTop = jumpedTo = offsetTop;
//console.log('set scrollTop:', offsetTop);
});
return stickyIntersector;
};
public static onMediaClick = (e: MouseEvent, clearDraft = false) => {
let target = e.target as HTMLElement;
target = findUpTag(target, 'DIV');
if(!target) return;
const fileId = target.dataset.docId;
if(!fileId) return;
if(appImManager.chat.input.sendMessageWithDocument(fileId, undefined, clearDraft)) {
/* dropdown.classList.remove('active');
toggleEl.classList.remove('active'); */
emoticonsDropdown.forceClose = true;
emoticonsDropdown.container.classList.add('disable-hover');
emoticonsDropdown.toggle(false);
} else {
console.warn('got no doc by id:', fileId);
}
};
public addLazyLoadQueueRepeat(lazyLoadQueue: LazyLoadQueueIntersector, processInvisibleDiv: (div: HTMLElement) => void) {
this.events.onClose.push(() => {
lazyLoadQueue.lock();
});
this.events.onCloseAfter.push(() => {
const divs = lazyLoadQueue.intersector.getVisible();
for(const div of divs) {
processInvisibleDiv(div);
}
lazyLoadQueue.intersector.clearVisible();
});
this.events.onOpenAfter.push(() => {
lazyLoadQueue.unlockAndRefresh();
});
}
}
const emoticonsDropdown = new EmoticonsDropdown();
MOUNT_CLASS_TO.emoticonsDropdown = emoticonsDropdown;
export default emoticonsDropdown;