diff --git a/src/components/emoticonsDropdown.ts b/src/components/emoticonsDropdown.ts index 4fb0dd89..b8332388 100644 --- a/src/components/emoticonsDropdown.ts +++ b/src/components/emoticonsDropdown.ts @@ -355,34 +355,27 @@ class StickersTab implements EmoticonsTab { //console.log('got stickerSet', stickerSet, li); if(stickerSet.set.thumb) { - appStickersManager.getStickerSetThumb(stickerSet.set).then((blob) => { - //console.log('setting thumb', stickerSet, blob); - if(stickerSet.set.pFlags.animated) { // means animated - const reader = new FileReader(); - - reader.addEventListener('loadend', async(e) => { - // @ts-ignore - const text = e.srcElement.result; - let json = await apiManager.gzipUncompress(text, true); - - let animation = await lottieLoader.loadAnimationWorker({ - container: li, - loop: true, - autoplay: false, - animationData: JSON.parse(json), - width: 32, - height: 32 - }, EMOTICONSSTICKERGROUP); - }); - - reader.readAsArrayBuffer(blob); - } else { - let image = new Image(); - renderImageFromUrl(image, URL.createObjectURL(blob)); + 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).then(() => { li.append(image); - } - }); + }) + } } else { // as thumb will be used first sticker wrapSticker({ doc: stickerSet.documents[0], diff --git a/src/components/misc.ts b/src/components/misc.ts index 577b3a71..0355c5db 100644 --- a/src/components/misc.ts +++ b/src/components/misc.ts @@ -165,29 +165,29 @@ let set = (elem: HTMLElement | HTMLImageElement | SVGImageElement | HTMLSourceEl else elem.style.backgroundImage = 'url(' + url + ')'; }; -export function renderImageFromUrl(elem: HTMLElement | HTMLImageElement | SVGImageElement | HTMLSourceElement, url: string): Promise { +export async function renderImageFromUrl(elem: HTMLElement | HTMLImageElement | SVGImageElement | HTMLSourceElement, url: string): Promise { if(loadedURLs[url]) { set(elem, url); - return Promise.resolve(true); + } else { + if(elem instanceof HTMLSourceElement) { + elem.src = url; + } else { + await new Promise((resolve, reject) => { + let loader = new Image(); + loader.src = url; + //let perf = performance.now(); + loader.addEventListener('load', () => { + set(elem, url); + loadedURLs[url] = true; + //console.log('onload:', url, performance.now() - perf); + resolve(false); + }); + loader.addEventListener('error', reject); + }); + } } - if(elem instanceof HTMLSourceElement) { - elem.src = url; - return Promise.resolve(false); - } else { - return new Promise((resolve, reject) => { - let loader = new Image(); - loader.src = url; - //let perf = performance.now(); - loader.addEventListener('load', () => { - set(elem, url); - loadedURLs[url] = true; - //console.log('onload:', url, performance.now() - perf); - resolve(false); - }); - loader.addEventListener('error', reject); - }); - } + return !!loadedURLs[url]; } export function putPreloader(elem: Element, returnDiv = false) { diff --git a/src/components/wrappers.ts b/src/components/wrappers.ts index 29dac030..2f8a6729 100644 --- a/src/components/wrappers.ts +++ b/src/components/wrappers.ts @@ -1,6 +1,4 @@ import appPhotosManager from '../lib/appManagers/appPhotosManager'; -//import CryptoWorker from '../lib/crypto/cryptoworker'; -import apiManager from '../lib/mtproto/mtprotoworker'; import LottieLoader from '../lib/lottieLoader'; import appDocsManager from "../lib/appManagers/appDocsManager"; import { formatBytes, getEmojiToneIndex } from "../lib/utils"; @@ -415,14 +413,19 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o let thumb = doc.thumbs[0]; //console.log('wrap sticker', thumb, div); + + let img: HTMLImageElement; + const afterRender = () => { + if(!div.childElementCount) { + div.append(img); + } + }; if(thumb.bytes) { - let img = new Image(); + img = new Image(); if((!isSafari || doc.stickerThumbConverted)/* && false */) { - renderImageFromUrl(img, appPhotosManager.getPreviewURLFromThumb(thumb, true)); - - div.append(img); + renderImageFromUrl(img, appPhotosManager.getPreviewURLFromThumb(thumb, true)).then(afterRender); } else { webpWorkerController.convert(doc.id, thumb.bytes).then(bytes => { if(middleware && !middleware()) return; @@ -431,11 +434,7 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o doc.stickerThumbConverted = true; if(!div.childElementCount) { - renderImageFromUrl(img, appPhotosManager.getPreviewURLFromThumb(thumb, true)).then(() => { - if(!div.childElementCount) { - div.append(img); - } - }); + renderImageFromUrl(img, appPhotosManager.getPreviewURLFromThumb(thumb, true)).then(afterRender); } }); } @@ -444,26 +443,24 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o return Promise.resolve(); } } else if(!onlyThumb && stickerType == 2 && withThumb && toneIndex <= 0) { - let img = new Image(); - let load = () => appDocsManager.downloadDocThumb(doc, thumb.type).then(url => { + img = new Image(); + + const load = () => appDocsManager.downloadDocThumb(doc, thumb.type).then(url => { if(div.childElementCount || (middleware && !middleware())) return; - let promise = renderImageFromUrl(img, url); + const promise = renderImageFromUrl(img, url); - if(!downloaded) { - promise.then(() => { - if(!div.childElementCount) { - div.append(img); - } - }); - } + //if(!downloaded) { + promise.then(afterRender); + //} }); - let downloaded = appDocsManager.hasDownloadedThumb(doc.id, thumb.type); + /* let downloaded = appDocsManager.hasDownloadedThumb(doc.id, thumb.type); if(downloaded) { div.append(img); - } + } */ - lazyLoadQueue && !downloaded ? lazyLoadQueue.push({div, load, wasSeen: group == 'chat'}) : load(); + //lazyLoadQueue && !downloaded ? lazyLoadQueue.push({div, load, wasSeen: group == 'chat'}) : load(); + load(); } } @@ -482,33 +479,27 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o } let downloaded = doc.downloaded; - let load = () => appDocsManager.downloadDocNew(doc.id).promise.then(blob => { - //console.log('loaded sticker:', doc, div); + let load = async() => { if(middleware && !middleware()) return; - //return; - if(stickerType == 2) { - const reader = new FileReader(); - - reader.addEventListener('loadend', async(e) => { - //console.time('decompress sticker' + doc.id); - //console.time('render sticker' + doc.id); - // @ts-ignore - const text = e.srcElement.result; - let json = await apiManager.gzipUncompress(text, true); + /* if(doc.id == '1860749763008266301') { + console.log('loaded sticker:', doc, div); + } */ - //console.timeEnd('decompress sticker' + doc.id); + //console.time('download sticker' + doc.id); - /* if(doc.id == '1860749763008266301') { - console.log('loaded sticker:', doc, div); - } */ + //appDocsManager.downloadDocNew(doc.id).promise.then(res => res.json()).then(async(json) => { + fetch(doc.url).then(res => res.json()).then(async(json) => { + //console.timeEnd('download sticker' + doc.id); + //console.log('loaded sticker:', doc, div); + if(middleware && !middleware()) return; let animation = await LottieLoader.loadAnimationWorker/* loadAnimation */({ container: div, loop: loop && !emoji, autoplay: play, - animationData: JSON.parse(json), + animationData: json, width, height }, group, toneIndex); @@ -530,11 +521,9 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o } }); } - - //console.timeEnd('render sticker' + doc.id); }); - - reader.readAsArrayBuffer(blob); + + //console.timeEnd('render sticker' + doc.id); } else if(stickerType == 1) { let img = new Image(); @@ -543,6 +532,8 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o img.style.opacity = '0'; img.addEventListener('load', () => { + doc.downloaded = true; + window.requestAnimationFrame(() => { img.style.opacity = ''; }); @@ -557,7 +548,7 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o div.append(img); }); } - }); + }; return lazyLoadQueue && (!doc.downloaded || stickerType == 2) ? (lazyLoadQueue.push({div, load, wasSeen: group == 'chat' && stickerType != 2}), Promise.resolve()) : load(); } diff --git a/src/lib/appManagers/appDocsManager.ts b/src/lib/appManagers/appDocsManager.ts index cccbdb12..20aee9d3 100644 --- a/src/lib/appManagers/appDocsManager.ts +++ b/src/lib/appManagers/appDocsManager.ts @@ -4,7 +4,7 @@ import { isObject, getFileURL } from '../utils'; import opusDecodeController from '../opusDecodeController'; import { MTDocument, inputDocumentFileLocation } from '../../types'; import { getFileNameByLocation } from '../bin_utils'; -import appDownloadManager, { Download } from './appDownloadManager'; +import appDownloadManager, { Download, ResponseMethod } from './appDownloadManager'; class AppDocsManager { private docs: {[docID: string]: MTDocument} = {}; @@ -273,7 +273,7 @@ class AppDocsManager { return this.downloadPromises[doc.id] = deferred; } - public downloadDocNew(docID: string | MTDocument, toFileEntry?: any): Download { + public downloadDocNew(docID: string | MTDocument/* , method: ResponseMethod = 'blob' */): Download { const doc = this.getDoc(docID); if(doc._ == 'documentEmpty') { @@ -287,39 +287,39 @@ class AppDocsManager { return download; } - download = appDownloadManager.download(fileName, doc.url); - //const _download: Download = {...download}; + download = appDownloadManager.download(fileName, doc.url/* , method */); - //_download.promise = _download.promise.then(async(blob) => { - download.promise = download.promise.then(async(blob) => { - if(blob) { - doc.downloaded = true; - - if(doc.type == 'voice' && !opusDecodeController.isPlaySupported()) { - let reader = new FileReader(); - - await new Promise((resolve, reject) => { - reader.onloadend = (e) => { - let uint8 = new Uint8Array(e.target.result as ArrayBuffer); - //console.log('sending uint8 to decoder:', uint8); - opusDecodeController.decode(uint8).then(result => { - doc.url = result.url; - resolve(); - }, (err) => { - delete doc.downloaded; - reject(err); - }); - }; - - reader.readAsArrayBuffer(blob); - }); - } - } - - return blob; + const originalPromise = download.promise; + originalPromise.then(() => { + doc.downloaded = true; }); - //return this.downloadPromisesNew[doc.id] = _download; + if(doc.type == 'voice' && !opusDecodeController.isPlaySupported()) { + download.promise = originalPromise.then(async(blob) => { + let reader = new FileReader(); + + await new Promise((resolve, reject) => { + reader.onloadend = (e) => { + let uint8 = new Uint8Array(e.target.result as ArrayBuffer); + //console.log('sending uint8 to decoder:', uint8); + opusDecodeController.decode(uint8).then(result => { + doc.url = result.url; + resolve(); + }, (err) => { + delete doc.downloaded; + reject(err); + }); + }; + + reader.readAsArrayBuffer(blob); + }); + + return blob; + //return originalPromise; + //return new Response(blob); + }); + } + return download; } diff --git a/src/lib/appManagers/appDownloadManager.ts b/src/lib/appManagers/appDownloadManager.ts index 4ce91696..94ff1b52 100644 --- a/src/lib/appManagers/appDownloadManager.ts +++ b/src/lib/appManagers/appDownloadManager.ts @@ -1,7 +1,14 @@ import { $rootScope } from "../utils"; import apiManager from "../mtproto/mtprotoworker"; -export type Download = {promise: Promise, controller: AbortController}; +export type ResponseMethodBlob = 'blob'; +export type ResponseMethodJson = 'json'; +export type ResponseMethod = ResponseMethodBlob | ResponseMethodJson; + +export type DownloadBlob = {promise: Promise, controller: AbortController}; +export type DownloadJson = {promise: Promise, controller: AbortController}; +export type Download = DownloadBlob/* | DownloadJson */; + export type Progress = {done: number, fileName: string, total: number, offset: number}; export type ProgressCallback = (details: Progress) => void; @@ -22,12 +29,14 @@ export class AppDownloadManager { }); } - public download(fileName: string, url: string) { + public download(fileName: string, url: string, responseMethod?: ResponseMethodBlob): DownloadBlob; + public download(fileName: string, url: string, responseMethod?: ResponseMethodJson): DownloadJson; + public download(fileName: string, url: string, responseMethod: ResponseMethod = 'blob'): Download { if(this.downloads.hasOwnProperty(fileName)) return this.downloads[fileName]; const controller = new AbortController(); const promise = fetch(url, {signal: controller.signal}) - .then(res => res.blob()) + .then(res => res[responseMethod]()) .catch(err => { // Только потому что event.request.signal не работает в SW, либо я кривой? if(err.name === 'AbortError') { //console.log('Fetch aborted'); diff --git a/src/lib/appManagers/appStickersManager.ts b/src/lib/appManagers/appStickersManager.ts index 9664caa4..c1ddc975 100644 --- a/src/lib/appManagers/appStickersManager.ts +++ b/src/lib/appManagers/appStickersManager.ts @@ -217,7 +217,7 @@ class AppStickersManager { }, 100); } - public getStickerSetThumb(stickerSet: MTStickerSet) { + public getStickerSetThumbURL(stickerSet: MTStickerSet) { const thumb = stickerSet.thumb; const dcID = stickerSet.thumb_dc_id; @@ -231,7 +231,7 @@ class AppStickersManager { }; const url = getFileURL('document', {dcID, location: input, size: thumb.size, mimeType: isAnimated ? "application/x-tgsticker" : 'image/webp'}); - return fetch(url).then(res => res.blob()); + return url; //return promise; } diff --git a/src/lib/bin_utils.ts b/src/lib/bin_utils.ts index 48871510..695ea408 100644 --- a/src/lib/bin_utils.ts +++ b/src/lib/bin_utils.ts @@ -134,15 +134,15 @@ export function dataUrlToBlob(url: string) { return blob; } -export function blobConstruct(blobParts: any, mimeType: string = '') { - var blob; - var safeMimeType = blobSafeMimeType(mimeType); +export function blobConstruct(blobParts: any, mimeType: string = ''): Blob { + let blob; + const safeMimeType = blobSafeMimeType(mimeType); try { blob = new Blob(blobParts, {type: safeMimeType}); } catch(e) { // @ts-ignore - var bb = new BlobBuilder; - blobParts.forEach(function(blobPart: any) { + let bb = new BlobBuilder; + blobParts.forEach((blobPart: any) => { bb.append(blobPart); }); blob = bb.getBlob(safeMimeType); @@ -163,6 +163,7 @@ export function blobSafeMimeType(mimeType: string) { 'audio/ogg', 'audio/mpeg', 'audio/mp4', + 'application/json' ].indexOf(mimeType) === -1) { return 'application/octet-stream'; } diff --git a/src/lib/filemanager.ts b/src/lib/filemanager.ts index 2aa009ee..b56337ab 100644 --- a/src/lib/filemanager.ts +++ b/src/lib/filemanager.ts @@ -15,12 +15,8 @@ export class FileManager { return this.blobSupported; } - public write(fileWriter: ReturnType, bytes: Uint8Array | Blob | {file: any}): Promise { - if('file' in bytes) { - return bytes.file((file: any) => { - return fileWriter.write(file); - }); - } else if(bytes instanceof Blob) { // is file bytes + public write(fileWriter: ReturnType, bytes: Uint8Array | Blob | string): Promise { + if(bytes instanceof Blob) { // is file bytes return new Promise((resolve, reject) => { let fileReader = new FileReader(); fileReader.onload = function(event) { @@ -39,20 +35,20 @@ export class FileManager { } public getFakeFileWriter(mimeType: string, saveFileCallback: (blob: Blob) => Promise) { - let blobParts: Array = []; + const blobParts: Array = []; const fakeFileWriter = { - write: async(blob: Uint8Array) => { + write: async(part: Uint8Array | string) => { if(!this.blobSupported) { throw false; } - blobParts.push(blob); + blobParts.push(part); }, truncate: () => { - blobParts = []; + blobParts.length = 0; }, finalize: () => { - const blob = blobConstruct(blobParts, mimeType) as Blob; + const blob = blobConstruct(blobParts, mimeType); if(saveFileCallback) { saveFileCallback(blob); } diff --git a/src/lib/mtproto/apiFileManager.ts b/src/lib/mtproto/apiFileManager.ts index 98e341a9..2ffd141a 100644 --- a/src/lib/mtproto/apiFileManager.ts +++ b/src/lib/mtproto/apiFileManager.ts @@ -4,9 +4,10 @@ import cacheStorage from "../cacheStorage"; import FileManager from "../filemanager"; import apiManager from "./apiManager"; import { deferredPromise, CancellablePromise } from "../polyfill"; -import { logger } from "../logger"; +import { logger, LogLevels } from "../logger"; import { InputFileLocation, FileLocation, UploadFile } from "../../types"; import { isSafari } from "../../helpers/userAgent"; +import cryptoWorker from "../crypto/cryptoworker"; type Delayed = { offset: number, @@ -43,7 +44,7 @@ export class ApiFileManager { public webpConvertPromises: {[fileName: string]: CancellablePromise} = {}; - private log: ReturnType = logger('AFM'); + private log: ReturnType = logger('AFM', LogLevels.error); public downloadRequest(dcID: 'upload', cb: () => Promise, activeDelta: number): Promise; public downloadRequest(dcID: number, cb: () => Promise, activeDelta: number): Promise; @@ -144,6 +145,29 @@ export class ApiFileManager { return bytes * 1024; } + uncompressTGS = (bytes: Uint8Array, fileName: string) => { + //this.log('uncompressTGS', bytes, bytes.slice().buffer); + // slice нужен потому что в uint8array - 5053 length, в arraybuffer - 5084 + return cryptoWorker.gzipUncompress(bytes.slice().buffer, true); + }; + + convertWebp = (bytes: Uint8Array, fileName: string) => { + const convertPromise = deferredPromise(); + + (self as any as ServiceWorkerGlobalScope) + .clients + .matchAll({includeUncontrolled: false, type: 'window'}) + .then((listeners) => { + if(!listeners.length) { + return; + } + + listeners[0].postMessage({type: 'convertWebp', payload: {fileName, bytes}}); + }); + + return this.webpConvertPromises[fileName] = convertPromise; + }; + public downloadFile(options: DownloadOptions): CancellablePromise { if(!FileManager.isAvailable()) { return Promise.reject({type: 'BROWSER_BLOB_NOT_SUPPORTED'}); @@ -152,18 +176,21 @@ export class ApiFileManager { let size = options.size ?? 0; let {dcID, location} = options; - let processWebp = false; + let process: ApiFileManager['uncompressTGS'] | ApiFileManager['convertWebp']; + if(options.mimeType == 'image/webp' && isSafari) { - processWebp = true; + process = this.convertWebp; options.mimeType = 'image/png'; + } else if(options.mimeType == 'application/x-tgsticker') { + process = this.uncompressTGS; + options.mimeType = 'application/json'; } - // this.log('Dload file', dcID, location, size) const fileName = getFileNameByLocation(location); const cachedPromise = this.cachedDownloadPromises[fileName]; const fileStorage = this.getFileStorage(); - //this.log('downloadFile', fileName, fileName.length, location, arguments); + //this.log('downloadFile', fileName, size, location, options.mimeType, process); if(cachedPromise) { if(options.processPart) { @@ -266,22 +293,11 @@ export class ApiFileManager { await options.processPart(bytes, offset, delayed); } - if(processWebp) { - const convertPromise = deferredPromise(); - - (self as any as ServiceWorkerGlobalScope) - .clients - .matchAll({includeUncontrolled: false, type: 'window'}) - .then((listeners) => { - if(!listeners.length) { - return; - } - - listeners[0].postMessage({type: 'convertWebp', payload: {fileName, bytes}}); - }); - - return await (this.webpConvertPromises[fileName] = convertPromise); - //return appWebpManager.convertToPng(bytes); + if(process) { + //const perf = performance.now(); + const processed = await process(bytes, fileName); + //this.log('downloadFile process downloaded time', performance.now() - perf, mimeType, process); + return processed; } return bytes; @@ -318,12 +334,12 @@ export class ApiFileManager { superpuper(); } - ////////////////////////////////////////// + //done += limit; + done += result.bytes.byteLength; + const processedResult = await processDownloaded(result.bytes, offset); checkCancel(); - //done += limit; - done += processedResult.byteLength; const isFinal = offset + limit >= size; //if(!isFinal) { ////this.log('deferred notify 2:', {done: offset + limit, total: size}, deferred); diff --git a/src/lib/mtproto/mtproto.service.ts b/src/lib/mtproto/mtproto.service.ts index ae383c04..34f8570d 100644 --- a/src/lib/mtproto/mtproto.service.ts +++ b/src/lib/mtproto/mtproto.service.ts @@ -11,7 +11,7 @@ import { getFileNameByLocation } from '../bin_utils'; import { logger, LogLevels } from '../logger'; import { isSafari } from '../../helpers/userAgent'; -const log = logger('SW'/* , LogLevels.error */); +const log = logger('SW', LogLevels.error); const ctx = self as any as ServiceWorkerGlobalScope; diff --git a/tsconfig.json b/tsconfig.json index d5f1ec48..2062d2ac 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,7 @@ "sourceMap": true, /* Generates corresponding '.map' file. */ // "outFile": "./", /* Concatenate and emit output to single file. */ // "outDir": "./dist/", /* Redirect output structure to the directory. */ - // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + //"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ // "composite": true, /* Enable project compilation */ // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ // "removeComments": true, /* Do not emit comments to output. */ @@ -65,8 +65,9 @@ "node_modules", "public", "coverage", + "./public/*.js", "./src/lib/crypto/crypto.worker.js", - "src/vendor/StackBlur.js", + "./src/vendor/StackBlur.js", "./src/lib/*.js", "./src/*.js", "*.js",