import appImManager from "../lib/appManagers/appImManager"; import { horizontalMenu, renderImageFromUrl, putPreloader } from "./misc"; import lottieLoader from "../lib/lottieLoader"; //import Scrollable from "./scrollable"; import Scrollable from "./scrollable_new"; import { findUpTag, whichChild, calcImageInBox, emojiUnicode } from "../lib/utils"; import { RichTextProcessor } from "../lib/richtextprocessor"; import appStickersManager, { MTStickerSet } from "../lib/appManagers/appStickersManager"; //import apiManager from '../lib/mtproto/apiManager'; import apiManager from '../lib/mtproto/mtprotoworker'; import LazyLoadQueue from "./lazyLoadQueue"; import { wrapSticker } from "./wrappers"; import appDocsManager from "../lib/appManagers/appDocsManager"; import ProgressivePreloader from "./preloader"; import Config, { touchSupport } from "../lib/config"; import { MTDocument } from "../types"; import animationIntersector from "./animationIntersector"; import appSidebarRight from "../lib/appManagers/appSidebarRight"; import appStateManager from "../lib/appManagers/appStateManager"; export const EMOTICONSSTICKERGROUP = 'emoticons-dropdown'; interface EmoticonsTab { init: () => void, onCloseAfterTimeout?: () => void } class EmojiTab implements EmoticonsTab { public content: HTMLElement; private recent: string[] = []; private recentItemsDiv: HTMLElement; private heights: number[] = []; private scroll: Scrollable; init() { this.content = document.getElementById('content-emoji') as HTMLDivElement; const categories = ["Smileys & Emotion", "Animals & Nature", "Food & Drink", "Travel & Places", "Activities", "Objects", /* "Symbols", */"Flags", "Skin Tones"]; const divs: { [category: string]: HTMLDivElement } = {}; const sorted: { [category: string]: string[] } = { 'Recent': [] }; for(const emoji in Config.Emoji) { const details = Config.Emoji[emoji]; const i = '' + details; const category = categories[+i[0] - 1]; if(!category) continue; // maybe it's skin tones if(!sorted[category]) sorted[category] = []; sorted[category][+i.slice(1) || 0] = emoji; } console.log('emoticons sorted:', sorted); //Object.keys(sorted).forEach(c => sorted[c].sort((a, b) => a - b)); categories.pop(); delete sorted["Skin Tones"]; //console.time('emojiParse'); for(const category in sorted) { const div = document.createElement('div'); div.classList.add('emoji-category'); const titleDiv = document.createElement('div'); titleDiv.classList.add('category-title'); titleDiv.innerText = category; const itemsDiv = document.createElement('div'); itemsDiv.classList.add('category-items'); div.append(titleDiv, itemsDiv); const emojis = sorted[category]; emojis.forEach(emoji => { /* if(emojiUnicode(emoji) == '1f481-200d-2642') { console.log('append emoji', emoji, emojiUnicode(emoji)); } */ this.appendEmoji(emoji/* .replace(/[\ufe0f\u2640\u2642\u2695]/g, '') */, itemsDiv); /* if(category == 'Smileys & Emotion') { console.log('appended emoji', emoji, itemsDiv.children[itemsDiv.childElementCount - 1].innerHTML, emojiUnicode(emoji)); } */ }); divs[category] = div; } //console.timeEnd('emojiParse'); let prevCategoryIndex = 0; const menu = this.content.previousElementSibling.firstElementChild as HTMLUListElement; const emojiScroll = this.scroll = new Scrollable(this.content, 'y', 'EMOJI', null); emojiScroll.container.addEventListener('scroll', (e) => { prevCategoryIndex = EmoticonsDropdown.contentOnScroll(menu, this.heights, prevCategoryIndex, emojiScroll.container); }); //emojiScroll.setVirtualContainer(emojiScroll.container); const preloader = putPreloader(this.content, true); Promise.all([ new Promise((resolve) => setTimeout(resolve, 200)), appStateManager.getState().then(state => { if(Array.isArray(state.recentEmoji)) { this.recent = state.recentEmoji; } }) ]).then(() => { preloader.remove(); this.recentItemsDiv = divs['Recent'].querySelector('.category-items'); for(const emoji of this.recent) { this.appendEmoji(emoji, this.recentItemsDiv); } categories.unshift('Recent'); categories.map(category => { const div = divs[category]; if(!div) { console.error('no div by category:', category); } emojiScroll.append(div); return div; }).forEach(div => { //console.log('emoji heights push: ', (heights[heights.length - 1] || 0) + div.scrollHeight, div, div.scrollHeight); this.heights.push((this.heights[this.heights.length - 1] || 0) + div.scrollHeight); }); }); this.content.addEventListener('click', this.onContentClick); EmoticonsDropdown.menuOnClick(menu, this.heights, emojiScroll); this.init = null; } private appendEmoji(emoji: string, container: HTMLElement, prepend = false) { //const emoji = details.unified; //const emoji = (details.unified as string).split('-') //.reduce((prev, curr) => prev + String.fromCodePoint(parseInt(curr, 16)), ''); const spanEmoji = document.createElement('span'); const kek = RichTextProcessor.wrapEmojiText(emoji); /* if(!kek.includes('emoji')) { console.log(emoji, kek, spanEmoji, emoji.length, new TextEncoder().encode(emoji), emojiUnicode(emoji)); return; } */ //console.log(kek); spanEmoji.innerHTML = kek; //spanEmoji = spanEmoji.firstElementChild as HTMLSpanElement; //spanEmoji.setAttribute('emoji', emoji); if(prepend) container.prepend(spanEmoji); else container.appendChild(spanEmoji); } private getEmojiFromElement(element: HTMLElement) { if(element.tagName == 'SPAN' && !element.classList.contains('emoji')) { element = element.firstElementChild as HTMLElement; } return element.getAttribute('alt') || element.innerText; } onContentClick = (e: MouseEvent) => { let target = e.target as HTMLElement; //if(target.tagName != 'SPAN') return; if(target.tagName == 'SPAN' && !target.classList.contains('emoji')) { target = target.firstElementChild as HTMLElement; } else if(target.tagName == 'DIV') return; //console.log('contentEmoji div', target); appImManager.chatInputC.messageInput.innerHTML += target.outerHTML; // Recent const emoji = this.getEmojiFromElement(target); (Array.from(this.recentItemsDiv.children) as HTMLElement[]).forEach((el, idx) => { const _emoji = this.getEmojiFromElement(el); if(emoji == _emoji) { el.remove(); } }); const scrollHeight = this.recentItemsDiv.scrollHeight; this.appendEmoji(emoji, this.recentItemsDiv, true); // нужно поставить новые размеры для скролла if(this.recentItemsDiv.scrollHeight != scrollHeight) { this.heights.length = 0; (Array.from(this.scroll.container.children) as HTMLElement[]).forEach(div => { this.heights.push((this.heights[this.heights.length - 1] || 0) + div.scrollHeight); }); } this.recent.findAndSplice(e => e == emoji); this.recent.unshift(emoji); if(this.recent.length > 36) { this.recent.length = 36; } appStateManager.pushToState('recentEmoji', this.recent); // Append to input const event = new Event('input', {bubbles: true, cancelable: true}); appImManager.chatInputC.messageInput.dispatchEvent(event); }; onClose() { } } class StickersTab implements EmoticonsTab { public content: HTMLElement; init() { this.content = document.getElementById('content-stickers'); //let stickersDiv = contentStickersDiv.querySelector('.os-content') as HTMLDivElement; let menuWrapper = this.content.previousElementSibling as HTMLDivElement; let menu = menuWrapper.firstElementChild.firstElementChild as HTMLUListElement; let menuScroll = new Scrollable(menuWrapper, 'x'); let stickersDiv = document.createElement('div'); stickersDiv.classList.add('stickers-categories'); this.content.append(stickersDiv); /* stickersDiv.addEventListener('mouseover', (e) => { let target = e.target as HTMLElement; if(target.tagName == 'CANVAS') { // turn on sticker let animation = lottieLoader.getAnimation(target.parentElement, EMOTICONSSTICKERGROUP); if(animation) { // @ts-ignore if(animation.currentFrame == animation.totalFrames - 1) { animation.goToAndPlay(0, true); } else { animation.play(); } } } }); */ stickersDiv.addEventListener('click', EmoticonsDropdown.onMediaClick); let heights: number[] = []; let heightRAF = 0; let categoryPush = (categoryDiv: HTMLDivElement, categoryTitle: string, docs: MTDocument[], prepend?: boolean) => { //if((docs.length % 5) != 0) categoryDiv.classList.add('not-full'); let itemsDiv = document.createElement('div'); itemsDiv.classList.add('category-items'); let titleDiv = document.createElement('div'); titleDiv.classList.add('category-title'); titleDiv.innerText = categoryTitle; categoryDiv.append(titleDiv, itemsDiv); docs.forEach(doc => { let div = document.createElement('div'); wrapSticker({ doc, div, lazyLoadQueue: EmoticonsDropdown.lazyLoadQueue, group: EMOTICONSSTICKERGROUP, onlyThumb: true }); itemsDiv.append(div); }); if(prepend) stickersScroll.prepend(categoryDiv); else stickersScroll.append(categoryDiv); /* let scrollHeight = categoryDiv.scrollHeight; let prevHeight = heights[heights.length - 1] || 0; //console.log('scrollHeight', scrollHeight, categoryDiv, stickersDiv.childElementCount); if(prepend && heights.length) {// all stickers loaded faster than recent heights.forEach((h, i) => heights[i] += scrollHeight); return heights.unshift(scrollHeight) - 1; } */ if(heightRAF) window.cancelAnimationFrame(heightRAF); heightRAF = window.requestAnimationFrame(() => { heightRAF = 0; let paddingTop = parseInt(window.getComputedStyle(stickersScroll.container).getPropertyValue('padding-top')) || 0; heights.length = 0; /* let concated = stickersScroll.hiddenElements.up.concat(stickersScroll.visibleElements, stickersScroll.hiddenElements.down); concated.forEach((el, i) => { heights[i] = (heights[i - 1] || 0) + el.height + (i == 0 ? paddingTop : 0); }); */ let concated = Array.from(stickersScroll.splitUp.children) as HTMLElement[]; concated.forEach((el, i) => { heights[i] = (heights[i - 1] || 0) + el.scrollHeight + (i == 0 ? paddingTop : 0); }); //console.log('stickers concated', concated, heights); }); /* Array.from(stickersDiv.children).forEach((div, i) => { heights[i] = (heights[i - 1] || 0) + div.scrollHeight; }); */ //stickersScroll.onScroll(); //return heights.push(prevHeight + scrollHeight) - 1; }; let prevCategoryIndex = 0; let stickersScroll = new Scrollable(this.content, 'y', 'STICKERS', undefined, undefined, 2); stickersScroll.container.addEventListener('scroll', (e) => { animationIntersector.checkAnimations(false, EMOTICONSSTICKERGROUP); prevCategoryIndex = EmoticonsDropdown.contentOnScroll(menu, heights, prevCategoryIndex, stickersScroll.container, menuScroll); }); stickersScroll.setVirtualContainer(stickersDiv); EmoticonsDropdown.menuOnClick(menu, heights, stickersScroll, menuScroll); const preloader = putPreloader(this.content, true); Promise.all([ appStickersManager.getRecentStickers().then(stickers => { let categoryDiv = document.createElement('div'); categoryDiv.classList.add('sticker-category'); //stickersScroll.prepend(categoryDiv); preloader.remove(); categoryPush(categoryDiv, 'Recent', stickers.stickers, true); }), apiManager.invokeApi('messages.getAllStickers', {hash: 0}).then(async(res) => { let stickers: { _: 'messages.allStickers', hash: number, sets: Array } = res as any; preloader.remove(); for(let set of stickers.sets) { let categoryDiv = document.createElement('div'); categoryDiv.classList.add('sticker-category'); let li = document.createElement('li'); li.classList.add('btn-icon'); menu.append(li); //stickersScroll.append(categoryDiv); let stickerSet = await appStickersManager.getStickerSet(set); //console.log('got stickerSet', stickerSet, li); if(stickerSet.set.thumb) { appStickersManager.getStickerSetThumb(stickerSet.set).then((blob) => { //console.log('setting thumb', stickerSet, blob); if(stickerSet.set.pFlags.animated) { // means animated const reader = new FileReader(); reader.addEventListener('loadend', async(e) => { // @ts-ignore const text = e.srcElement.result; let json = await apiManager.gzipUncompress(text, true); let animation = await lottieLoader.loadAnimationWorker({ container: li, loop: true, autoplay: false, animationData: JSON.parse(json), width: 32, height: 32 }, EMOTICONSSTICKERGROUP); }); reader.readAsArrayBuffer(blob); } else { let image = new Image(); renderImageFromUrl(image, URL.createObjectURL(blob)); li.append(image); } }); } else { // as thumb will be used first sticker wrapSticker({ doc: stickerSet.documents[0], div: li as any, group: EMOTICONSSTICKERGROUP }); // kostil } categoryPush(categoryDiv, stickerSet.set.title, stickerSet.documents, false); } }) ]); this.init = null; } onClose() { } } class GifsTab implements EmoticonsTab { public content: HTMLElement; init() { this.content = document.getElementById('content-gifs'); const masonry = this.content.firstElementChild as HTMLDivElement; masonry.addEventListener('click', EmoticonsDropdown.onMediaClick); const scroll = new Scrollable(this.content, 'y', 'GIFS', null); const preloader = putPreloader(this.content, true); const width = 400; const maxSingleWidth = width - 100; const height = 100; apiManager.invokeApi('messages.getSavedGifs', {hash: 0}).then((_res) => { let res = _res as { _: 'messages.savedGifs', gifs: MTDocument[], hash: number }; //console.log('getSavedGifs res:', res); //let line: MTDocument[] = []; let wastedWidth = 0; res.gifs.forEach((gif, idx) => { res.gifs[idx] = appDocsManager.saveDoc(gif); }); preloader.remove(); for(let i = 0, length = res.gifs.length; i < length;) { let gif = res.gifs[i]; let gifWidth = gif.w; let gifHeight = gif.h; if(gifHeight < height) { gifWidth = height / gifHeight * gifWidth; gifHeight = height; } let willUseWidth = Math.min(maxSingleWidth, width - wastedWidth, gifWidth); let {w, h} = calcImageInBox(gifWidth, gifHeight, willUseWidth, height); /* wastedWidth += w; if(wastedWidth == width || h < height) { wastedWidth = 0; console.log('completed line', i, line); line = []; continue; } line.push(gif); */ ++i; //console.log('gif:', gif, w, h); let div = document.createElement('div'); div.style.width = w + 'px'; //div.style.height = h + 'px'; div.dataset.docID = gif.id; masonry.append(div); let preloader = new ProgressivePreloader(div); EmoticonsDropdown.lazyLoadQueue.push({ div, load: () => { let promise = appDocsManager.downloadDoc(gif); preloader.attach(div, true, promise); promise.then(blob => { preloader.detach(); div.innerHTML = ``; }); return promise; } }); } }); this.init = null; } onClose() { } } class EmoticonsDropdown { public static lazyLoadQueue = new LazyLoadQueue(); private element: HTMLElement; private emojiTab: EmojiTab; private stickersTab: StickersTab; private gifsTab: GifsTab; private container: HTMLElement; private tabsEl: HTMLElement; private tabID = -1; private tabs: {[id: number]: EmoticonsTab}; public searchButton: HTMLElement; public deleteBtn: HTMLElement; public toggleEl: HTMLElement; private displayTimeout: number; constructor() { this.element = document.getElementById('emoji-dropdown') as HTMLDivElement; let firstTime = true; this.toggleEl = document.getElementById('toggle-emoticons'); if(touchSupport) { this.toggleEl.addEventListener('click', () => { if(firstTime) { firstTime = false; this.toggle(true); } else { this.toggle(); } }); } else { this.toggleEl.onmouseover = (e) => { clearTimeout(this.displayTimeout); //this.displayTimeout = setTimeout(() => { if(firstTime) { this.toggleEl.onmouseout = this.element.onmouseout = (e) => { clearTimeout(this.displayTimeout); this.displayTimeout = setTimeout(() => { this.toggle(); }, 200); }; this.element.onmouseover = (e) => { clearTimeout(this.displayTimeout); }; firstTime = false; } this.toggle(true); //}, 0/* 200 */); }; } } 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; horizontalMenu(this.tabsEl, this.container, (id) => { animationIntersector.checkAnimations(true, EMOTICONSSTICKERGROUP); this.tabID = id; this.searchButton.classList.toggle('hide', this.tabID != 1); this.deleteBtn.classList.toggle('hide', this.tabID != 0); }, () => { 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', () => { appSidebarRight.stickersTab.init(); }); this.deleteBtn = this.element.querySelector('.emoji-tabs-delete'); this.deleteBtn.addEventListener('click', () => { const input = appImManager.chatInputC.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.chatInputC.messageInput.dispatchEvent(event); //appSidebarRight.stickersTab.init(); }); (this.tabsEl.firstElementChild.children[1] as HTMLLIElement).click(); // set emoji tab this.tabs[0].init(); // onTransitionEnd не вызовется, т.к. это первая открытая вкладка } 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(touchSupport) { this.toggleEl.classList.toggle('flip-icon', willBeActive); if(willBeActive) { appImManager.chatInputC.saveScroll(); // @ts-ignore document.activeElement.blur(); await new Promise((resolve) => { setTimeout(resolve, 100); }); } } else { this.toggleEl.classList.toggle('active', enable); } if((this.element.style.display && enable === undefined) || enable) { this.element.style.display = ''; void this.element.offsetLeft; // reflow this.element.classList.add('active'); EmoticonsDropdown.lazyLoadQueue.unlock(); clearTimeout(this.displayTimeout); /* if(touchSupport) { this.restoreScroll(); } */ } else { this.element.classList.remove('active'); animationIntersector.checkAnimations(true, EMOTICONSSTICKERGROUP); EmoticonsDropdown.lazyLoadQueue.lock(); clearTimeout(this.displayTimeout); this.displayTimeout = setTimeout(() => { this.element.style.display = 'none'; }, touchSupport ? 0 : 200); /* if(touchSupport) { this.restoreScroll(); } */ } animationIntersector.checkAnimations(false, EMOTICONSSTICKERGROUP); }; public static menuOnClick = (menu: HTMLUListElement, heights: number[], scroll: Scrollable, menuScroll?: Scrollable) => { menu.addEventListener('click', function(e) { let target = e.target as HTMLLIElement; target = findUpTag(target, 'LI'); let index = whichChild(target); let y = heights[index - 1/* 2 */] || 0; // 10 == padding .scrollable /* if(menuScroll) { menuScroll.container.scrollLeft = target.scrollWidth * index; } console.log('emoticonsMenuOnClick', menu.getBoundingClientRect(), target.getBoundingClientRect()); */ /* scroll.onAddedBottom = () => { // привет, костыль, давно не виделись! scroll.container.scrollTop = y; scroll.onAddedBottom = () => {}; }; */ scroll.container.scrollTop = y; setTimeout(() => { animationIntersector.checkAnimations(true, EMOTICONSSTICKERGROUP); }, 100); /* window.requestAnimationFrame(() => { window.requestAnimationFrame(() => { lottieLoader.checkAnimations(true, EMOTICONSSTICKERGROUP); }); }); */ }); }; public static contentOnScroll = (menu: HTMLUListElement, heights: number[], prevCategoryIndex: number, scroll: HTMLDivElement, menuScroll?: Scrollable) => { let y = scroll.scrollTop; //console.log(heights, y); for(let i = 0; i < heights.length; ++i) { let height = heights[i]; if(y < height) { menu.children[prevCategoryIndex].classList.remove('active'); prevCategoryIndex = i/* + 1 */; menu.children[prevCategoryIndex].classList.add('active'); if(menuScroll) { if(i < heights.length - 4) { menuScroll.container.scrollLeft = (i - 3) * 47; } else { menuScroll.container.scrollLeft = i * 47; } } break; } } return prevCategoryIndex; }; public static onMediaClick = (e: MouseEvent) => { let target = e.target as HTMLDivElement; target = findUpTag(target, 'DIV'); let fileID = target.dataset.docID; if(appImManager.chatInputC.sendMessageWithDocument(fileID)) { /* dropdown.classList.remove('active'); toggleEl.classList.remove('active'); */ emoticonsDropdown.toggle(false); } else { console.warn('got no doc by id:', fileID); } }; } const emoticonsDropdown = new EmoticonsDropdown(); // @ts-ignore if(process.env.NODE_ENV != 'production') { (window as any).emoticonsDropdown = emoticonsDropdown; } export default emoticonsDropdown;