Fallback to regular worker due to instability

More fixes
WebP stickers fix
This commit is contained in:
morethanwords 2020-08-28 14:25:43 +03:00
parent e8073991a0
commit 5d0f91c5f1
39 changed files with 1483 additions and 1480 deletions

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -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";

View File

@ -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<MTStickerSet>
} = 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;

View File

@ -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;

View File

@ -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() {
}
}

View File

@ -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() {
}
}

View File

@ -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<MTStickerSet>
} = 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() {
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}
};

10
src/helpers/blob.ts Normal file
View File

@ -0,0 +1,10 @@
export const readBlobAsText = (blob: Blob) => {
return new Promise<string>(resolve => {
const reader = new FileReader();
reader.addEventListener('loadend', async(e) => {
// @ts-ignore
resolve(e.srcElement.result);
});
reader.readAsText(blob);
});
};

29
src/helpers/context.ts Normal file
View File

@ -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);

View File

@ -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<Blob>} = {};
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<any> = 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);
}
}

View File

@ -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<Blob>();
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, () => {

View File

@ -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<Blob> {
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');
}
}

View File

@ -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' ? {

View File

@ -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<Uint8Array>();
(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;
};

View File

@ -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<any>} = {};
/* 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<Response>((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<Response>((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<UploadFile>();
deferred.then(result => {
let ab = result.bytes;
//log.debug('[stream] requestFilePart result:', result);
const headers: Record<string, string> = {
@ -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<Response>((resolve) => {
const headers: Record<string, string> = {
'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<Response>((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;
}

View File

@ -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');

View File

@ -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<ServiceWorkerRegistration>).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();

View File

@ -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<T>(methodName: 'set' | 'get' | 'remove' | 'clear', ..._args: any[]) {
return new Promise<T>((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) => {

View File

@ -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));

View File

@ -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();
}

View File

@ -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<Uint8Array>();
fileName = 'main-' + fileName;
this.postMessage({type: 'convertWebp', payload: {fileName, bytes}});
return this.convertPromises[fileName] = convertPromise;
}
}
export const webpWorkerController = new WebpWorkerController();
const webpWorkerController = new WebpWorkerController();
// @ts-ignore
if(process.env.NODE_ENV != 'production') {
(window as any).webpWorkerController = webpWorkerController;
}
export default webpWorkerController;

View File

@ -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";

View File

@ -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 {

View File

@ -191,7 +191,7 @@
}
.player-volume {
margin: -3px 12px 0 16px;
margin: -3px 2px 0 10px;
display: flex;
align-items: center;

View File

@ -481,6 +481,10 @@
height: 2px;
}
&__loaded {
background-color: #cacaca;
}
&__seek {
height: 2px;
//background-color: #e6ecf0;

8
src/types.d.ts vendored
View File

@ -144,4 +144,10 @@ export type inputStickerSetThumb = {
local_id: number
};
export type InputFileLocation = inputFileLocation | inputDocumentFileLocation | inputPhotoFileLocation | inputPeerPhotoFileLocation | inputStickerSetThumb;
export type InputFileLocation = inputFileLocation | inputDocumentFileLocation | inputPhotoFileLocation | inputPeerPhotoFileLocation | inputStickerSetThumb;
export type WorkerTaskTemplate = {
type: string,
id: number,
payload: any
};

View File

@ -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')