diff --git a/src/components/chatInput.ts b/src/components/chatInput.ts index 0eda2a3d..eafa4a1c 100644 --- a/src/components/chatInput.ts +++ b/src/components/chatInput.ts @@ -1,6 +1,5 @@ import Scrollable from "./scrollable_new"; import { RichTextProcessor } from "../lib/richtextprocessor"; -//import apiManager from "../lib/mtproto/apiManager"; import apiManager from "../lib/mtproto/mtprotoworker"; import appWebPagesManager from "../lib/appManagers/appWebPagesManager"; import appImManager from "../lib/appManagers/appImManager"; diff --git a/src/components/emoticonsDropdown.ts b/src/components/emoticonsDropdown.ts deleted file mode 100644 index 5045714c..00000000 --- a/src/components/emoticonsDropdown.ts +++ /dev/null @@ -1,853 +0,0 @@ -import appImManager from "../lib/appManagers/appImManager"; -import { renderImageFromUrl, putPreloader } from "./misc"; -import lottieLoader from "../lib/lottieLoader"; -//import Scrollable from "./scrollable"; -import Scrollable from "./scrollable_new"; -import { findUpTag, whichChild, calcImageInBox, emojiUnicode, $rootScope, cancelEvent, findUpClassName } 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, wrapVideo } 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"; -import { horizontalMenu } from "./horizontalMenu"; -import GifsMasonry from "./gifsMasonry"; - -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; - - if(spanEmoji.firstElementChild) { - (spanEmoji.firstElementChild as HTMLImageElement).setAttribute('loading', 'lazy'); - } - - //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; - - private stickerSets: {[id: string]: { - stickers: HTMLElement, - tab: HTMLElement - }} = {}; - - private recentDiv: HTMLElement; - private recentStickers: MTDocument[] = []; - - private heights: number[] = []; - private heightRAF = 0; - private scroll: Scrollable; - - private menu: HTMLUListElement; - - private mounted = false; - - categoryPush(categoryDiv: HTMLElement, 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 => { - itemsDiv.append(this.renderSticker(doc)); - }); - - if(prepend) { - if(this.recentDiv.parentElement) { - this.scroll.prepend(categoryDiv); - this.scroll.prepend(this.recentDiv); - } else { - this.scroll.prepend(categoryDiv); - } - } else this.scroll.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; - } */ - - this.setNewHeights(); - - /* Array.from(stickersDiv.children).forEach((div, i) => { - heights[i] = (heights[i - 1] || 0) + div.scrollHeight; - }); */ - - //this.scroll.onScroll(); - - //return heights.push(prevHeight + scrollHeight) - 1; - } - - setNewHeights() { - if(this.heightRAF) return; - //if(this.heightRAF) window.cancelAnimationFrame(this.heightRAF); - this.heightRAF = window.requestAnimationFrame(() => { - this.heightRAF = 0; - - const heights = this.heights; - - let paddingTop = parseInt(window.getComputedStyle(this.scroll.container).getPropertyValue('padding-top')) || 0; - - heights.length = 0; - /* let concated = this.scroll.hiddenElements.up.concat(this.scroll.visibleElements, this.scroll.hiddenElements.down); - concated.forEach((el, i) => { - heights[i] = (heights[i - 1] || 0) + el.height + (i == 0 ? paddingTop : 0); - }); */ - let concated = Array.from(this.scroll.splitUp.children) as HTMLElement[]; - concated.forEach((el, i) => { - heights[i] = (heights[i - 1] || 0) + el.scrollHeight + (i == 0 ? paddingTop : 0); - }); - - this.scroll.reorder(); - - //console.log('stickers concated', concated, heights); - }); - } - - renderSticker(doc: MTDocument) { - let div = document.createElement('div'); - wrapSticker({ - doc, - div, - /* width: 80, - height: 80, - play: false, - loop: false, */ - lazyLoadQueue: EmoticonsDropdown.lazyLoadQueue, - group: EMOTICONSSTICKERGROUP, - onlyThumb: doc.sticker == 2 - }); - - return div; - } - - async renderStickerSet(set: MTStickerSet, prepend = false) { - let categoryDiv = document.createElement('div'); - categoryDiv.classList.add('sticker-category'); - - let li = document.createElement('li'); - li.classList.add('btn-icon'); - - this.stickerSets[set.id] = { - stickers: categoryDiv, - tab: li - }; - - if(prepend) { - this.menu.insertBefore(li, this.menu.firstElementChild.nextSibling); - } else { - this.menu.append(li); - } - - //stickersScroll.append(categoryDiv); - - let stickerSet = await appStickersManager.getStickerSet(set); - - //console.log('got stickerSet', stickerSet, li); - - if(stickerSet.set.thumb) { - const thumbURL = appStickersManager.getStickerSetThumbURL(stickerSet.set); - - if(stickerSet.set.pFlags.animated) { - fetch(thumbURL) - .then(res => res.json()) - .then(json => { - lottieLoader.loadAnimationWorker({ - container: li, - loop: true, - autoplay: false, - animationData: json, - width: 32, - height: 32 - }, EMOTICONSSTICKERGROUP); - }); - } else { - const image = new Image(); - renderImageFromUrl(image, thumbURL, () => { - li.append(image); - }); - } - } else { // as thumb will be used first sticker - wrapSticker({ - doc: stickerSet.documents[0], - div: li as any, - group: EMOTICONSSTICKERGROUP - }); // kostil - } - - this.categoryPush(categoryDiv, stickerSet.set.title, stickerSet.documents, prepend); - } - - init() { - this.content = document.getElementById('content-stickers'); - //let stickersDiv = contentStickersDiv.querySelector('.os-content') as HTMLDivElement; - - this.recentDiv = document.createElement('div'); - this.recentDiv.classList.add('sticker-category'); - - let menuWrapper = this.content.previousElementSibling as HTMLDivElement; - this.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(); - } - } - } - }); */ - - $rootScope.$on('stickers_installed', (e: CustomEvent) => { - const set: MTStickerSet = e.detail; - - if(!this.stickerSets[set.id] && this.mounted) { - this.renderStickerSet(set, true); - } - }); - - $rootScope.$on('stickers_deleted', (e: CustomEvent) => { - const set: MTStickerSet = e.detail; - - if(this.stickerSets[set.id] && this.mounted) { - const elements = this.stickerSets[set.id]; - elements.stickers.remove(); - elements.tab.remove(); - this.setNewHeights(); - delete this.stickerSets[set.id]; - } - }); - - stickersDiv.addEventListener('click', EmoticonsDropdown.onMediaClick); - - let prevCategoryIndex = 0; - this.scroll = new Scrollable(this.content, 'y', 'STICKERS', undefined, undefined, 2); - this.scroll.container.addEventListener('scroll', (e) => { - //animationIntersector.checkAnimations(false, EMOTICONSSTICKERGROUP); - - if(this.heights[1] == 0) { - this.setNewHeights(); - } - - prevCategoryIndex = EmoticonsDropdown.contentOnScroll(this.menu, this.heights, prevCategoryIndex, this.scroll.container, menuScroll); - }); - this.scroll.setVirtualContainer(stickersDiv); - - this.menu.addEventListener('click', () => { - if(this.heights[1] == 0) { - this.setNewHeights(); - } - }); - - EmoticonsDropdown.menuOnClick(this.menu, this.heights, this.scroll, menuScroll); - - const preloader = putPreloader(this.content, true); - - Promise.all([ - appStickersManager.getRecentStickers().then(stickers => { - this.recentStickers = stickers.stickers.slice(0, 20); - - //stickersScroll.prepend(categoryDiv); - - this.stickerSets['recent'] = { - stickers: this.recentDiv, - tab: this.menu.firstElementChild as HTMLElement - }; - - preloader.remove(); - this.categoryPush(this.recentDiv, 'Recent', this.recentStickers, 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) { - this.renderStickerSet(set); - } - }) - ]).finally(() => { - this.mounted = true; - }); - - this.init = null; - } - - pushRecentSticker(doc: MTDocument) { - if(!this.recentDiv.parentElement) { - return; - } - - let div = this.recentDiv.querySelector(`[data-doc-i-d="${doc.id}"]`); - if(!div) { - div = this.renderSticker(doc); - } - - const items = this.recentDiv.lastElementChild; - items.prepend(div); - - if(items.childElementCount > 20) { - (Array.from(items.children) as HTMLElement[]).slice(20).forEach(el => el.remove()); - } - - this.setNewHeights(); - } - - onClose() { - - } -} - -class GifsTab implements EmoticonsTab { - public content: HTMLElement; - - init() { - this.content = document.getElementById('content-gifs'); - const gifsContainer = this.content.firstElementChild as HTMLDivElement; - gifsContainer.addEventListener('click', EmoticonsDropdown.onMediaClick); - - const masonry = new GifsMasonry(gifsContainer); - const scroll = new Scrollable(this.content, 'y', 'GIFS', null); - const preloader = putPreloader(this.content, true); - - 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[] = []; - - preloader.remove(); - res.gifs.forEach((doc, idx) => { - res.gifs[idx] = appDocsManager.saveDoc(doc); - masonry.add(res.gifs[idx], EMOTICONSSTICKERGROUP, EmoticonsDropdown.lazyLoadQueue); - }); - }); - - this.init = null; - } - - onClose() { - - } -} - -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; - - 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) => { - const toElement = (e as any).toElement as Element; - if(toElement && findUpClassName(toElement, 'emoji-dropdown')) { - return; - } - - 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 == 0); - 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', () => { - if(this.tabID == 1) { - appSidebarRight.stickersTab.init(); - } else { - appSidebarRight.gifsTab.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.lockIntersection(); - //EmoticonsDropdown.lazyLoadQueue.unlock(); - animationIntersector.lockIntersectionGroup(EMOTICONSSTICKERGROUP); - - clearTimeout(this.displayTimeout); - this.displayTimeout = setTimeout(() => { - animationIntersector.unlockIntersectionGroup(EMOTICONSSTICKERGROUP); - EmoticonsDropdown.lazyLoadQueue.unlockIntersection(); - }, touchSupport ? 0 : 200); - - /* if(touchSupport) { - this.restoreScroll(); - } */ - } else { - this.element.classList.remove('active'); - - EmoticonsDropdown.lazyLoadQueue.lockIntersection(); - //EmoticonsDropdown.lazyLoadQueue.lock(); - - // нужно залочить группу и выключить стикеры - animationIntersector.lockIntersectionGroup(EMOTICONSSTICKERGROUP); - animationIntersector.checkAnimations(true, EMOTICONSSTICKERGROUP); - - clearTimeout(this.displayTimeout); - this.displayTimeout = setTimeout(() => { - this.element.style.display = 'none'; - - // теперь можно убрать visible, чтобы они не включились после фокуса - animationIntersector.unlockIntersectionGroup(EMOTICONSSTICKERGROUP); - - EmoticonsDropdown.lazyLoadQueue.unlockIntersection(); - }, 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 HTMLElement; - target = findUpTag(target, 'LI'); - - if(!target) { - return; - } - - let index = whichChild(target); - let y = heights[index - 1/* 2 */] || 0; // 10 == padding .scrollable - - //console.log('emoticonsMenuOnClick', index, heights, target); - - /* 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: HTMLElement, menuScroll?: Scrollable) => { - let y = Math.round(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 HTMLElement; - target = findUpTag(target, 'DIV'); - - if(!target) return; - - 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; diff --git a/src/components/emoticonsDropdown/index.ts b/src/components/emoticonsDropdown/index.ts new file mode 100644 index 00000000..2028ea16 --- /dev/null +++ b/src/components/emoticonsDropdown/index.ts @@ -0,0 +1,302 @@ +import LazyLoadQueue from "../lazyLoadQueue"; +import GifsTab from "./tabs/gifs"; +import { touchSupport } from "../../lib/config"; +import { findUpClassName, findUpTag, whichChild } from "../../lib/utils"; +import { horizontalMenu } from "../horizontalMenu"; +import animationIntersector from "../animationIntersector"; +import appSidebarRight from "../../lib/appManagers/appSidebarRight"; +import appImManager from "../../lib/appManagers/appImManager"; +import Scrollable from "../scrollable_new"; +import EmojiTab from "./tabs/emoji"; +import StickersTab from "./tabs/stickers"; + +export const EMOTICONSSTICKERGROUP = 'emoticons-dropdown'; + +export interface EmoticonsTab { + init: () => void, + onCloseAfterTimeout?: () => void +} + +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; + + 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) => { + const toElement = (e as any).toElement as Element; + if(toElement && findUpClassName(toElement, 'emoji-dropdown')) { + return; + } + + 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 == 0); + 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', () => { + if(this.tabID == 1) { + appSidebarRight.stickersTab.init(); + } else { + appSidebarRight.gifsTab.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.lockIntersection(); + //EmoticonsDropdown.lazyLoadQueue.unlock(); + animationIntersector.lockIntersectionGroup(EMOTICONSSTICKERGROUP); + + clearTimeout(this.displayTimeout); + this.displayTimeout = setTimeout(() => { + animationIntersector.unlockIntersectionGroup(EMOTICONSSTICKERGROUP); + EmoticonsDropdown.lazyLoadQueue.unlockIntersection(); + }, touchSupport ? 0 : 200); + + /* if(touchSupport) { + this.restoreScroll(); + } */ + } else { + this.element.classList.remove('active'); + + EmoticonsDropdown.lazyLoadQueue.lockIntersection(); + //EmoticonsDropdown.lazyLoadQueue.lock(); + + // нужно залочить группу и выключить стикеры + animationIntersector.lockIntersectionGroup(EMOTICONSSTICKERGROUP); + animationIntersector.checkAnimations(true, EMOTICONSSTICKERGROUP); + + clearTimeout(this.displayTimeout); + this.displayTimeout = setTimeout(() => { + this.element.style.display = 'none'; + + // теперь можно убрать visible, чтобы они не включились после фокуса + animationIntersector.unlockIntersectionGroup(EMOTICONSSTICKERGROUP); + + EmoticonsDropdown.lazyLoadQueue.unlockIntersection(); + }, 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 HTMLElement; + target = findUpTag(target, 'LI'); + + if(!target) { + return; + } + + let index = whichChild(target); + let y = heights[index - 1/* 2 */] || 0; // 10 == padding .scrollable + + //console.log('emoticonsMenuOnClick', index, heights, target); + + /* 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: HTMLElement, menuScroll?: Scrollable) => { + let y = Math.round(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 HTMLElement; + target = findUpTag(target, 'DIV'); + + if(!target) return; + + 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; \ No newline at end of file diff --git a/src/components/emoticonsDropdown/tabs/emoji.ts b/src/components/emoticonsDropdown/tabs/emoji.ts new file mode 100644 index 00000000..69a5b325 --- /dev/null +++ b/src/components/emoticonsDropdown/tabs/emoji.ts @@ -0,0 +1,209 @@ +import { EmoticonsTab, EmoticonsDropdown } from ".."; +import Scrollable from "../../scrollable_new"; +import Config from "../../../lib/config"; +import { putPreloader } from "../../misc"; +import appStateManager from "../../../lib/appManagers/appStateManager"; +import { RichTextProcessor } from "../../../lib/richtextprocessor"; +import appImManager from "../../../lib/appManagers/appImManager"; + +export default 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; + + if(spanEmoji.firstElementChild) { + (spanEmoji.firstElementChild as HTMLImageElement).setAttribute('loading', 'lazy'); + } + + //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() { + + } +} \ No newline at end of file diff --git a/src/components/emoticonsDropdown/tabs/gifs.ts b/src/components/emoticonsDropdown/tabs/gifs.ts new file mode 100644 index 00000000..742ee971 --- /dev/null +++ b/src/components/emoticonsDropdown/tabs/gifs.ts @@ -0,0 +1,44 @@ +import { EmoticonsDropdown, EmoticonsTab, EMOTICONSSTICKERGROUP } from ".."; +import GifsMasonry from "../../gifsMasonry"; +import Scrollable from "../../scrollable_new"; +import { putPreloader } from "../../misc"; +import apiManager from "../../../lib/mtproto/mtprotoworker"; +import { MTDocument } from "../../../types"; +import appDocsManager from "../../../lib/appManagers/appDocsManager"; + +export default class GifsTab implements EmoticonsTab { + public content: HTMLElement; + + init() { + this.content = document.getElementById('content-gifs'); + const gifsContainer = this.content.firstElementChild as HTMLDivElement; + gifsContainer.addEventListener('click', EmoticonsDropdown.onMediaClick); + + const masonry = new GifsMasonry(gifsContainer); + const scroll = new Scrollable(this.content, 'y', 'GIFS', null); + const preloader = putPreloader(this.content, true); + + 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[] = []; + + preloader.remove(); + res.gifs.forEach((doc, idx) => { + res.gifs[idx] = appDocsManager.saveDoc(doc); + masonry.add(res.gifs[idx], EMOTICONSSTICKERGROUP, EmoticonsDropdown.lazyLoadQueue); + }); + }); + + this.init = null; + } + + onClose() { + + } +} \ No newline at end of file diff --git a/src/components/emoticonsDropdown/tabs/stickers.ts b/src/components/emoticonsDropdown/tabs/stickers.ts new file mode 100644 index 00000000..47af0203 --- /dev/null +++ b/src/components/emoticonsDropdown/tabs/stickers.ts @@ -0,0 +1,318 @@ +import { EmoticonsTab, EMOTICONSSTICKERGROUP, EmoticonsDropdown } from ".."; +import { MTDocument } from "../../../types"; +import Scrollable from "../../scrollable_new"; +import { wrapSticker } from "../../wrappers"; +import appStickersManager, { MTStickerSet } from "../../../lib/appManagers/appStickersManager"; +import appDownloadManager from "../../../lib/appManagers/appDownloadManager"; +import { readBlobAsText } from "../../../helpers/blob"; +import lottieLoader from "../../../lib/lottieLoader"; +import { renderImageFromUrl, putPreloader } from "../../misc"; +import { RichTextProcessor } from "../../../lib/richtextprocessor"; +import { $rootScope } from "../../../lib/utils"; +import apiManager from "../../../lib/mtproto/mtprotoworker"; + +export default class StickersTab implements EmoticonsTab { + public content: HTMLElement; + + private stickerSets: {[id: string]: { + stickers: HTMLElement, + tab: HTMLElement + }} = {}; + + private recentDiv: HTMLElement; + private recentStickers: MTDocument[] = []; + + private heights: number[] = []; + private heightRAF = 0; + private scroll: Scrollable; + + private menu: HTMLUListElement; + + private mounted = false; + + categoryPush(categoryDiv: HTMLElement, categoryTitle: string, docs: MTDocument[], prepend?: boolean) { + //if((docs.length % 5) != 0) categoryDiv.classList.add('not-full'); + + const itemsDiv = document.createElement('div'); + itemsDiv.classList.add('category-items'); + + const titleDiv = document.createElement('div'); + titleDiv.classList.add('category-title'); + titleDiv.innerHTML = categoryTitle; + + categoryDiv.append(titleDiv, itemsDiv); + + docs.forEach(doc => { + itemsDiv.append(this.renderSticker(doc)); + }); + + if(prepend) { + if(this.recentDiv.parentElement) { + this.scroll.prepend(categoryDiv); + this.scroll.prepend(this.recentDiv); + } else { + this.scroll.prepend(categoryDiv); + } + } else this.scroll.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; + } */ + + this.setNewHeights(); + + /* Array.from(stickersDiv.children).forEach((div, i) => { + heights[i] = (heights[i - 1] || 0) + div.scrollHeight; + }); */ + + //this.scroll.onScroll(); + + //return heights.push(prevHeight + scrollHeight) - 1; + } + + setNewHeights() { + if(this.heightRAF) return; + //if(this.heightRAF) window.cancelAnimationFrame(this.heightRAF); + this.heightRAF = window.requestAnimationFrame(() => { + this.heightRAF = 0; + + const heights = this.heights; + + let paddingTop = parseInt(window.getComputedStyle(this.scroll.container).getPropertyValue('padding-top')) || 0; + + heights.length = 0; + /* let concated = this.scroll.hiddenElements.up.concat(this.scroll.visibleElements, this.scroll.hiddenElements.down); + concated.forEach((el, i) => { + heights[i] = (heights[i - 1] || 0) + el.height + (i == 0 ? paddingTop : 0); + }); */ + let concated = Array.from(this.scroll.splitUp.children) as HTMLElement[]; + concated.forEach((el, i) => { + heights[i] = (heights[i - 1] || 0) + el.scrollHeight + (i == 0 ? paddingTop : 0); + }); + + this.scroll.reorder(); + + //console.log('stickers concated', concated, heights); + }); + } + + renderSticker(doc: MTDocument) { + const div = document.createElement('div'); + wrapSticker({ + doc, + div, + /* width: 80, + height: 80, + play: false, + loop: false, */ + lazyLoadQueue: EmoticonsDropdown.lazyLoadQueue, + group: EMOTICONSSTICKERGROUP, + onlyThumb: doc.sticker == 2 + }); + + return div; + } + + async renderStickerSet(set: MTStickerSet, prepend = false) { + const categoryDiv = document.createElement('div'); + categoryDiv.classList.add('sticker-category'); + + const li = document.createElement('li'); + li.classList.add('btn-icon'); + + this.stickerSets[set.id] = { + stickers: categoryDiv, + tab: li + }; + + if(prepend) { + this.menu.insertBefore(li, this.menu.firstElementChild.nextSibling); + } else { + this.menu.append(li); + } + + //stickersScroll.append(categoryDiv); + + const stickerSet = await appStickersManager.getStickerSet(set); + + //console.log('got stickerSet', stickerSet, li); + + if(stickerSet.set.thumb) { + const downloadOptions = appStickersManager.getStickerSetThumbDownloadOptions(stickerSet.set); + const promise = appDownloadManager.download(downloadOptions); + + if(stickerSet.set.pFlags.animated) { + promise + .then(readBlobAsText) + .then(JSON.parse) + .then(json => { + lottieLoader.loadAnimationWorker({ + container: li, + loop: true, + autoplay: false, + animationData: json, + width: 32, + height: 32 + }, EMOTICONSSTICKERGROUP); + }); + } else { + const image = new Image(); + promise.then(blob => { + 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 + } + + this.categoryPush(categoryDiv, RichTextProcessor.wrapEmojiText(stickerSet.set.title), stickerSet.documents, prepend); + } + + init() { + this.content = document.getElementById('content-stickers'); + //let stickersDiv = contentStickersDiv.querySelector('.os-content') as HTMLDivElement; + + this.recentDiv = document.createElement('div'); + this.recentDiv.classList.add('sticker-category'); + + let menuWrapper = this.content.previousElementSibling as HTMLDivElement; + this.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(); + } + } + } + }); */ + + $rootScope.$on('stickers_installed', (e: CustomEvent) => { + const set: MTStickerSet = e.detail; + + if(!this.stickerSets[set.id] && this.mounted) { + this.renderStickerSet(set, true); + } + }); + + $rootScope.$on('stickers_deleted', (e: CustomEvent) => { + const set: MTStickerSet = e.detail; + + if(this.stickerSets[set.id] && this.mounted) { + const elements = this.stickerSets[set.id]; + elements.stickers.remove(); + elements.tab.remove(); + this.setNewHeights(); + delete this.stickerSets[set.id]; + } + }); + + stickersDiv.addEventListener('click', EmoticonsDropdown.onMediaClick); + + let prevCategoryIndex = 0; + this.scroll = new Scrollable(this.content, 'y', 'STICKERS', undefined, undefined, 2); + this.scroll.container.addEventListener('scroll', (e) => { + //animationIntersector.checkAnimations(false, EMOTICONSSTICKERGROUP); + + if(this.heights[1] == 0) { + this.setNewHeights(); + } + + prevCategoryIndex = EmoticonsDropdown.contentOnScroll(this.menu, this.heights, prevCategoryIndex, this.scroll.container, menuScroll); + }); + this.scroll.setVirtualContainer(stickersDiv); + + this.menu.addEventListener('click', () => { + if(this.heights[1] == 0) { + this.setNewHeights(); + } + }); + + EmoticonsDropdown.menuOnClick(this.menu, this.heights, this.scroll, menuScroll); + + const preloader = putPreloader(this.content, true); + + Promise.all([ + appStickersManager.getRecentStickers().then(stickers => { + this.recentStickers = stickers.stickers.slice(0, 20); + + //stickersScroll.prepend(categoryDiv); + + this.stickerSets['recent'] = { + stickers: this.recentDiv, + tab: this.menu.firstElementChild as HTMLElement + }; + + preloader.remove(); + this.categoryPush(this.recentDiv, 'Recent', this.recentStickers, 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) { + this.renderStickerSet(set); + } + }) + ]).finally(() => { + this.mounted = true; + }); + + this.init = null; + } + + pushRecentSticker(doc: MTDocument) { + if(!this.recentDiv.parentElement) { + return; + } + + let div = this.recentDiv.querySelector(`[data-doc-i-d="${doc.id}"]`); + if(!div) { + div = this.renderSticker(doc); + } + + const items = this.recentDiv.lastElementChild; + items.prepend(div); + + if(items.childElementCount > 20) { + (Array.from(items.children) as HTMLElement[]).slice(20).forEach(el => el.remove()); + } + + this.setNewHeights(); + } + + onClose() { + + } +} \ No newline at end of file diff --git a/src/components/gifsMasonry.ts b/src/components/gifsMasonry.ts index 8a38fa42..85223311 100644 --- a/src/components/gifsMasonry.ts +++ b/src/components/gifsMasonry.ts @@ -49,11 +49,18 @@ export default class GifsMasonry { //let preloader = new ProgressivePreloader(div); - const posterURL = appDocsManager.getThumbURL(doc, false); + const gotThumb = appDocsManager.getThumb(doc, false); + + const willBeAPoster = !!gotThumb; let img: HTMLImageElement; - if(posterURL) { + if(willBeAPoster) { img = new Image(); - img.src = posterURL; + + if(!gotThumb.thumb.url) { + gotThumb.promise.then(() => { + img.src = gotThumb.thumb.url; + }); + } } let mouseOut = false; @@ -124,6 +131,6 @@ export default class GifsMasonry { } }; - (posterURL ? renderImageFromUrl(img, posterURL, afterRender) : afterRender()); + (gotThumb?.thumb?.url ? renderImageFromUrl(img, gotThumb.thumb.url, afterRender) : afterRender()); } } \ No newline at end of file diff --git a/src/components/wrappers.ts b/src/components/wrappers.ts index 19e81a82..80e82aff 100644 --- a/src/components/wrappers.ts +++ b/src/components/wrappers.ts @@ -6,7 +6,7 @@ import ProgressivePreloader from './preloader'; import LazyLoadQueue from './lazyLoadQueue'; import VideoPlayer from '../lib/mediaPlayer'; import { RichTextProcessor } from '../lib/richtextprocessor'; -import { renderImageFromUrl, loadedURLs } from './misc'; +import { renderImageFromUrl } from './misc'; import appMessagesManager from '../lib/appManagers/appMessagesManager'; import { Layouter, RectPart } from './groupedLayout'; import PollElement from './poll'; @@ -14,8 +14,9 @@ import { mediaSizes, isSafari } from '../lib/config'; import { MTDocument, MTPhotoSize } from '../types'; import animationIntersector from './animationIntersector'; import AudioElement from './audio'; -import appDownloadManager, { Download, Progress, DownloadBlob } from '../lib/appManagers/appDownloadManager'; -import { webpWorkerController } from '../lib/webp/webpWorkerController'; +import { DownloadBlob } from '../lib/appManagers/appDownloadManager'; +import webpWorkerController from '../lib/webp/webpWorkerController'; +import { readBlobAsText } from '../helpers/blob'; export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTail, isOut, middleware, lazyLoadQueue, noInfo, group}: { doc: MTDocument, @@ -92,9 +93,11 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai } if(!img?.parentElement) { - const posterURL = appDocsManager.getThumbURL(doc, false); - if(posterURL) { - video.poster = posterURL; + const gotThumb = appDocsManager.getThumb(doc, false); + if(gotThumb) { + gotThumb.promise.then(() => { + video.poster = gotThumb.thumb.url; + }); } } @@ -379,16 +382,7 @@ export function wrapPhoto(photo: MTPhoto | MTDocument, message: any, container: if(preloader) { preloader.attach(container, true, promise); } - - /* const url = appPhotosManager.getPhotoURL(photoID, size); - return renderImageFromUrl(image || container, url).then(() => { - photo.downloaded = true; - }); */ - - /* if(preloader) { - preloader.attach(container, true, promise); - } */ - + return promise.then(() => { if(middleware && !middleware()) return; @@ -413,7 +407,7 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o withThumb?: boolean, loop?: boolean }) { - let stickerType = doc.sticker; + const stickerType = doc.sticker; if(!width) { width = !emoji ? 200 : undefined; @@ -439,8 +433,8 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o const toneIndex = emoji ? getEmojiToneIndex(emoji) : -1; - if(doc.thumbs && !div.firstElementChild && (!doc.downloaded || stickerType == 2)) { - let thumb = doc.thumbs[0]; + if(doc.thumbs?.length && !div.firstElementChild && (!doc.downloaded || stickerType == 2 || onlyThumb) && toneIndex <= 0) { + const thumb = doc.thumbs[0]; //console.log('wrap sticker', thumb, div); @@ -454,56 +448,50 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o if(thumb.bytes || thumb.url) { img = new Image(); - if((!isSafari || doc.stickerThumbConverted)/* && false */) { + if((!isSafari || doc.stickerThumbConverted || thumb.url)/* && false */) { renderImageFromUrl(img, appPhotosManager.getPreviewURLFromThumb(thumb, true), afterRender); } else { webpWorkerController.convert(doc.id, thumb.bytes).then(bytes => { - if(middleware && !middleware()) return; - thumb.bytes = bytes; doc.stickerThumbConverted = true; + + if(middleware && !middleware()) return; if(!div.childElementCount) { renderImageFromUrl(img, appPhotosManager.getPreviewURLFromThumb(thumb, true), afterRender); } - }); + }).catch(() => {}); } - - if(onlyThumb) { - return Promise.resolve(); - } - } else if(!onlyThumb && stickerType == 2 && withThumb && toneIndex <= 0) { + } else if(stickerType == 2 && (withThumb || onlyThumb)) { img = new Image(); - + const load = () => { if(div.childElementCount || (middleware && !middleware())) return; - renderImageFromUrl(img, appDocsManager.getFileURL(doc, false, thumb), afterRender); + + const r = () => { + if(div.childElementCount || (middleware && !middleware())) return; + renderImageFromUrl(img, thumb.url, afterRender); + }; + + if(thumb.url) { + r(); + return Promise.resolve(); + } else { + return appDocsManager.getThumbURL(doc, thumb).promise.then(r); + } }; - /* let downloaded = appDocsManager.hasDownloadedThumb(doc.id, thumb.type); - if(downloaded) { - div.append(img); - } */ - - //lazyLoadQueue && !downloaded ? lazyLoadQueue.push({div, load, wasSeen: group == 'chat'}) : load(); - load(); + if(lazyLoadQueue && onlyThumb) { + lazyLoadQueue.push({div, load}); + return Promise.resolve(); + } else { + load(); + } } } - if(onlyThumb && doc.thumbs) { // for sticker panel - let thumb = doc.thumbs[0]; - - let load = () => { - let img = new Image(); - renderImageFromUrl(img, appDocsManager.getFileURL(doc, false, thumb), () => { - if(middleware && !middleware()) return; - div.append(img); - }); - - return Promise.resolve(); - }; - - return lazyLoadQueue ? (lazyLoadQueue.push({div, load}), Promise.resolve()) : load(); + if(onlyThumb) { // for sticker panel + return Promise.resolve(); } let downloaded = doc.downloaded; @@ -519,13 +507,14 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o //appDocsManager.downloadDocNew(doc.id).promise.then(res => res.json()).then(async(json) => { //fetch(doc.url).then(res => res.json()).then(async(json) => { - appDownloadManager.download(doc.url, appDocsManager.getInputFileName(doc), 'json').then(async(json) => { + appDocsManager.downloadDocNew(doc.id) + .then(readBlobAsText) + .then(JSON.parse) + .then(async(json) => { //console.timeEnd('download sticker' + doc.id); - //console.log('loaded sticker:', doc, div); + //console.log('loaded sticker:', doc, div, blob); if(middleware && !middleware()) return; - //await new Promise((resolve) => setTimeout(resolve, 5e3)); - let animation = await LottieLoader.loadAnimationWorker/* loadAnimation */({ container: div, loop: loop && !emoji, @@ -534,7 +523,7 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o width, height }, group, toneIndex); - + animation.addListener('firstFrame', () => { if(div.firstElementChild && div.firstElementChild.tagName == 'IMG') { div.firstElementChild.remove(); @@ -542,16 +531,17 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o animation.canvas.classList.add('fade-in'); } }, true); - + if(emoji) { div.addEventListener('click', () => { let animation = LottieLoader.getAnimation(div); - + if(animation.paused) { animation.restart(); } }); } + //await new Promise((resolve) => setTimeout(resolve, 5e3)); }); //console.timeEnd('render sticker' + doc.id); @@ -571,13 +561,22 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o }); } - renderImageFromUrl(img, doc.url, () => { - if(div.firstElementChild && div.firstElementChild != img) { - div.firstElementChild.remove(); - } + const r = () => { + if(middleware && !middleware()) return; - div.append(img); - }); + renderImageFromUrl(img, doc.url, () => { + if(div.firstElementChild && div.firstElementChild != img) { + div.firstElementChild.remove(); + } + + div.append(img); + }); + }; + + if(doc.url) r(); + else { + appDocsManager.downloadDocNew(doc).then(r); + } } }; diff --git a/src/helpers/blob.ts b/src/helpers/blob.ts new file mode 100644 index 00000000..e36cc1c5 --- /dev/null +++ b/src/helpers/blob.ts @@ -0,0 +1,10 @@ +export const readBlobAsText = (blob: Blob) => { + return new Promise(resolve => { + const reader = new FileReader(); + reader.addEventListener('loadend', async(e) => { + // @ts-ignore + resolve(e.srcElement.result); + }); + reader.readAsText(blob); + }); +}; \ No newline at end of file diff --git a/src/helpers/context.ts b/src/helpers/context.ts new file mode 100644 index 00000000..01c6ec6a --- /dev/null +++ b/src/helpers/context.ts @@ -0,0 +1,29 @@ +export const isWebWorker = typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope; +export const isServiceWorker = typeof ServiceWorkerGlobalScope !== 'undefined' && self instanceof ServiceWorkerGlobalScope; +export const isWorker = isWebWorker || isServiceWorker; + +// в SW может быть сразу две переменных TRUE, поэтому проверяю по последней + +const notifyServiceWorker = (...args: any[]) => { + (self as any as ServiceWorkerGlobalScope) + .clients + .matchAll({ includeUncontrolled: false, type: 'window' }) + .then((listeners) => { + if(!listeners.length) { + //console.trace('no listeners?', self, listeners); + return; + } + + // @ts-ignore + listeners[0].postMessage(...args); + }); +}; + +const notifyWorker = (...args: any[]) => { + // @ts-ignore + (self as any as DedicatedWorkerGlobalScope).postMessage(...args); +}; + +const empty = () => {}; + +export const notifySomeone = isServiceWorker ? notifyServiceWorker : (isWebWorker ? notifyWorker : empty); \ No newline at end of file diff --git a/src/lib/appManagers/appDocsManager.ts b/src/lib/appManagers/appDocsManager.ts index 35c2a584..9c320345 100644 --- a/src/lib/appManagers/appDocsManager.ts +++ b/src/lib/appManagers/appDocsManager.ts @@ -1,23 +1,21 @@ import {RichTextProcessor} from '../richtextprocessor'; -import { CancellablePromise, deferredPromise } from '../polyfill'; import { isObject, getFileURL, FileURLType } from '../utils'; import opusDecodeController from '../opusDecodeController'; import { MTDocument, inputDocumentFileLocation, MTPhotoSize } from '../../types'; import { getFileNameByLocation } from '../bin_utils'; -import appDownloadManager, { Download, ResponseMethod, DownloadBlob } from './appDownloadManager'; +import appDownloadManager, { DownloadBlob } from './appDownloadManager'; import appPhotosManager from './appPhotosManager'; class AppDocsManager { private docs: {[docID: string]: MTDocument} = {}; - private downloadPromises: {[docID: string]: CancellablePromise} = {}; - - public saveDoc(apiDoc: MTDocument, context?: any) { - //console.log('saveDoc', apiDoc, this.docs[apiDoc.id]); - if(this.docs[apiDoc.id]) { - const d = this.docs[apiDoc.id]; - if(apiDoc.thumbs) { - if(!d.thumbs) d.thumbs = apiDoc.thumbs; + public saveDoc(doc: MTDocument, context?: any) { + //console.log('saveDoc', apiDoc, this.docs[apiDoc.id]); + if(this.docs[doc.id]) { + const d = this.docs[doc.id]; + + if(doc.thumbs) { + if(!d.thumbs) d.thumbs = doc.thumbs; /* else if(apiDoc.thumbs[0].bytes && !d.thumbs[0].bytes) { d.thumbs.unshift(apiDoc.thumbs[0]); } else if(d.thumbs[0].url) { // fix for converted thumb in safari @@ -25,7 +23,7 @@ class AppDocsManager { } */ } - d.file_reference = apiDoc.file_reference; + d.file_reference = doc.file_reference; return d; //return Object.assign(d, apiDoc, context); @@ -33,22 +31,22 @@ class AppDocsManager { } if(context) { - Object.assign(apiDoc, context); + Object.assign(doc, context); } - this.docs[apiDoc.id] = apiDoc; + this.docs[doc.id] = doc; - apiDoc.attributes.forEach((attribute: any) => { + doc.attributes.forEach((attribute: any) => { switch(attribute._) { case 'documentAttributeFilename': - apiDoc.file_name = RichTextProcessor.wrapPlainText(attribute.file_name); + doc.file_name = RichTextProcessor.wrapPlainText(attribute.file_name); break; case 'documentAttributeAudio': - apiDoc.duration = attribute.duration; - apiDoc.audioTitle = attribute.title; - apiDoc.audioPerformer = attribute.performer; - apiDoc.type = attribute.pFlags.voice && apiDoc.mime_type == "audio/ogg" ? 'voice' : 'audio'; + doc.duration = attribute.duration; + doc.audioTitle = attribute.title; + doc.audioPerformer = attribute.performer; + doc.type = attribute.pFlags.voice && doc.mime_type == "audio/ogg" ? 'voice' : 'audio'; /* if(apiDoc.type == 'audio') { apiDoc.supportsStreaming = true; @@ -56,97 +54,98 @@ class AppDocsManager { break; case 'documentAttributeVideo': - apiDoc.duration = attribute.duration; - apiDoc.w = attribute.w; - apiDoc.h = attribute.h; + doc.duration = attribute.duration; + doc.w = attribute.w; + doc.h = attribute.h; //apiDoc.supportsStreaming = attribute.pFlags?.supports_streaming/* && apiDoc.size > 524288 */; if(/* apiDoc.thumbs && */attribute.pFlags.round_message) { - apiDoc.type = 'round'; + doc.type = 'round'; } else /* if(apiDoc.thumbs) */ { - apiDoc.type = 'video'; + doc.type = 'video'; } break; case 'documentAttributeSticker': if(attribute.alt !== undefined) { - apiDoc.stickerEmojiRaw = attribute.alt; - apiDoc.stickerEmoji = RichTextProcessor.wrapRichText(apiDoc.stickerEmojiRaw, {noLinks: true, noLinebreaks: true}); + doc.stickerEmojiRaw = attribute.alt; + doc.stickerEmoji = RichTextProcessor.wrapRichText(doc.stickerEmojiRaw, {noLinks: true, noLinebreaks: true}); } if(attribute.stickerset) { if(attribute.stickerset._ == 'inputStickerSetEmpty') { delete attribute.stickerset; } else if(attribute.stickerset._ == 'inputStickerSetID') { - apiDoc.stickerSetInput = attribute.stickerset; + doc.stickerSetInput = attribute.stickerset; } } - if(/* apiDoc.thumbs && */apiDoc.mime_type == 'image/webp') { - apiDoc.type = 'sticker'; - apiDoc.sticker = 1; + if(/* apiDoc.thumbs && */doc.mime_type == 'image/webp') { + doc.type = 'sticker'; + doc.sticker = 1; } break; case 'documentAttributeImageSize': - apiDoc.w = attribute.w; - apiDoc.h = attribute.h; + doc.w = attribute.w; + doc.h = attribute.h; break; case 'documentAttributeAnimated': - if((apiDoc.mime_type == 'image/gif' || apiDoc.mime_type == 'video/mp4')/* && apiDoc.thumbs */) { - apiDoc.type = 'gif'; + if((doc.mime_type == 'image/gif' || doc.mime_type == 'video/mp4')/* && apiDoc.thumbs */) { + doc.type = 'gif'; } - apiDoc.animated = true; + doc.animated = true; break; } }); - if(!apiDoc.mime_type) { - switch(apiDoc.type) { + if(!doc.mime_type) { + switch(doc.type) { case 'gif': case 'video': case 'round': - apiDoc.mime_type = 'video/mp4'; + doc.mime_type = 'video/mp4'; break; case 'sticker': - apiDoc.mime_type = 'image/webp'; + doc.mime_type = 'image/webp'; break; case 'audio': - apiDoc.mime_type = 'audio/mpeg'; + doc.mime_type = 'audio/mpeg'; break; case 'voice': - apiDoc.mime_type = 'audio/ogg'; + doc.mime_type = 'audio/ogg'; break; default: - apiDoc.mime_type = 'application/octet-stream'; + doc.mime_type = 'application/octet-stream'; break; } } - if((apiDoc.type == 'gif' && apiDoc.size > 8e6) || apiDoc.type == 'audio' || apiDoc.type == 'video') { - apiDoc.supportsStreaming = true; + if((doc.type == 'gif' && doc.size > 8e6) || doc.type == 'audio' || doc.type == 'video') { + doc.supportsStreaming = true; + doc.url = this.getFileURL(doc); } - if(!apiDoc.file_name) { - apiDoc.file_name = ''; + if(!doc.file_name) { + doc.file_name = ''; } - if(apiDoc.mime_type == 'application/x-tgsticker' && apiDoc.file_name == "AnimatedSticker.tgs") { - apiDoc.type = 'sticker'; - apiDoc.animated = true; - apiDoc.sticker = 2; + if(doc.mime_type == 'application/x-tgsticker' && doc.file_name == "AnimatedSticker.tgs") { + doc.type = 'sticker'; + doc.animated = true; + doc.sticker = 2; } - if(apiDoc._ == 'documentEmpty') { - apiDoc.size = 0; + if(doc._ == 'documentEmpty') { + doc.size = 0; } - if(!apiDoc.url) { - apiDoc.url = this.getFileURL(apiDoc); - } + /* if(!doc.url) { + doc.url = this.getFileURL(doc); + } */ - return apiDoc; + return doc; } public getDoc(docID: string | MTDocument): MTDocument { @@ -177,9 +176,26 @@ class AppDocsManager { }; } - public getFileURL(doc: MTDocument, download = false, thumb?: MTPhotoSize) { + public getFileDownloadOptions(doc: MTDocument, thumb?: MTPhotoSize) { const inputFileLocation = this.getInput(doc, thumb?.type); + let mimeType: string; + if(thumb) { + mimeType = doc.sticker ? 'image/webp' : 'image/jpeg'/* doc.mime_type */; + } else { + mimeType = doc.mime_type || 'application/octet-stream'; + } + + return { + dcID: doc.dc_id, + location: inputFileLocation, + size: thumb ? thumb.size : doc.size, + mimeType: mimeType, + fileName: doc.file_name + }; + } + + public getFileURL(doc: MTDocument, download = false, thumb?: MTPhotoSize) { let type: FileURLType; if(download) { type = 'download'; @@ -191,23 +207,25 @@ class AppDocsManager { type = 'document'; } - let mimeType: string; - if(thumb) { - mimeType = doc.sticker ? 'image/webp' : 'image/jpeg'/* doc.mime_type */; - } else { - mimeType = doc.mime_type || 'application/octet-stream'; - } - - return getFileURL(type, { - dcID: doc.dc_id, - location: inputFileLocation, - size: thumb ? thumb.size : doc.size, - mimeType: mimeType, - fileName: doc.file_name - }); + return getFileURL(type, this.getFileDownloadOptions(doc, thumb)); } - public getThumbURL(doc: MTDocument, useBytes = true) { + public getThumbURL(doc: MTDocument, thumb: MTPhotoSize) { + let promise: Promise = Promise.resolve(); + + if(!thumb.url) { + if(thumb.bytes) { + thumb.url = appPhotosManager.getPreviewURLFromBytes(thumb.bytes, !!doc.sticker); + } else { + //return this.getFileURL(doc, false, thumb); + promise = this.downloadDocNew(doc, thumb); + } + } + + return {thumb, promise}; + } + + public getThumb(doc: MTDocument, useBytes = true) { if(doc.thumbs?.length) { let thumb: MTPhotoSize; if(!useBytes) { @@ -218,43 +236,43 @@ class AppDocsManager { thumb = doc.thumbs[0]; } - if(thumb.bytes) { - return appPhotosManager.getPreviewURLFromBytes(doc.thumbs[0].bytes, !!doc.sticker); - } else { - return this.getFileURL(doc, false, thumb); - } + return this.getThumbURL(doc, thumb); } - return ''; + return null; } public getInputFileName(doc: MTDocument, thumbSize?: string) { return getFileNameByLocation(this.getInput(doc, thumbSize), {fileName: doc.file_name}); } - public downloadDocNew(docID: string | MTDocument/* , method: ResponseMethod = 'blob' */): DownloadBlob { + public downloadDocNew(docID: string | MTDocument, thumb?: MTPhotoSize): DownloadBlob { const doc = this.getDoc(docID); if(doc._ == 'documentEmpty') { throw new Error('Document empty!'); } - const fileName = this.getInputFileName(doc); + const fileName = this.getInputFileName(doc, thumb?.type); let download: DownloadBlob = appDownloadManager.getDownload(fileName); if(download) { return download; } - download = appDownloadManager.download(doc.url, fileName/* , method */); + const downloadOptions = this.getFileDownloadOptions(doc, thumb); + download = appDownloadManager.download(downloadOptions); const originalPromise = download; originalPromise.then((blob) => { - doc.downloaded = true; - - if(!doc.supportsStreaming) { + if(thumb) { + thumb.url = URL.createObjectURL(blob); + return; + } else if(!doc.supportsStreaming) { doc.url = URL.createObjectURL(blob); } + + doc.downloaded = true; }); if(doc.type == 'voice' && !opusDecodeController.isPlaySupported()) { @@ -278,8 +296,6 @@ class AppDocsManager { }); return blob; - //return originalPromise; - //return new Response(blob); }); } @@ -287,10 +303,8 @@ class AppDocsManager { } public saveDocFile(doc: MTDocument) { - const url = this.getFileURL(doc, true); - const fileName = this.getInputFileName(doc); - - return appDownloadManager.downloadToDisc(fileName, url, doc.file_name); + const options = this.getFileDownloadOptions(doc); + return appDownloadManager.downloadToDisc(options, doc.file_name); } } diff --git a/src/lib/appManagers/appDownloadManager.ts b/src/lib/appManagers/appDownloadManager.ts index 70a2e614..06983b58 100644 --- a/src/lib/appManagers/appDownloadManager.ts +++ b/src/lib/appManagers/appDownloadManager.ts @@ -1,6 +1,8 @@ import { $rootScope } from "../utils"; import apiManager from "../mtproto/mtprotoworker"; import { deferredPromise, CancellablePromise } from "../polyfill"; +import type { DownloadOptions } from "../mtproto/apiFileManager"; +import { getFileNameByLocation } from "../bin_utils"; export type ResponseMethodBlob = 'blob'; export type ResponseMethodJson = 'json'; @@ -38,40 +40,24 @@ export class AppDownloadManager { }); } - public download(url: string, fileName: string, responseMethod?: ResponseMethodBlob): DownloadBlob; - public download(url: string, fileName: string, responseMethod?: ResponseMethodJson): DownloadJson; - public download(url: string, fileName: string, responseMethod: ResponseMethod = 'blob'): DownloadBlob { + public download(options: DownloadOptions, responseMethod?: ResponseMethodBlob): DownloadBlob; + public download(options: DownloadOptions, responseMethod?: ResponseMethodJson): DownloadJson; + public download(options: DownloadOptions, responseMethod: ResponseMethod = 'blob'): DownloadBlob { + const fileName = getFileNameByLocation(options.location, {fileName: options.fileName}); + if(this.downloads.hasOwnProperty(fileName)) return this.downloads[fileName]; const deferred = deferredPromise(); - const controller = new AbortController(); - const promise = fetch(url, {signal: controller.signal}) - .then(res => res[responseMethod]()) - .then(res => deferred.resolve(res)) - .catch(err => { // Только потому что event.request.signal не работает в SW, либо я кривой? - if(err.name === 'AbortError') { - //console.log('Fetch aborted'); - apiManager.cancelDownload(fileName); - delete this.downloads[fileName]; - delete this.progress[fileName]; - delete this.progressCallbacks[fileName]; - } else { - //console.error('Uh oh, an error!', err); - } - - deferred.reject(err); - throw err; + apiManager.downloadFile(options) + .then(deferred.resolve, deferred.reject) + .finally(() => { + delete this.progressCallbacks[fileName]; }); //console.log('Will download file:', fileName, url); - promise.finally(() => { - delete this.progressCallbacks[fileName]; - }); - deferred.cancel = () => { - controller.abort(); deferred.cancel = () => {}; }; @@ -129,8 +115,8 @@ export class AppDownloadManager { return this.download(fileName, url); } */ - public downloadToDisc(fileName: string, url: string, discFileName: string) { - const download = this.download(url, fileName); + public downloadToDisc(options: DownloadOptions, discFileName: string) { + const download = this.download(options); download/* .promise */.then(blob => { const objectURL = URL.createObjectURL(blob); this.createDownloadAnchor(objectURL, discFileName, () => { diff --git a/src/lib/appManagers/appPhotosManager.ts b/src/lib/appManagers/appPhotosManager.ts index 28eaf04c..0b7e31b0 100644 --- a/src/lib/appManagers/appPhotosManager.ts +++ b/src/lib/appManagers/appPhotosManager.ts @@ -1,8 +1,8 @@ -import { calcImageInBox, isObject, getFileURL } from "../utils"; +import { calcImageInBox, isObject } from "../utils"; import { bytesFromHex, getFileNameByLocation } from "../bin_utils"; import { MTPhotoSize, inputPhotoFileLocation, inputDocumentFileLocation, FileLocation, MTDocument } from "../../types"; -import appDownloadManager, { Download } from "./appDownloadManager"; -import { deferredPromise, CancellablePromise } from "../polyfill"; +import appDownloadManager from "./appDownloadManager"; +import { CancellablePromise } from "../polyfill"; import { isSafari } from "../../helpers/userAgent"; export type MTPhoto = { @@ -203,8 +203,8 @@ export class AppPhotosManager { return photoSize; } - - public getPhotoURL(photo: MTPhoto | MTDocument, photoSize: MTPhotoSize) { + + public getPhotoDownloadOptions(photo: MTPhoto | MTDocument, photoSize: MTPhotoSize) { const isDocument = photo._ == 'document'; if(!photoSize || photoSize._ == 'photoSizeEmpty') { @@ -222,8 +222,14 @@ export class AppPhotosManager { thumb_size: photoSize.type } : photoSize.location; - return {url: getFileURL('photo', {dcID: photo.dc_id, location, size: isPhoto ? photoSize.size : undefined}), location}; + return {dcID: photo.dc_id, location, size: isPhoto ? photoSize.size : undefined}; } + + /* public getPhotoURL(photo: MTPhoto | MTDocument, photoSize: MTPhotoSize) { + const downloadOptions = this.getPhotoDownloadOptions(photo, photoSize); + + return {url: getFileURL('photo', downloadOptions), location: downloadOptions.location}; + } */ public preloadPhoto(photoID: any, photoSize?: MTPhotoSize): CancellablePromise { const photo = this.getPhoto(photoID); @@ -240,15 +246,15 @@ export class AppPhotosManager { return Promise.resolve() as any; } - const {url, location} = this.getPhotoURL(photo, photoSize); - const fileName = getFileNameByLocation(location); + const downloadOptions = this.getPhotoDownloadOptions(photo, photoSize); + const fileName = getFileNameByLocation(downloadOptions.location); let download = appDownloadManager.getDownload(fileName); if(download) { return download; } - download = appDownloadManager.download(url, fileName); + download = appDownloadManager.download(downloadOptions); download.then(blob => { if(!cacheContext.downloaded || cacheContext.downloaded < blob.size) { cacheContext.downloaded = blob.size; @@ -261,7 +267,6 @@ export class AppPhotosManager { }); return download; - //return fetch(url).then(res => res.blob()); } public getCacheContext(photo: any) { @@ -302,10 +307,12 @@ export class AppPhotosManager { thumb_size: fullPhotoSize.type }; - const url = getFileURL('download', {dcID: photo.dc_id, location, size: fullPhotoSize.size, fileName: 'photo' + photo.id + '.jpg'}); - const fileName = getFileNameByLocation(location); - - appDownloadManager.downloadToDisc(fileName, url, 'photo' + photo.id + '.jpg'); + appDownloadManager.downloadToDisc({ + dcID: photo.dc_id, + location, + size: fullPhotoSize.size, + fileName: 'photo' + photo.id + '.jpg' + }, 'photo' + photo.id + '.jpg'); } } diff --git a/src/lib/appManagers/appStickersManager.ts b/src/lib/appManagers/appStickersManager.ts index c1ddc975..0ab9e58a 100644 --- a/src/lib/appManagers/appStickersManager.ts +++ b/src/lib/appManagers/appStickersManager.ts @@ -3,7 +3,7 @@ import AppStorage from '../storage'; import apiManager from '../mtproto/mtprotoworker'; import appDocsManager from './appDocsManager'; import { MTDocument, inputStickerSetThumb } from '../../types'; -import { $rootScope, getFileURL } from '../utils'; +import { $rootScope } from '../utils'; export type MTStickerSet = { _: 'stickerSet', @@ -217,7 +217,7 @@ class AppStickersManager { }, 100); } - public getStickerSetThumbURL(stickerSet: MTStickerSet) { + public getStickerSetThumbDownloadOptions(stickerSet: MTStickerSet) { const thumb = stickerSet.thumb; const dcID = stickerSet.thumb_dc_id; @@ -230,11 +230,27 @@ class AppStickersManager { local_id: thumb.location.local_id }; - const url = getFileURL('document', {dcID, location: input, size: thumb.size, mimeType: isAnimated ? "application/x-tgsticker" : 'image/webp'}); + return {dcID, location: input, size: thumb.size, mimeType: isAnimated ? "application/x-tgsticker" : 'image/webp'}; + } + + /* public getStickerSetThumbURL(stickerSet: MTStickerSet) { + const thumb = stickerSet.thumb; + const dcID = stickerSet.thumb_dc_id; + + const isAnimated = stickerSet.pFlags?.animated; + + const input: inputStickerSetThumb = { + _: 'inputStickerSetThumb', + stickerset: this.getStickerSetInput(stickerSet), + volume_id: thumb.location.volume_id, + local_id: thumb.location.local_id + }; + + const url = getFileURL('document', this.getStickerSetThumbDownloadOptions(stickerSet)); return url; //return promise; - } + } */ public getStickerSetInput(set: {id: string, access_hash: string}) { return set.id == 'emoji' ? { diff --git a/src/lib/mtproto/apiFileManager.ts b/src/lib/mtproto/apiFileManager.ts index da84192d..1d7e512e 100644 --- a/src/lib/mtproto/apiFileManager.ts +++ b/src/lib/mtproto/apiFileManager.ts @@ -8,6 +8,7 @@ import { logger, LogLevels } from "../logger"; import { InputFileLocation, FileLocation, UploadFile } from "../../types"; import { isSafari } from "../../helpers/userAgent"; import cryptoWorker from "../crypto/cryptoworker"; +import { notifySomeone } from "../../helpers/context"; type Delayed = { offset: number, @@ -18,7 +19,7 @@ type Delayed = { export type DownloadOptions = { dcID: number, location: InputFileLocation | FileLocation, - size: number, + size?: number, fileName?: string, mimeType?: string, limitPart?: number, @@ -156,17 +157,8 @@ export class ApiFileManager { convertWebp = (bytes: Uint8Array, fileName: string) => { const convertPromise = deferredPromise(); - (self as any as ServiceWorkerGlobalScope) - .clients - .matchAll({includeUncontrolled: false, type: 'window'}) - .then((listeners) => { - if(!listeners.length) { - return; - } - - listeners[0].postMessage({type: 'convertWebp', payload: {fileName, bytes}}); - }); - + const task = {type: 'convertWebp', payload: {fileName, bytes}}; + notifySomeone(task); return this.webpConvertPromises[fileName] = convertPromise; }; diff --git a/src/lib/mtproto/mtproto.service.ts b/src/lib/mtproto/mtproto.service.ts index caebddc2..bd4f8e27 100644 --- a/src/lib/mtproto/mtproto.service.ts +++ b/src/lib/mtproto/mtproto.service.ts @@ -1,148 +1,38 @@ -// just to include -import {secureRandom} from '../polyfill'; -secureRandom; - -import apiManager from "./apiManager"; -import AppStorage from '../storage'; -import cryptoWorker from "../crypto/cryptoworker"; -import networkerFactory from "./networkerFactory"; -import apiFileManager, { DownloadOptions } from './apiFileManager'; -import { getFileNameByLocation } from '../bin_utils'; -import { logger, LogLevels } from '../logger'; import { isSafari } from '../../helpers/userAgent'; +import { logger, LogLevels } from '../logger'; +import type { DownloadOptions } from './apiFileManager'; +import type { InputFileLocation, FileLocation, UploadFile, WorkerTaskTemplate } from '../../types'; +import { deferredPromise, CancellablePromise } from '../polyfill'; +import { notifySomeone } from '../../helpers/context'; const log = logger('SW', LogLevels.error); - const ctx = self as any as ServiceWorkerGlobalScope; -//console.error('INCLUDE !!!', new Error().stack); +const deferredPromises: {[taskID: number]: CancellablePromise} = {}; -/* function isObject(object: any) { - return typeof(object) === 'object' && object !== null; -} */ +ctx.addEventListener('message', (e) => { + const task = e.data as ServiceWorkerTaskResponse; + const promise = deferredPromises[task.id]; -/* function fillTransfer(transfer: any, obj: any) { - if(!obj) return; - - if(obj instanceof ArrayBuffer) { - transfer.add(obj); - } else if(obj.buffer && obj.buffer instanceof ArrayBuffer) { - transfer.add(obj.buffer); - } else if(isObject(obj)) { - for(var i in obj) { - fillTransfer(transfer, obj[i]); - } - } else if(Array.isArray(obj)) { - obj.forEach(value => { - fillTransfer(transfer, value); - }); + if(task.payload) { + promise.resolve(task.payload); + } else { + promise.reject(); } -} */ -/** - * Respond to request - */ -function respond(client: Client | ServiceWorker | MessagePort, ...args: any[]) { - // отключил для всего потому что не успел пофиксить transfer detached - //if(isSafari(self)/* || true */) { - // @ts-ignore - client.postMessage(...args); - /* } else { - var transfer = new Set(); - fillTransfer(transfer, arguments); - - //console.log('reply', transfer, [...transfer]); - ctx.postMessage(...arguments, [...transfer]); - //console.log('reply', transfer, [...transfer]); - } */ -} - -/** - * Broadcast Notification - */ -function notify(...args: any[]) { - ctx.clients.matchAll({includeUncontrolled: false, type: 'window'}).then((listeners) => { - if(!listeners.length) { - //console.trace('no listeners?', self, listeners); - return; - } - - listeners.forEach(listener => { - // @ts-ignore - listener.postMessage(...args); - }); - }); -} - -networkerFactory.setUpdatesProcessor((obj, bool) => { - notify({update: {obj, bool}}); + delete deferredPromises[task.id]; }); -const onMessage = async(e: ExtendableMessageEvent) => { - try { - const taskID = e.data.taskID; +let taskID = 0; - log.debug('got message:', taskID, e, e.data); - - if(e.data.useLs) { - AppStorage.finishTask(e.data.taskID, e.data.args); - return; - } else if(e.data.type == 'convertWebp') { - const {fileName, bytes} = e.data.payload; - const deferred = apiFileManager.webpConvertPromises[fileName]; - if(deferred) { - deferred.resolve(bytes); - delete apiFileManager.webpConvertPromises[fileName]; - } - } - - switch(e.data.task) { - case 'computeSRP': - case 'gzipUncompress': - // @ts-ignore - return cryptoWorker[e.data.task].apply(cryptoWorker, e.data.args).then(result => { - respond(e.source, {taskID: taskID, result: result}); - }); - - case 'cancelDownload': - case 'downloadFile': { - /* // @ts-ignore - return apiFileManager.downloadFile(...e.data.args); */ - - try { - // @ts-ignore - let result = apiFileManager[e.data.task].apply(apiFileManager, e.data.args); - - if(result instanceof Promise) { - result = await result; - } - - respond(e.source, {taskID: taskID, result: result}); - } catch(err) { - respond(e.source, {taskID: taskID, error: err}); - } - } - - default: { - try { - // @ts-ignore - let result = apiManager[e.data.task].apply(apiManager, e.data.args); - - if(result instanceof Promise) { - result = await result; - } - - respond(e.source, {taskID: taskID, result: result}); - } catch(err) { - respond(e.source, {taskID: taskID, error: err}); - } - - //throw new Error('Unknown task: ' + e.data.task); - } - } - } catch(err) { +export interface ServiceWorkerTask extends WorkerTaskTemplate { + type: 'requestFilePart', + payload: [number, InputFileLocation | FileLocation, number, number] +}; - } +export interface ServiceWorkerTaskResponse extends WorkerTaskTemplate { + type: 'requestFilePart', + payload: UploadFile }; const onFetch = (event: FetchEvent): void => { @@ -152,70 +42,6 @@ const onFetch = (event: FetchEvent): void => { log.debug('[fetch]:', event); switch(scope) { - case 'download': - case 'thumb': - case 'document': - case 'photo': { - const info: DownloadOptions = JSON.parse(decodeURIComponent(params)); - - const rangeHeader = event.request.headers.get('Range'); - if(rangeHeader && info.mimeType && info.size) { // maybe safari - const range = parseRange(event.request.headers.get('Range')); - const possibleResponse = responseForSafariFirstRange(range, info.mimeType, info.size); - if(possibleResponse) { - return event.respondWith(possibleResponse); - } - } - - const fileName = getFileNameByLocation(info.location, {fileName: info.fileName}); - - /* event.request.signal.addEventListener('abort', (e) => { - console.log('[SW] user aborted request:', fileName); - cancellablePromise.cancel(); - }); - - event.request.signal.onabort = (e) => { - console.log('[SW] user aborted request:', fileName); - cancellablePromise.cancel(); - }; - - if(fileName == '5452060085729624717') { - setInterval(() => { - console.log('[SW] request status:', fileName, event.request.signal.aborted); - }, 1000); - } */ - - const cancellablePromise = apiFileManager.downloadFile(info); - cancellablePromise.notify = (progress: {done: number, total: number, offset: number}) => { - notify({progress: {fileName, ...progress}}); - }; - - log.debug('[fetch] file:', /* info, */fileName); - - event.respondWith(Promise.race([ - timeout(45 * 1000), - new Promise((resolve) => { // пробую это чтобы проверить, не сдохнет ли воркер - cancellablePromise.then(b => { - const responseInit: ResponseInit = {}; - - if(rangeHeader) { - responseInit.headers = { - 'Accept-Ranges': 'bytes', - 'Content-Range': `bytes 0-${info.size - 1}/${info.size || '*'}`, - 'Content-Length': `${info.size}`, - } - } - - resolve(new Response(b, responseInit)); - }).catch(err => { - - }); - }) - ])); - - break; - } - case 'stream': { const range = parseRange(event.request.headers.get('Range')); const [offset, end] = range; @@ -227,6 +53,7 @@ const onFetch = (event: FetchEvent): void => { event.respondWith(Promise.race([ timeout(45 * 1000), + new Promise((resolve, reject) => { // safari workaround const possibleResponse = responseForSafariFirstRange(range, info.mimeType, info.size); @@ -237,11 +64,19 @@ const onFetch = (event: FetchEvent): void => { const limit = end && end < STREAM_CHUNK_UPPER_LIMIT ? alignLimit(end - offset + 1) : STREAM_CHUNK_UPPER_LIMIT; const alignedOffset = alignOffset(offset, limit); - //log.debug('[stream] requestFilePart:', info.dcID, info.location, alignedOffset, limit); - - apiFileManager.requestFilePart(info.dcID, info.location, alignedOffset, limit).then(result => { + log.debug('[stream] requestFilePart:', info.dcID, info.location, alignedOffset, limit); + + const task: ServiceWorkerTask = { + type: 'requestFilePart', + id: taskID++, + payload: [info.dcID, info.location, alignedOffset, limit] + }; + + + const deferred = deferredPromises[task.id] = deferredPromise(); + deferred.then(result => { let ab = result.bytes; - + //log.debug('[stream] requestFilePart result:', result); const headers: Record = { @@ -267,127 +102,12 @@ const onFetch = (event: FetchEvent): void => { })); //}, 2.5e3); }).catch(err => {}); + + notifySomeone(task); }) ])); break; } - - /* case 'download': { - const info: DownloadOptions = JSON.parse(decodeURIComponent(params)); - - const promise = new Promise((resolve) => { - const headers: Record = { - 'Content-Disposition': `attachment; filename="${info.fileName}"`, - }; - - if(info.size) headers['Content-Length'] = info.size.toString(); - if(info.mimeType) headers['Content-Type'] = info.mimeType; - - log('[download] file:', info); - - const stream = new ReadableStream({ - start(controller: ReadableStreamDefaultController) { - const limitPart = DOWNLOAD_CHUNK_LIMIT; - - apiFileManager.downloadFile({ - ...info, - limitPart, - processPart: (bytes, offset) => { - log('[download] file processPart:', bytes, offset); - - controller.enqueue(new Uint8Array(bytes)); - - const isFinal = offset + limitPart >= info.size; - if(isFinal) { - controller.close(); - } - - return Promise.resolve(); - } - }).catch(err => { - log.error('[download] error:', err); - controller.error(err); - }); - }, - - cancel() { - log.error('[download] file canceled:', info); - } - }); - - resolve(new Response(stream, {headers})); - }); - - event.respondWith(promise); - - break; - } */ - - case 'upload': { - if(event.request.method == 'POST') { - event.respondWith(event.request.blob().then(blob => { - return apiFileManager.uploadFile(blob).then(v => new Response(JSON.stringify(v), {headers: {'Content-Type': 'application/json'}})); - })); - } - - break; - } - - /* default: { - - break; - } - case 'documents': - case 'photos': - case 'profiles': - // direct download - if (event.request.method === 'POST') { - event.respondWith(// download(url, 'unknown file.txt', getFilePartRequest)); - event.request.text() - .then((text) => { - const [, filename] = text.split('='); - return download(url, filename ? filename.toString() : 'unknown file', getFilePartRequest); - }), - ); - - // inline - } else { - event.respondWith( - ctx.cache.match(url).then((cached) => { - if (cached) return cached; - - return Promise.race([ - timeout(45 * 1000), // safari fix - new Promise((resolve) => { - fetchRequest(url, resolve, getFilePartRequest, ctx.cache, fileProgress); - }), - ]); - }), - ); - } - break; - - case 'stream': { - const [offset, end] = parseRange(event.request.headers.get('Range') || ''); - - log('stream', url, offset, end); - - event.respondWith(new Promise((resolve) => { - fetchStreamRequest(url, offset, end, resolve, getFilePartRequest); - })); - break; - } - - case 'stripped': - case 'cached': { - const bytes = getThumb(url) || null; - event.respondWith(new Response(bytes, { headers: { 'Content-Type': 'image/jpg' } })); - break; - } - - default: - if (url && url.endsWith('.tgs')) event.respondWith(fetchTGS(url)); - else event.respondWith(fetch(event.request.url)); */ } } catch(err) { event.respondWith(new Response('', { @@ -398,7 +118,6 @@ const onFetch = (event: FetchEvent): void => { }; const onChangeState = () => { - ctx.onmessage = onMessage; ctx.onfetch = onFetch; }; @@ -496,6 +215,5 @@ function alignLimit(limit: number) { // @ts-ignore if(process.env.NODE_ENV != 'production') { - (ctx as any).onMessage = onMessage; (ctx as any).onFetch = onFetch; } diff --git a/src/lib/mtproto/mtproto.worker.ts b/src/lib/mtproto/mtproto.worker.ts new file mode 100644 index 00000000..4318384b --- /dev/null +++ b/src/lib/mtproto/mtproto.worker.ts @@ -0,0 +1,146 @@ +// just to include +import {secureRandom} from '../polyfill'; +secureRandom; + +import apiManager from "./apiManager"; +import AppStorage from '../storage'; +import cryptoWorker from "../crypto/cryptoworker"; +import networkerFactory from "./networkerFactory"; +import apiFileManager from './apiFileManager'; +import { logger, LogLevels } from '../logger'; +import type { ServiceWorkerTask, ServiceWorkerTaskResponse } from './mtproto.service'; + +const log = logger('DW', LogLevels.error); + +const ctx = self as any as DedicatedWorkerGlobalScope; + +//console.error('INCLUDE !!!', new Error().stack); + +/* function isObject(object: any) { + return typeof(object) === 'object' && object !== null; +} */ + +/* function fillTransfer(transfer: any, obj: any) { + if(!obj) return; + + if(obj instanceof ArrayBuffer) { + transfer.add(obj); + } else if(obj.buffer && obj.buffer instanceof ArrayBuffer) { + transfer.add(obj.buffer); + } else if(isObject(obj)) { + for(var i in obj) { + fillTransfer(transfer, obj[i]); + } + } else if(Array.isArray(obj)) { + obj.forEach(value => { + fillTransfer(transfer, value); + }); + } +} */ + +function respond(...args: any[]) { + // отключил для всего потому что не успел пофиксить transfer detached + //if(isSafari(self)/* || true */) { + // @ts-ignore + ctx.postMessage(...args); + /* } else { + var transfer = new Set(); + fillTransfer(transfer, arguments); + + //console.log('reply', transfer, [...transfer]); + ctx.postMessage(...arguments, [...transfer]); + //console.log('reply', transfer, [...transfer]); + } */ +} + +networkerFactory.setUpdatesProcessor((obj, bool) => { + respond({update: {obj, bool}}); +}); + +ctx.addEventListener('message', async(e) => { + try { + const task = e.data; + const taskID = task.taskID; + + log.debug('got message:', taskID, task); + + //debugger; + + if(task.useLs) { + AppStorage.finishTask(task.taskID, task.args); + return; + } else if(task.type == 'convertWebp') { + const {fileName, bytes} = task.payload; + const deferred = apiFileManager.webpConvertPromises[fileName]; + if(deferred) { + deferred.resolve(bytes); + delete apiFileManager.webpConvertPromises[fileName]; + } + + return; + } else if((task as ServiceWorkerTask).type == 'requestFilePart') { + const task = e.data as ServiceWorkerTask; + const responseTask: ServiceWorkerTaskResponse = { + type: task.type, + id: task.id, + payload: null + }; + + try { + const res = await apiFileManager.requestFilePart(...task.payload); + responseTask.payload = res; + } catch(err) { + + } + + respond(responseTask); + return; + } + + switch(task.task) { + case 'computeSRP': + case 'gzipUncompress': + // @ts-ignore + return cryptoWorker[task.task].apply(cryptoWorker, task.args).then(result => { + respond({taskID: taskID, result: result}); + }); + + case 'cancelDownload': + case 'downloadFile': { + try { + // @ts-ignore + let result = apiFileManager[task.task].apply(apiFileManager, task.args); + + if(result instanceof Promise) { + result = await result; + } + + respond({taskID: taskID, result: result}); + } catch(err) { + respond({taskID: taskID, error: err}); + } + } + + default: { + try { + // @ts-ignore + let result = apiManager[task.task].apply(apiManager, task.args); + + if(result instanceof Promise) { + result = await result; + } + + respond({taskID: taskID, result: result}); + } catch(err) { + respond({taskID: taskID, error: err}); + } + + //throw new Error('Unknown task: ' + task.task); + } + } + } catch(err) { + + } +}); + +ctx.postMessage('ready'); diff --git a/src/lib/mtproto/mtprotoworker.ts b/src/lib/mtproto/mtprotoworker.ts index ed713f04..a919a90c 100644 --- a/src/lib/mtproto/mtprotoworker.ts +++ b/src/lib/mtproto/mtprotoworker.ts @@ -1,9 +1,11 @@ import {isObject, $rootScope} from '../utils'; import AppStorage from '../storage'; import CryptoWorkerMethods from '../crypto/crypto_methods'; -//import runtime from 'serviceworker-webpack-plugin/lib/runtime'; import { logger } from '../logger'; -import { webpWorkerController } from '../webp/webpWorkerController'; +import webpWorkerController from '../webp/webpWorkerController'; +import MTProtoWorker from 'worker-loader!./mtproto.worker'; +import type { DownloadOptions } from './apiFileManager'; +import type { ServiceWorkerTask, ServiceWorkerTaskResponse } from './mtproto.service'; type Task = { taskID: number, @@ -11,7 +13,12 @@ type Task = { args: any[] }; +const USEWORKERASWORKER = true; + class ApiManagerProxy extends CryptoWorkerMethods { + public worker: Worker; + public postMessage: (...args: any[]) => void; + private taskID = 0; private awaiting: { [id: number]: { @@ -30,10 +37,11 @@ class ApiManagerProxy extends CryptoWorkerMethods { super(); this.log('constructor'); - /** - * Service worker - */ - //(runtime.register({ scope: './' }) as Promise).then(registration => { + this.registerServiceWorker(); + this.registerWorker(); + } + + private registerServiceWorker() { navigator.serviceWorker.register('./sw.js', {scope: './'}).then(registration => { }, (err) => { @@ -44,6 +52,10 @@ class ApiManagerProxy extends CryptoWorkerMethods { this.log('set SW'); this.releasePending(); + if(!USEWORKERASWORKER) { + this.postMessage = navigator.serviceWorker.controller.postMessage.bind(navigator.serviceWorker.controller); + } + //registration.update(); }); @@ -60,26 +72,12 @@ class ApiManagerProxy extends CryptoWorkerMethods { * Message resolver */ navigator.serviceWorker.addEventListener('message', (e) => { - if(!isObject(e.data)) { + const task: ServiceWorkerTask = e.data; + if(!isObject(task)) { return; } - if(e.data.useLs) { - // @ts-ignore - AppStorage[e.data.task](...e.data.args).then(res => { - navigator.serviceWorker.controller.postMessage({useLs: true, taskID: e.data.taskID, args: res}); - }); - } else if(e.data.update) { - if(this.updatesProcessor) { - this.updatesProcessor(e.data.update.obj, e.data.update.bool); - } - } else if(e.data.progress) { - $rootScope.$broadcast('download_progress', e.data.progress); - } else if(e.data.type == 'convertWebp') { - webpWorkerController.postMessage(e.data); - } else { - this.finalizeTask(e.data.taskID, e.data.result, e.data.error); - } + this.postMessage(task); }); navigator.serviceWorker.addEventListener('messageerror', (e) => { @@ -87,6 +85,49 @@ class ApiManagerProxy extends CryptoWorkerMethods { }); } + private registerWorker() { + const worker = new MTProtoWorker(); + worker.addEventListener('message', (e) => { + if(!this.worker) { + this.worker = worker; + this.log('set webWorker'); + + if(USEWORKERASWORKER) { + this.postMessage = this.worker.postMessage.bind(this.worker); + } + + this.releasePending(); + } + + //this.log('got message from worker:', e.data); + + const task = e.data; + + if(!isObject(task)) { + return; + } + + if(task.useLs) { + // @ts-ignore + AppStorage[task.task](...task.args).then(res => { + this.postMessage({useLs: true, taskID: task.taskID, args: res}); + }); + } else if(task.update) { + if(this.updatesProcessor) { + this.updatesProcessor(task.update.obj, task.update.bool); + } + } else if(task.progress) { + $rootScope.$broadcast('download_progress', task.progress); + } else if(task.type == 'convertWebp') { + webpWorkerController.postMessage(task); + } else if((task as ServiceWorkerTaskResponse).type == 'requestFilePart') { + navigator.serviceWorker.controller.postMessage(task); + } else { + this.finalizeTask(task.taskID, task.result, task.error); + } + }); + } + private finalizeTask(taskID: number, result: any, error: any) { const deferred = this.awaiting[taskID]; if(deferred !== undefined) { @@ -116,10 +157,10 @@ class ApiManagerProxy extends CryptoWorkerMethods { } private releasePending() { - if(navigator.serviceWorker.controller) { + if(this.postMessage) { this.log.debug('releasing tasks, length:', this.pending.length); this.pending.forEach(pending => { - navigator.serviceWorker.controller.postMessage(pending); + this.postMessage(pending); }); this.log.debug('released tasks'); @@ -174,6 +215,10 @@ class ApiManagerProxy extends CryptoWorkerMethods { public cancelDownload(fileName: string) { return this.performTaskWorker('cancelDownload', fileName); } + + public downloadFile(options: DownloadOptions) { + return this.performTaskWorker('downloadFile', options); + } } const apiManagerProxy = new ApiManagerProxy(); diff --git a/src/lib/storage.ts b/src/lib/storage.ts index 35effa80..39dc7f40 100644 --- a/src/lib/storage.ts +++ b/src/lib/storage.ts @@ -1,4 +1,5 @@ import { Modes } from './mtproto/mtproto_config'; +import { notifySomeone, isWorker } from '../helpers/context'; class ConfigStorage { public keyPrefix = ''; @@ -137,7 +138,6 @@ class ConfigStorage { } class AppStorage { - private isWorker: boolean; private taskID = 0; private tasks: {[taskID: number]: (result: any) => void} = {}; //private log = (...args: any[]) => console.log('[SW LS]', ...args); @@ -150,11 +150,7 @@ class AppStorage { this.setPrefix('t_'); } - // @ts-ignore - //this.isWebWorker = typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope; - this.isWorker = typeof ServiceWorkerGlobalScope !== 'undefined' && self instanceof ServiceWorkerGlobalScope; - - if(!this.isWorker) { + if(!isWorker) { this.configStorage = new ConfigStorage(); } } @@ -185,26 +181,13 @@ class AppStorage { private proxy(methodName: 'set' | 'get' | 'remove' | 'clear', ..._args: any[]) { return new Promise((resolve, reject) => { - if(this.isWorker) { + if(isWorker) { const taskID = this.taskID++; this.tasks[taskID] = resolve; + const task = {useLs: true, task: methodName, taskID, args: _args}; - (self as any as ServiceWorkerGlobalScope) - .clients - .matchAll({ includeUncontrolled: false, type: 'window' }) - .then((listeners) => { - if(!listeners.length) { - //console.trace('no listeners?', self, listeners); - return; - } - - this.log('will proxy', {useLs: true, task: methodName, taskID, args: _args}); - listeners[0].postMessage({useLs: true, task: methodName, taskID, args: _args}); - }); - - // @ts-ignore - //self.postMessage({useLs: true, task: methodName, taskID: this.taskID, args: _args}); + notifySomeone(task); } else { let args = Array.prototype.slice.call(_args); args.push((result: T) => { diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 55701b68..704a7994 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -5,7 +5,7 @@ * https://github.com/zhukov/webogram/blob/master/LICENSE */ -import { InputFileLocation, FileLocation } from "../types"; +import type { DownloadOptions } from "./mtproto/apiFileManager"; var _logTimer = Date.now(); export function dT () { @@ -539,13 +539,7 @@ export function getEmojiToneIndex(input: string) { } export type FileURLType = 'photo' | 'thumb' | 'document' | 'stream' | 'download'; -export function getFileURL(type: FileURLType, options: { - dcID: number, - location: InputFileLocation | FileLocation, - size?: number, - mimeType?: string, - fileName?: string -}) { +export function getFileURL(type: FileURLType, options: DownloadOptions) { //console.log('getFileURL', location); //const perf = performance.now(); const encoded = encodeURIComponent(JSON.stringify(options)); diff --git a/src/lib/webp/webp.worker.ts b/src/lib/webp/webp.worker.ts index 4cf4face..2b43083a 100644 --- a/src/lib/webp/webp.worker.ts +++ b/src/lib/webp/webp.worker.ts @@ -3,30 +3,37 @@ import type { WebpConvertTask } from './webpWorkerController'; const ctx = self as any as DedicatedWorkerGlobalScope; const tasks: WebpConvertTask[] = []; -let isProcessing = false; +//let isProcessing = false; function finishTask() { - isProcessing = false; + //isProcessing = false; processTasks(); } function processTasks() { - if(isProcessing) return; + //if(isProcessing) return; const task = tasks.shift(); if(!task) return; - isProcessing = true; + //isProcessing = true; switch(task.type) { case 'convertWebp': { const {fileName, bytes} = task.payload; + let convertedBytes: Uint8Array; + try { + convertedBytes = webp2png(bytes).bytes; + } catch(err) { + console.error('Convert webp2png error:', err, 'payload:', task.payload); + } + ctx.postMessage({ type: 'convertWebp', payload: { fileName, - bytes: webp2png(bytes).bytes + bytes: convertedBytes } }); @@ -42,6 +49,12 @@ function processTasks() { function scheduleTask(task: WebpConvertTask) { tasks.push(task); + /* if(task.payload.fileName.indexOf('main-') === 0) { + tasks.push(task); + } else { + tasks.unshift(task); + } */ + processTasks(); } diff --git a/src/lib/webp/webpWorkerController.ts b/src/lib/webp/webpWorkerController.ts index 3a211bb8..00234dc6 100644 --- a/src/lib/webp/webpWorkerController.ts +++ b/src/lib/webp/webpWorkerController.ts @@ -1,5 +1,6 @@ import WebpWorker from 'worker-loader!./webp.worker'; import { CancellablePromise, deferredPromise } from '../polyfill'; +import apiManagerProxy from '../mtproto/mtprotoworker'; export type WebpConvertTask = { type: 'convertWebp', @@ -21,11 +22,11 @@ export class WebpWorkerController { if(payload.fileName.indexOf('main-') === 0) { const promise = this.convertPromises[payload.fileName]; if(promise) { - promise.resolve(payload.bytes); + payload.bytes ? promise.resolve(payload.bytes) : promise.reject(); delete this.convertPromises[payload.fileName]; } } else { - navigator.serviceWorker.controller.postMessage(e.data); + apiManagerProxy.postMessage(e.data); } }); } @@ -40,18 +41,23 @@ export class WebpWorkerController { } convert(fileName: string, bytes: Uint8Array) { + fileName = 'main-' + fileName; + if(this.convertPromises.hasOwnProperty(fileName)) { return this.convertPromises[fileName]; } const convertPromise = deferredPromise(); - fileName = 'main-' + fileName; - this.postMessage({type: 'convertWebp', payload: {fileName, bytes}}); return this.convertPromises[fileName] = convertPromise; } } -export const webpWorkerController = new WebpWorkerController(); \ No newline at end of file +const webpWorkerController = new WebpWorkerController(); +// @ts-ignore +if(process.env.NODE_ENV != 'production') { + (window as any).webpWorkerController = webpWorkerController; +} +export default webpWorkerController; \ No newline at end of file diff --git a/src/pages/pageSignIn.ts b/src/pages/pageSignIn.ts index a6fef199..37318368 100644 --- a/src/pages/pageSignIn.ts +++ b/src/pages/pageSignIn.ts @@ -6,7 +6,6 @@ import Config from '../lib/config'; import { findUpTag } from "../lib/utils"; import pageAuthCode from "./pageAuthCode"; import pageSignQR from './pageSignQR'; -//import apiManager from "../lib/mtproto/apiManager"; import apiManager from "../lib/mtproto/mtprotoworker"; import Page from "./page"; import { App, Modes } from "../lib/mtproto/mtproto_config"; diff --git a/src/scss/partials/_chatBubble.scss b/src/scss/partials/_chatBubble.scss index 52164eb9..306565b0 100644 --- a/src/scss/partials/_chatBubble.scss +++ b/src/scss/partials/_chatBubble.scss @@ -1211,6 +1211,10 @@ $bubble-margin: .25rem; background-color: #0089ff; } + &__loaded { + background-color: #cacaca; + } + input::-webkit-slider-thumb { background: #63a2e3; border: none; @@ -1355,7 +1359,8 @@ $bubble-margin: .25rem; } &.is-edited .time { - width: 85px; + /* width: 85px; */ + width: 90px !important; } .document-ico:after { @@ -1444,6 +1449,12 @@ $bubble-margin: .25rem; &.is-sending poll-element { pointer-events: none; } + + .media-progress { + &__loaded { + background-color: #90e18d !important; + } + } } .reply-markup { diff --git a/src/scss/partials/_ckin.scss b/src/scss/partials/_ckin.scss index 76995955..981fbc27 100644 --- a/src/scss/partials/_ckin.scss +++ b/src/scss/partials/_ckin.scss @@ -191,7 +191,7 @@ } .player-volume { - margin: -3px 12px 0 16px; + margin: -3px 2px 0 10px; display: flex; align-items: center; diff --git a/src/scss/partials/_rightSidebar.scss b/src/scss/partials/_rightSidebar.scss index b3da6867..0e0fa11d 100644 --- a/src/scss/partials/_rightSidebar.scss +++ b/src/scss/partials/_rightSidebar.scss @@ -481,6 +481,10 @@ height: 2px; } + &__loaded { + background-color: #cacaca; + } + &__seek { height: 2px; //background-color: #e6ecf0; diff --git a/src/types.d.ts b/src/types.d.ts index 2f1b3421..372bb112 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -144,4 +144,10 @@ export type inputStickerSetThumb = { local_id: number }; -export type InputFileLocation = inputFileLocation | inputDocumentFileLocation | inputPhotoFileLocation | inputPeerPhotoFileLocation | inputStickerSetThumb; \ No newline at end of file +export type InputFileLocation = inputFileLocation | inputDocumentFileLocation | inputPhotoFileLocation | inputPeerPhotoFileLocation | inputStickerSetThumb; + +export type WorkerTaskTemplate = { + type: string, + id: number, + payload: any +}; \ No newline at end of file diff --git a/webpack.prod.js b/webpack.prod.js index 49c9fc36..9caf33b4 100644 --- a/webpack.prod.js +++ b/webpack.prod.js @@ -64,9 +64,7 @@ module.exports = merge(common, { files.forEach(file => { //console.log('to unlink 1:', file); - if(file.includes('mitm.') - || file.includes('sw.js') - || file.includes('.xml') + if(file.includes('.xml') || file.includes('.webmanifest') || file.includes('.wasm') || file.includes('rlottie')