Browse Source

Fallback to regular worker due to instability

More fixes
WebP stickers fix
master
morethanwords 4 years ago
parent
commit
c7bd824873
  1. 1
      src/components/chatInput.ts
  2. 853
      src/components/emoticonsDropdown.ts
  3. 302
      src/components/emoticonsDropdown/index.ts
  4. 209
      src/components/emoticonsDropdown/tabs/emoji.ts
  5. 44
      src/components/emoticonsDropdown/tabs/gifs.ts
  6. 318
      src/components/emoticonsDropdown/tabs/stickers.ts
  7. 15
      src/components/gifsMasonry.ts
  8. 127
      src/components/wrappers.ts
  9. 10
      src/helpers/blob.ts
  10. 29
      src/helpers/context.ts
  11. 188
      src/lib/appManagers/appDocsManager.ts
  12. 40
      src/lib/appManagers/appDownloadManager.ts
  13. 35
      src/lib/appManagers/appPhotosManager.ts
  14. 24
      src/lib/appManagers/appStickersManager.ts
  15. 16
      src/lib/mtproto/apiFileManager.ts
  16. 356
      src/lib/mtproto/mtproto.service.ts
  17. 146
      src/lib/mtproto/mtproto.worker.ts
  18. 91
      src/lib/mtproto/mtprotoworker.ts
  19. 27
      src/lib/storage.ts
  20. 10
      src/lib/utils.ts
  21. 23
      src/lib/webp/webp.worker.ts
  22. 16
      src/lib/webp/webpWorkerController.ts
  23. 1
      src/pages/pageSignIn.ts
  24. 13
      src/scss/partials/_chatBubble.scss
  25. 2
      src/scss/partials/_ckin.scss
  26. 4
      src/scss/partials/_rightSidebar.scss
  27. 8
      src/types.d.ts
  28. 4
      webpack.prod.js

1
src/components/chatInput.ts

@ -1,6 +1,5 @@ @@ -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";

853
src/components/emoticonsDropdown.ts

@ -1,853 +0,0 @@ @@ -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;

302
src/components/emoticonsDropdown/index.ts

@ -0,0 +1,302 @@ @@ -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;

209
src/components/emoticonsDropdown/tabs/emoji.ts

@ -0,0 +1,209 @@ @@ -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() {
}
}

44
src/components/emoticonsDropdown/tabs/gifs.ts

@ -0,0 +1,44 @@ @@ -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() {
}
}

318
src/components/emoticonsDropdown/tabs/stickers.ts

@ -0,0 +1,318 @@ @@ -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() {
}
}

15
src/components/gifsMasonry.ts

@ -49,11 +49,18 @@ export default class GifsMasonry { @@ -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 { @@ -124,6 +131,6 @@ export default class GifsMasonry {
}
};
(posterURL ? renderImageFromUrl(img, posterURL, afterRender) : afterRender());
(gotThumb?.thumb?.url ? renderImageFromUrl(img, gotThumb.thumb.url, afterRender) : afterRender());
}
}

127
src/components/wrappers.ts

@ -6,7 +6,7 @@ import ProgressivePreloader from './preloader'; @@ -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'; @@ -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 @@ -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: @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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

@ -0,0 +1,10 @@ @@ -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

@ -0,0 +1,29 @@ @@ -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);

188
src/lib/appManagers/appDocsManager.ts

@ -1,23 +1,21 @@ @@ -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) {
public saveDoc(doc: MTDocument, context?: any) {
//console.log('saveDoc', apiDoc, this.docs[apiDoc.id]);
if(this.docs[apiDoc.id]) {
const d = this.docs[apiDoc.id];
if(this.docs[doc.id]) {
const d = this.docs[doc.id];
if(apiDoc.thumbs) {
if(!d.thumbs) d.thumbs = apiDoc.thumbs;
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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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, this.getFileDownloadOptions(doc, thumb));
}
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 getFileURL(type, {
dcID: doc.dc_id,
location: inputFileLocation,
size: thumb ? thumb.size : doc.size,
mimeType: mimeType,
fileName: doc.file_name
});
return {thumb, promise};
}
public getThumbURL(doc: MTDocument, useBytes = true) {
public getThumb(doc: MTDocument, useBytes = true) {
if(doc.thumbs?.length) {
let thumb: MTPhotoSize;
if(!useBytes) {
@ -218,43 +236,43 @@ class AppDocsManager { @@ -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 { @@ -278,8 +296,6 @@ class AppDocsManager {
});
return blob;
//return originalPromise;
//return new Response(blob);
});
}
@ -287,10 +303,8 @@ class AppDocsManager { @@ -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);
}
}

40
src/lib/appManagers/appDownloadManager.ts

@ -1,6 +1,8 @@ @@ -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 { @@ -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 { @@ -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, () => {

35
src/lib/appManagers/appPhotosManager.ts

@ -1,8 +1,8 @@ @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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');
}
}

24
src/lib/appManagers/appStickersManager.ts

@ -3,7 +3,7 @@ import AppStorage from '../storage'; @@ -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 { @@ -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 { @@ -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' ? {

16
src/lib/mtproto/apiFileManager.ts

@ -8,6 +8,7 @@ import { logger, LogLevels } from "../logger"; @@ -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 = { @@ -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 { @@ -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;
};

356
src/lib/mtproto/mtproto.service.ts

@ -1,148 +1,38 @@ @@ -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 => { @@ -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 => { @@ -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 => { @@ -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 => { @@ -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 => { @@ -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) { @@ -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;
}

146
src/lib/mtproto/mtproto.worker.ts

@ -0,0 +1,146 @@ @@ -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');

91
src/lib/mtproto/mtprotoworker.ts

@ -1,9 +1,11 @@ @@ -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 = { @@ -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 { @@ -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 { @@ -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,31 +72,60 @@ class ApiManagerProxy extends CryptoWorkerMethods { @@ -60,31 +72,60 @@ 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) {
this.postMessage(task);
});
navigator.serviceWorker.addEventListener('messageerror', (e) => {
this.log.error('SW messageerror:', e);
});
}
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[e.data.task](...e.data.args).then(res => {
navigator.serviceWorker.controller.postMessage({useLs: true, taskID: e.data.taskID, args: res});
AppStorage[task.task](...task.args).then(res => {
this.postMessage({useLs: true, taskID: task.taskID, args: res});
});
} else if(e.data.update) {
} else if(task.update) {
if(this.updatesProcessor) {
this.updatesProcessor(e.data.update.obj, e.data.update.bool);
this.updatesProcessor(task.update.obj, task.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 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(e.data.taskID, e.data.result, e.data.error);
this.finalizeTask(task.taskID, task.result, task.error);
}
});
navigator.serviceWorker.addEventListener('messageerror', (e) => {
this.log.error('SW messageerror:', e);
});
}
private finalizeTask(taskID: number, result: any, error: any) {
@ -116,10 +157,10 @@ class ApiManagerProxy extends CryptoWorkerMethods { @@ -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 { @@ -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();

27
src/lib/storage.ts

@ -1,4 +1,5 @@ @@ -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 { @@ -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 { @@ -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 { @@ -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) => {

10
src/lib/utils.ts

@ -5,7 +5,7 @@ @@ -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) { @@ -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));

23
src/lib/webp/webp.worker.ts

@ -3,30 +3,37 @@ import type { WebpConvertTask } from './webpWorkerController'; @@ -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() { @@ -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();
}

16
src/lib/webp/webpWorkerController.ts

@ -1,5 +1,6 @@ @@ -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 { @@ -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 { @@ -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;

1
src/pages/pageSignIn.ts

@ -6,7 +6,6 @@ import Config from '../lib/config'; @@ -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";

13
src/scss/partials/_chatBubble.scss

@ -1211,6 +1211,10 @@ $bubble-margin: .25rem; @@ -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; @@ -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; @@ -1444,6 +1449,12 @@ $bubble-margin: .25rem;
&.is-sending poll-element {
pointer-events: none;
}
.media-progress {
&__loaded {
background-color: #90e18d !important;
}
}
}
.reply-markup {

2
src/scss/partials/_ckin.scss

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

4
src/scss/partials/_rightSidebar.scss

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

8
src/types.d.ts vendored

@ -144,4 +144,10 @@ export type inputStickerSetThumb = { @@ -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
};

4
webpack.prod.js

@ -64,9 +64,7 @@ module.exports = merge(common, { @@ -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')

Loading…
Cancel
Save