From 17410a7a59e3b911ff01d34585ec7d3da46344eb Mon Sep 17 00:00:00 2001 From: morethanwords Date: Tue, 25 Aug 2020 12:39:39 +0300 Subject: [PATCH] Avatar & images fallback to blob due to speed --- src/components/appAudio.ts | 2 +- src/components/audio.ts | 8 +- src/components/avatar.ts | 5 + src/components/emoticonsDropdown.ts | 14 +-- src/components/misc.ts | 48 +++++---- src/components/preloader.ts | 59 ++++++----- src/components/preloader_new.ts | 123 ---------------------- src/components/wrappers.ts | 69 ++++++------ src/helpers/userAgent.ts | 2 +- src/lib/appManagers/appDocsManager.ts | 27 ++--- src/lib/appManagers/appDownloadManager.ts | 45 +++++--- src/lib/appManagers/appImManager.ts | 31 +++--- src/lib/appManagers/appMediaViewer.ts | 4 +- src/lib/appManagers/appPhotosManager.ts | 101 +++++++++--------- src/lib/appManagers/appSidebarRight.ts | 12 +-- src/lib/polyfill.ts | 35 +++++- src/lib/webp/webpWorkerController.ts | 4 + src/scss/partials/_scrollable.scss | 5 + src/types.d.ts | 5 +- 19 files changed, 288 insertions(+), 311 deletions(-) delete mode 100644 src/components/preloader_new.ts diff --git a/src/components/appAudio.ts b/src/components/appAudio.ts index 4acef3b7..a0ecc01d 100644 --- a/src/components/appAudio.ts +++ b/src/components/appAudio.ts @@ -61,7 +61,7 @@ class AppAudio { audio.addEventListener('error', onError); - const downloadPromise: Promise = !doc.supportsStreaming ? appDocsManager.downloadDocNew(doc.id).promise : Promise.resolve(); + const downloadPromise: Promise = !doc.supportsStreaming ? appDocsManager.downloadDocNew(doc.id) : Promise.resolve(); downloadPromise.then(() => { this.container.append(audio); diff --git a/src/components/audio.ts b/src/components/audio.ts index d7e087ca..8f7e0cc9 100644 --- a/src/components/audio.ts +++ b/src/components/audio.ts @@ -1,7 +1,7 @@ import appDocsManager from "../lib/appManagers/appDocsManager"; import { RichTextProcessor } from "../lib/richtextprocessor"; import { formatDate } from "./wrappers"; -import ProgressivePreloader from "./preloader_new"; +import ProgressivePreloader from "./preloader"; import { MediaProgressLine } from "../lib/mediaPlayer"; import appAudio from "./appAudio"; import { MTDocument } from "../types"; @@ -367,9 +367,9 @@ export default class AudioElement extends HTMLElement { } download = appDocsManager.downloadDocNew(doc.id); - preloader.attach(downloadDiv, true, appDocsManager.getInputFileName(doc)); + preloader.attach(downloadDiv, true, download); - download.promise.then(() => { + download.then(() => { downloadDiv.remove(); this.removeEventListener('click', onClick); onLoad(); @@ -383,7 +383,7 @@ export default class AudioElement extends HTMLElement { downloadDiv.classList.add('downloading'); } else { - download.controller.abort(); + download.cancel(); } }; diff --git a/src/components/avatar.ts b/src/components/avatar.ts index 1cbb31d5..60f89ff5 100644 --- a/src/components/avatar.ts +++ b/src/components/avatar.ts @@ -38,8 +38,13 @@ export default class AvatarElement extends HTMLElement { } attributeChangedCallback(name: string, oldValue: string, newValue: string) { + //console.log('avatar changed attribute:', name, oldValue, newValue); // вызывается при изменении одного из перечисленных выше атрибутов if(name == 'peer') { + if(this.peerID == +newValue) { + return; + } + this.peerID = +newValue; this.update(); } else if(name == 'peer-title') { diff --git a/src/components/emoticonsDropdown.ts b/src/components/emoticonsDropdown.ts index f10b2f83..d123245a 100644 --- a/src/components/emoticonsDropdown.ts +++ b/src/components/emoticonsDropdown.ts @@ -11,7 +11,7 @@ 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_new"; +import ProgressivePreloader from "./preloader"; import Config, { touchSupport } from "../lib/config"; import { MTDocument } from "../types"; import animationIntersector from "./animationIntersector"; @@ -377,9 +377,9 @@ class StickersTab implements EmoticonsTab { }); } else { const image = new Image(); - renderImageFromUrl(image, thumbURL).then(() => { + renderImageFromUrl(image, thumbURL, () => { li.append(image); - }) + }); } } else { // as thumb will be used first sticker wrapSticker({ @@ -636,7 +636,7 @@ class GifsTab implements EmoticonsTab { }, {once: true}); }; - ((posterURL ? renderImageFromUrl(img, posterURL) : Promise.resolve()) as Promise).then(() => { + const afterRender = () => { if(img) { div.append(img); div.style.opacity = ''; @@ -658,7 +658,9 @@ class GifsTab implements EmoticonsTab { div.append(img); div.addEventListener('mouseover', onMouseOver, {once: true}); }); - }); + }; + + (posterURL ? renderImageFromUrl(img, posterURL, afterRender) : afterRender()); /* wrapVideo({ doc, @@ -742,7 +744,7 @@ class EmoticonsDropdown { if(firstTime) { this.toggleEl.onmouseout = this.element.onmouseout = (e) => { const toElement = (e as any).toElement as Element; - if(findUpClassName(toElement, 'emoji-dropdown')) { + if(toElement && findUpClassName(toElement, 'emoji-dropdown')) { return; } diff --git a/src/components/misc.ts b/src/components/misc.ts index bfcb249e..2c8afe96 100644 --- a/src/components/misc.ts +++ b/src/components/misc.ts @@ -1,36 +1,40 @@ import Config, { touchSupport, isApple, mediaSizes } from "../lib/config"; -let loadedURLs: {[url: string]: boolean} = {}; -let set = (elem: HTMLElement | HTMLImageElement | SVGImageElement | HTMLVideoElement, url: string) => { +export const loadedURLs: {[url: string]: boolean} = {}; +const set = (elem: HTMLElement | HTMLImageElement | SVGImageElement | HTMLVideoElement, url: string) => { if(elem instanceof HTMLImageElement || elem instanceof HTMLVideoElement) elem.src = url; else if(elem instanceof SVGImageElement) elem.setAttributeNS(null, 'href', url); else elem.style.backgroundImage = 'url(' + url + ')'; }; -export async function renderImageFromUrl(elem: HTMLElement | HTMLImageElement | SVGImageElement | HTMLVideoElement, url: string): Promise { - if(loadedURLs[url]) { +// проблема функции в том, что она не подходит для ссылок, пригодна только для blob'ов, потому что обычным ссылкам нужен 'load' каждый раз. +export function renderImageFromUrl(elem: HTMLElement | HTMLImageElement | SVGImageElement | HTMLVideoElement, url: string, callback?: (err?: Event) => void): boolean { + if((loadedURLs[url]/* && false */) || elem instanceof HTMLVideoElement) { set(elem, url); + callback && callback(); + return true; } else { - if(elem instanceof HTMLVideoElement) { - set(elem, 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); - }); + const isImage = elem instanceof HTMLImageElement; + const loader = isImage ? elem as HTMLImageElement : new Image(); + //const loader = new Image(); + loader.src = url; + //let perf = performance.now(); + loader.addEventListener('load', () => { + if(!isImage) { + set(elem, url); + } + + loadedURLs[url] = true; + //console.log('onload:', url, performance.now() - perf); + callback && callback(); + }); + + if(callback) { + loader.addEventListener('error', callback); } - - } - return !!loadedURLs[url]; + return false; + } } export function putPreloader(elem: Element, returnDiv = false) { diff --git a/src/components/preloader.ts b/src/components/preloader.ts index 95e10d6d..30991de4 100644 --- a/src/components/preloader.ts +++ b/src/components/preloader.ts @@ -1,13 +1,15 @@ -import { isInDOM } from "../lib/utils"; +import { isInDOM, cancelEvent } from "../lib/utils"; import { CancellablePromise } from "../lib/polyfill"; export default class ProgressivePreloader { - public preloader: HTMLDivElement = null; - private circle: SVGCircleElement = null; - private promise: CancellablePromise = null; + public preloader: HTMLDivElement; + private circle: SVGCircleElement; + private tempID = 0; private detached = true; + private promise: CancellablePromise = null; + constructor(elem?: Element, private cancelable = true) { this.preloader = document.createElement('div'); this.preloader.classList.add('preloader-container'); @@ -36,7 +38,9 @@ export default class ProgressivePreloader { } if(this.cancelable) { - this.preloader.addEventListener('click', () => { + this.preloader.addEventListener('click', (e) => { + cancelEvent(e); + if(this.promise && this.promise.cancel) { this.promise.cancel(); this.detach(); @@ -44,13 +48,14 @@ export default class ProgressivePreloader { }); } } - + public attach(elem: Element, reset = true, promise?: CancellablePromise, append = true) { if(promise) { this.promise = promise; - let tempID = --this.tempID; - let onEnd = () => { + const tempID = --this.tempID; + + const onEnd = () => { promise.notify = null; if(tempID == this.tempID) { @@ -58,37 +63,41 @@ export default class ProgressivePreloader { this.promise = promise = null; } }; + //promise.catch(onEnd); promise.finally(onEnd); - promise.notify = (details: {done: number, total: number}) => { - /* if(details.done >= details.total) { - onEnd(); - } */ - - if(tempID != this.tempID) return; - - //console.log('preloader download', promise, details); - let percents = details.done / details.total * 100; - this.setProgress(percents); - }; + if(promise.addNotifyListener) { + promise.addNotifyListener((details: {done: number, total: number}) => { + /* if(details.done >= details.total) { + onEnd(); + } */ + + if(tempID != this.tempID) return; + + //console.log('preloader download', promise, details); + const percents = details.done / details.total * 100; + this.setProgress(percents); + }); + } } - if(this.cancelable && reset) { - this.setProgress(0); - } - this.detached = false; window.requestAnimationFrame(() => { if(this.detached) return; this.detached = false; - + elem[append ? 'append' : 'prepend'](this.preloader); + + if(this.cancelable && reset) { + this.setProgress(0); + } }); } public detach() { this.detached = true; + if(this.preloader.parentElement) { window.requestAnimationFrame(() => { if(!this.detached) return; @@ -112,7 +121,7 @@ export default class ProgressivePreloader { } try { - let totalLength = this.circle.getTotalLength(); + const totalLength = this.circle.getTotalLength(); //console.log('setProgress', (percents / 100 * totalLength)); this.circle.style.strokeDasharray = '' + Math.max(5, percents / 100 * totalLength) + ', 200'; } catch(err) {} diff --git a/src/components/preloader_new.ts b/src/components/preloader_new.ts deleted file mode 100644 index 81c4d1f6..00000000 --- a/src/components/preloader_new.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { isInDOM, $rootScope, cancelEvent } from "../lib/utils"; -import appDownloadManager, { Progress } from "../lib/appManagers/appDownloadManager"; - -export default class ProgressivePreloader { - public preloader: HTMLDivElement; - private circle: SVGCircleElement; - - //private tempID = 0; - private detached = true; - - private fileName: string; - public controller: AbortController; - - constructor(elem?: Element, private cancelable = true) { - this.preloader = document.createElement('div'); - this.preloader.classList.add('preloader-container'); - - this.preloader.innerHTML = ` -
- - - -
`; - - if(cancelable) { - this.preloader.innerHTML += ` - - - - `; - } else { - this.preloader.classList.add('preloader-swing'); - } - - this.circle = this.preloader.firstElementChild.firstElementChild.firstElementChild as SVGCircleElement; - - if(elem) { - this.attach(elem); - } - - if(this.cancelable) { - this.preloader.addEventListener('click', (e) => { - cancelEvent(e); - this.detach(); - - if(!this.fileName) { - return; - } - - const download = appDownloadManager.getDownload(this.fileName); - if(download && download.controller && !download.controller.signal.aborted) { - download.controller.abort(); - } - }); - } - } - - downloadProgressHandler = (details: Progress) => { - if(details.done >= details.total) { - this.detach(); - } - - //console.log('preloader download', promise, details); - let percents = details.done / details.total * 100; - this.setProgress(percents); - }; - - public attach(elem: Element, reset = true, fileName?: string, append = true) { - this.fileName = fileName; - if(this.fileName) { - const download = appDownloadManager.getDownload(fileName); - download.promise.catch(() => { - this.detach(); - }); - - appDownloadManager.addProgressCallback(this.fileName, this.downloadProgressHandler); - } - - this.detached = false; - window.requestAnimationFrame(() => { - if(this.detached) return; - this.detached = false; - - elem[append ? 'append' : 'prepend'](this.preloader); - - if(this.cancelable && reset) { - this.setProgress(0); - } - }); - } - - public detach() { - this.detached = true; - - if(this.preloader.parentElement) { - window.requestAnimationFrame(() => { - if(!this.detached) return; - this.detached = true; - - if(this.preloader.parentElement) { - this.preloader.parentElement.removeChild(this.preloader); - } - }); - } - } - - public setProgress(percents: number) { - if(!isInDOM(this.circle)) { - return; - } - - if(percents == 0) { - this.circle.style.strokeDasharray = ''; - return; - } - - try { - let totalLength = this.circle.getTotalLength(); - //console.log('setProgress', (percents / 100 * totalLength)); - this.circle.style.strokeDasharray = '' + Math.max(5, percents / 100 * totalLength) + ', 200'; - } catch(err) {} - } -} diff --git a/src/components/wrappers.ts b/src/components/wrappers.ts index 0266df8f..7e3fe832 100644 --- a/src/components/wrappers.ts +++ b/src/components/wrappers.ts @@ -3,11 +3,10 @@ import LottieLoader from '../lib/lottieLoader'; import appDocsManager from "../lib/appManagers/appDocsManager"; import { formatBytes, getEmojiToneIndex } from "../lib/utils"; import ProgressivePreloader from './preloader'; -import ProgressivePreloaderNew from './preloader_new'; import LazyLoadQueue from './lazyLoadQueue'; import VideoPlayer from '../lib/mediaPlayer'; import { RichTextProcessor } from '../lib/richtextprocessor'; -import { renderImageFromUrl } from './misc'; +import { renderImageFromUrl, loadedURLs } from './misc'; import appMessagesManager from '../lib/appManagers/appMessagesManager'; import { Layouter, RectPart } from './groupedLayout'; import PollElement from './poll'; @@ -15,7 +14,7 @@ import { mediaSizes, isSafari } from '../lib/config'; import { MTDocument, MTPhotoSize } from '../types'; import animationIntersector from './animationIntersector'; import AudioElement from './audio'; -import { Download } from '../lib/appManagers/appDownloadManager'; +import appDownloadManager, { Download } from '../lib/appManagers/appDownloadManager'; import { webpWorkerController } from '../lib/webp/webpWorkerController'; export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTail, isOut, middleware, lazyLoadQueue, noInfo, group}: { @@ -228,7 +227,7 @@ export function wrapDocument(doc: MTDocument, withTime = false, uploading = fals if(!uploading) { let downloadDiv = docDiv.querySelector('.document-download') as HTMLDivElement; - let preloader: ProgressivePreloaderNew; + let preloader: ProgressivePreloader; let download: Download; docDiv.addEventListener('click', () => { @@ -238,13 +237,13 @@ export function wrapDocument(doc: MTDocument, withTime = false, uploading = fals } if(!preloader) { - preloader = new ProgressivePreloaderNew(null, true); + preloader = new ProgressivePreloader(null, true); } download = appDocsManager.saveDocFile(doc); - preloader.attach(downloadDiv, true, appDocsManager.getInputFileName(doc)); + preloader.attach(downloadDiv, true, download); - download.promise.then(() => { + download.then(() => { downloadDiv.remove(); }).catch(err => { if(err.name === 'AbortError') { @@ -256,7 +255,7 @@ export function wrapDocument(doc: MTDocument, withTime = false, uploading = fals downloadDiv.classList.add('downloading'); } else { - download.controller.abort(); + download.cancel(); } }); } @@ -328,7 +327,7 @@ function wrapMediaWithTail(photo: any, message: {mid: number, message: string}, } export function wrapPhoto(photoID: any, message: any, container: HTMLDivElement, boxWidth = mediaSizes.active.regular.width, boxHeight = mediaSizes.active.regular.height, withTail = true, isOut = false, lazyLoadQueue: LazyLoadQueue, middleware: () => boolean, size: MTPhotoSize = null) { - let photo = appPhotosManager.getPhoto(photoID); + const photo = appPhotosManager.getPhoto(photoID); let image: HTMLImageElement; if(withTail) { @@ -351,36 +350,39 @@ export function wrapPhoto(photoID: any, message: any, container: HTMLDivElement, //console.log('wrapPhoto downloaded:', photo, photo.downloaded, container); - // так нельзя делать, потому что может быть загружен неправильный размер картинки - /* if(photo.downloaded && photo.url) { - renderImageFromUrl(image, photo.url); - return; - } */ + const cacheContext = appPhotosManager.getCacheContext(photo); let preloader: ProgressivePreloader; if(message.media.preloader) { // means upload message.media.preloader.attach(container); - } else if(!photo.downloaded) { + } else if(!cacheContext.downloaded) { preloader = new ProgressivePreloader(container, false); } - let load = () => { - let promise = appPhotosManager.preloadPhoto(photoID, size); - + const load = () => { + const promise = appPhotosManager.preloadPhoto(photoID, size); + 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; - renderImageFromUrl(image || container, photo._ == 'photo' ? photo.url : appPhotosManager.getDocumentCachedThumb(photo.id).url); + renderImageFromUrl(image || container, cacheContext.url); }); }; - - /////////console.log('wrapPhoto', load, container, image); - - return photo.downloaded ? load() : lazyLoadQueue.push({div: container, load: load, wasSeen: true}); + + return cacheContext.downloaded ? load() : lazyLoadQueue.push({div: container, load: load, wasSeen: true}); } export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, onlyThumb, emoji, width, height, withThumb, loop}: { @@ -439,7 +441,7 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o img = new Image(); if((!isSafari || doc.stickerThumbConverted)/* && false */) { - renderImageFromUrl(img, appPhotosManager.getPreviewURLFromThumb(thumb, true)).then(afterRender); + renderImageFromUrl(img, appPhotosManager.getPreviewURLFromThumb(thumb, true), afterRender); } else { webpWorkerController.convert(doc.id, thumb.bytes).then(bytes => { if(middleware && !middleware()) return; @@ -448,7 +450,7 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o doc.stickerThumbConverted = true; if(!div.childElementCount) { - renderImageFromUrl(img, appPhotosManager.getPreviewURLFromThumb(thumb, true)).then(afterRender); + renderImageFromUrl(img, appPhotosManager.getPreviewURLFromThumb(thumb, true), afterRender); } }); } @@ -461,11 +463,7 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o const load = () => { if(div.childElementCount || (middleware && !middleware())) return; - const promise = renderImageFromUrl(img, appDocsManager.getFileURL(doc, false, thumb)); - - //if(!downloaded) { - promise.then(afterRender); - //} + renderImageFromUrl(img, appDocsManager.getFileURL(doc, false, thumb), afterRender); }; /* let downloaded = appDocsManager.hasDownloadedThumb(doc.id, thumb.type); @@ -483,10 +481,12 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o let load = () => { let img = new Image(); - return renderImageFromUrl(img, appDocsManager.getFileURL(doc, false, thumb)).then(() => { + 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(); @@ -504,11 +504,14 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o //console.time('download sticker' + doc.id); //appDocsManager.downloadDocNew(doc.id).promise.then(res => res.json()).then(async(json) => { - fetch(doc.url).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) => { //console.timeEnd('download sticker' + doc.id); //console.log('loaded sticker:', doc, div); if(middleware && !middleware()) return; + //await new Promise((resolve) => setTimeout(resolve, 5e3)); + let animation = await LottieLoader.loadAnimationWorker/* loadAnimation */({ container: div, loop: loop && !emoji, @@ -554,7 +557,7 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o }); } - renderImageFromUrl(img, doc.url).then(() => { + renderImageFromUrl(img, doc.url, () => { if(div.firstElementChild && div.firstElementChild != img) { div.firstElementChild.remove(); } diff --git a/src/helpers/userAgent.ts b/src/helpers/userAgent.ts index ec565c44..9e0d95fc 100644 --- a/src/helpers/userAgent.ts +++ b/src/helpers/userAgent.ts @@ -12,4 +12,4 @@ export const isAndroid = navigator.userAgent.toLowerCase().indexOf('android') != */ const ctx = typeof(window) !== 'undefined' ? window : self; -export const isSafari = !!('safari' in ctx) || !!(userAgent && (/\b(iPad|iPhone|iPod)\b/.test(userAgent) || (!!userAgent.match('Safari') && !userAgent.match('Chrome')))); \ No newline at end of file +export const isSafari = !!('safari' in ctx) || !!(userAgent && (/\b(iPad|iPhone|iPod)\b/.test(userAgent) || (!!userAgent.match('Safari') && !userAgent.match('Chrome'))))/* || true */; \ No newline at end of file diff --git a/src/lib/appManagers/appDocsManager.ts b/src/lib/appManagers/appDocsManager.ts index ccfeaaf0..4a0710ed 100644 --- a/src/lib/appManagers/appDocsManager.ts +++ b/src/lib/appManagers/appDocsManager.ts @@ -14,16 +14,21 @@ class AppDocsManager { public saveDoc(apiDoc: MTDocument, context?: any) { //console.log('saveDoc', apiDoc, this.docs[apiDoc.id]); if(this.docs[apiDoc.id]) { - let d = this.docs[apiDoc.id]; + const d = this.docs[apiDoc.id]; if(apiDoc.thumbs) { if(!d.thumbs) d.thumbs = apiDoc.thumbs; - else if(apiDoc.thumbs[0].bytes && !d.thumbs[0].bytes) { + /* 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 + apiDoc.thumbs[0] = d.thumbs[0]; + } */ } - return Object.assign(d, apiDoc, context); + d.file_reference = apiDoc.file_reference; + return d; + + //return Object.assign(d, apiDoc, context); //return context ? Object.assign(d, context) : d; } @@ -33,10 +38,6 @@ class AppDocsManager { this.docs[apiDoc.id] = apiDoc; - if(apiDoc.thumb && apiDoc.thumb._ == 'photoSizeEmpty') { - delete apiDoc.thumb; - } - apiDoc.attributes.forEach((attribute: any) => { switch(attribute._) { case 'documentAttributeFilename': @@ -216,7 +217,7 @@ class AppDocsManager { const thumb = doc.thumbs.find(t => !t.bytes); if(thumb) { - const url = appDocsManager.getFileURL(doc, false, thumb); + const url = this.getFileURL(doc, false, thumb); return url; } } @@ -304,15 +305,15 @@ class AppDocsManager { return download; } - download = appDownloadManager.download(fileName, doc.url/* , method */); + download = appDownloadManager.download(doc.url, fileName, /*method*/); - const originalPromise = download.promise; + const originalPromise = download; originalPromise.then(() => { doc.downloaded = true; }); if(doc.type == 'voice' && !opusDecodeController.isPlaySupported()) { - download.promise = originalPromise.then(async(blob) => { + download = originalPromise.then(async(blob) => { let reader = new FileReader(); await new Promise((resolve, reject) => { @@ -344,7 +345,7 @@ class AppDocsManager { const url = this.getFileURL(doc, true); const fileName = this.getInputFileName(doc); - return appDownloadManager.downloadToDisc(fileName, url); + return appDownloadManager.downloadToDisc(fileName, url, doc.file_name); } } diff --git a/src/lib/appManagers/appDownloadManager.ts b/src/lib/appManagers/appDownloadManager.ts index 94ff1b52..6fcee415 100644 --- a/src/lib/appManagers/appDownloadManager.ts +++ b/src/lib/appManagers/appDownloadManager.ts @@ -1,12 +1,16 @@ import { $rootScope } from "../utils"; import apiManager from "../mtproto/mtprotoworker"; +import { deferredPromise, CancellablePromise } from "../polyfill"; 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 DownloadBlob = {promise: Promise, controller: AbortController}; +export type DownloadJson = {promise: Promise, controller: AbortController}; */ +export type DownloadBlob = CancellablePromise; +export type DownloadJson = CancellablePromise; +//export type Download = DownloadBlob/* | DownloadJson */; export type Download = DownloadBlob/* | DownloadJson */; export type Progress = {done: number, fileName: string, total: number, offset: number}; @@ -26,17 +30,25 @@ export class AppDownloadManager { if(callbacks) { callbacks.forEach(callback => callback(details)); } + + const download = this.downloads[details.fileName]; + if(download) { + download.notifyAll(details); + } }); } - 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 { + 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'): Download { if(this.downloads.hasOwnProperty(fileName)) return this.downloads[fileName]; + const deferred = deferredPromise(); + const controller = new AbortController(); const promise = fetch(url, {signal: controller.signal}) .then(res => res[responseMethod]()) + .then(res => deferred.resolve(res)) .catch(err => { // Только потому что event.request.signal не работает в SW, либо я кривой? if(err.name === 'AbortError') { //console.log('Fetch aborted'); @@ -48,6 +60,7 @@ export class AppDownloadManager { //console.error('Uh oh, an error!', err); } + deferred.reject(err); throw err; }); @@ -57,7 +70,13 @@ export class AppDownloadManager { delete this.progressCallbacks[fileName]; }); - return this.downloads[fileName] = {promise, controller}; + deferred.cancel = () => { + controller.abort(); + deferred.cancel = () => {}; + }; + + //return this.downloads[fileName] = {promise, controller}; + return this.downloads[fileName] = deferred; } public getDownload(fileName: string) { @@ -73,10 +92,12 @@ export class AppDownloadManager { } } - private createDownloadAnchor(url: string, onRemove?: () => void) { + private createDownloadAnchor(url: string, fileName: string, onRemove?: () => void) { const a = document.createElement('a'); a.href = url; - + a.download = fileName; + a.target = '_blank'; + a.style.position = 'absolute'; a.style.top = '1px'; a.style.left = '1px'; @@ -108,11 +129,11 @@ export class AppDownloadManager { return this.download(fileName, url); } */ - public downloadToDisc(fileName: string, url: string) { - const download = this.download(fileName, url); - download.promise.then(blob => { + public downloadToDisc(fileName: string, url: string, discFileName: string) { + const download = this.download(url, fileName); + download/* .promise */.then(blob => { const objectURL = URL.createObjectURL(blob); - this.createDownloadAnchor(objectURL, () => { + this.createDownloadAnchor(objectURL, discFileName, () => { URL.revokeObjectURL(objectURL); }); }); diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index 302d0cd5..791f9873 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -553,7 +553,7 @@ export class AppImManager { private closeBtn = this.topbar.querySelector('.sidebar-close-button') as HTMLButtonElement; constructor() { - this.log = logger('IM', /* LogLevels.log | LogLevels.warn | LogLevels.debug | */ LogLevels.error); + this.log = logger('IM', LogLevels.log | LogLevels.warn | LogLevels.debug | LogLevels.error); this.chatInputC = new ChatInput(); this.preloader = new ProgressivePreloader(null, false); this.selectTab(0); @@ -1624,6 +1624,7 @@ export class AppImManager { this.peerChanged = true; this.avatarEl.setAttribute('peer', '' + this.peerID); + this.avatarEl.update(); const isAnyGroup = appPeersManager.isAnyGroup(peerID); const isChannel = appPeersManager.isChannel(peerID); @@ -1822,8 +1823,7 @@ export class AppImManager { resolve(); // lol - el.removeEventListener('canplay', onLoad); - el.removeEventListener('load', onLoad); + el.removeEventListener(el instanceof HTMLVideoElement ? 'canplay' : 'load', onLoad); }; if(el instanceof HTMLVideoElement) { @@ -2286,20 +2286,26 @@ export class AppImManager { bubble.classList.add('hide-name', 'photo'); const tailSupported = !isAndroid; if(tailSupported) bubble.classList.add('with-media-tail'); + if(message.grouped_id) { bubble.classList.add('is-album'); - wrapAlbum({ - groupID: message.grouped_id, - attachmentDiv, - middleware: this.getMiddleware(), - isOut: our, - lazyLoadQueue: this.lazyLoadQueue - }); - } else { - wrapPhoto(photo.id, message, attachmentDiv, undefined, undefined, tailSupported, isOut, this.lazyLoadQueue, this.getMiddleware()); + let storage = appMessagesManager.groupedMessagesStorage[message.grouped_id]; + if(Object.keys(storage).length != 1) { + wrapAlbum({ + groupID: message.grouped_id, + attachmentDiv, + middleware: this.getMiddleware(), + isOut: our, + lazyLoadQueue: this.lazyLoadQueue + }); + + break; + } } + wrapPhoto(photo.id, message, attachmentDiv, undefined, undefined, tailSupported, isOut, this.lazyLoadQueue, this.getMiddleware()); + break; } @@ -2645,6 +2651,7 @@ export class AppImManager { } avatarElem.setAttribute('peer', '' + ((message.fwd_from && this.peerID == this.myID ? message.fwdFromID : message.fromID) || 0)); + avatarElem.update(); //this.log('exec loadDialogPhoto', message); diff --git a/src/lib/appManagers/appMediaViewer.ts b/src/lib/appManagers/appMediaViewer.ts index 979aeb64..d5daf884 100644 --- a/src/lib/appManagers/appMediaViewer.ts +++ b/src/lib/appManagers/appMediaViewer.ts @@ -132,7 +132,7 @@ export class AppMediaViewer { const download = (e: MouseEvent) => { let message = appMessagesManager.getMessage(this.currentMessageID); if(message.media.photo) { - appPhotosManager.downloadPhoto(message.media.photo.id); + appPhotosManager.savePhotoFile(message.media.photo.id); } else { let document: any = null; @@ -895,7 +895,7 @@ export class AppMediaViewer { //this.log('will renderImageFromUrl:', image, div, target); - renderImageFromUrl(image, url).then(() => { + renderImageFromUrl(image, url, () => { div.append(image); }); } diff --git a/src/lib/appManagers/appPhotosManager.ts b/src/lib/appManagers/appPhotosManager.ts index da545958..fadcd179 100644 --- a/src/lib/appManagers/appPhotosManager.ts +++ b/src/lib/appManagers/appPhotosManager.ts @@ -1,10 +1,9 @@ -import appUsersManager from "./appUsersManager"; import { calcImageInBox, isObject, getFileURL } from "../utils"; import { bytesFromHex, getFileNameByLocation } from "../bin_utils"; -//import apiManager from '../mtproto/apiManager'; -import apiManager from '../mtproto/mtprotoworker'; import { MTPhotoSize, inputPhotoFileLocation, inputDocumentFileLocation, FileLocation } from "../../types"; -import appDownloadManager from "./appDownloadManager"; +import appDownloadManager, { Download } from "./appDownloadManager"; +import { deferredPromise, CancellablePromise } from "../polyfill"; +import { isSafari } from "../../helpers/userAgent"; export type MTPhoto = { _: 'photo' | 'photoEmpty' | string, @@ -98,7 +97,7 @@ export class AppPhotosManager { return bestPhotoSize; } - public getUserPhotos(userID: number, maxID: number, limit: number) { + /* public getUserPhotos(userID: number, maxID: number, limit: number) { var inputUser = appUsersManager.getUserInput(userID); return apiManager.invokeApi('photos.getUserPhotos', { user_id: inputUser, @@ -120,7 +119,7 @@ export class AppPhotosManager { photos: photoIDs }; }); - } + } */ public getPreviewURLFromBytes(bytes: Uint8Array | number[], isSticker = false) { let arr: Uint8Array; @@ -131,17 +130,15 @@ export class AppPhotosManager { } else { arr = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes); } - - //console.log('getPreviewURLFromBytes', bytes, arr, div, isSticker); - /* let reader = new FileReader(); - reader.onloadend = () => { - let src = reader.result; - }; - reader.readAsDataURL(blob); */ - - let blob = new Blob([arr], {type: "image/jpeg"}); + let mimeType: string; + if(isSticker) { + mimeType = isSafari ? 'image/png' : 'image/webp'; + } else { + mimeType = 'image/jpeg'; + } + const blob = new Blob([arr], {type: mimeType}); return URL.createObjectURL(blob); } @@ -215,32 +212,18 @@ export class AppPhotosManager { return photoSize; } - - public preloadPhoto(photoID: any, photoSize?: MTPhotoSize): Promise { - const photo = this.getPhoto(photoID); - - if(!photoSize) { - const fullWidth = this.windowW; - const fullHeight = this.windowH; - - photoSize = this.choosePhotoSize(photo, fullWidth, fullHeight); - } + public getPhotoURL(photo: MTPhoto, photoSize: MTPhotoSize) { const isDocument = photo._ == 'document'; - const cacheContext = isDocument ? (this.documentThumbsCache[photo.id] ?? (this.documentThumbsCache[photo.id] = {downloaded: -1, url: ''})) : photo; - - if(cacheContext.downloaded >= photoSize.size && cacheContext.url) { - return Promise.resolve(); - } if(!photoSize || photoSize._ == 'photoSizeEmpty') { //console.error('no photoSize by photo:', photo); - return Promise.reject('no photoSize'); + throw new Error('photoSizeEmpty!'); } // maybe it's a thumb - let isPhoto = photoSize.size && photo.access_hash && photo.file_reference; - let location: inputPhotoFileLocation | inputDocumentFileLocation | FileLocation = isPhoto ? { + const isPhoto = photoSize.size && photo.access_hash && photo.file_reference; + const location: inputPhotoFileLocation | inputDocumentFileLocation | FileLocation = isPhoto ? { _: isDocument ? 'inputDocumentFileLocation' : 'inputPhotoFileLocation', id: photo.id, access_hash: photo.access_hash, @@ -248,30 +231,54 @@ export class AppPhotosManager { thumb_size: photoSize.type } : photoSize.location; - const url = getFileURL('photo', {dcID: photo.dc_id, location, size: isPhoto ? photoSize.size : undefined}); - let promise: Promise; - if(isPhoto/* && photoSize.size >= 1e6 */) { - promise = fetch(url).then(res => res.blob()); - } else { - //console.log('Photos downloadSmallFile exec', photo, location); - promise = fetch(url).then(res => res.blob()); + return {url: getFileURL('photo', {dcID: photo.dc_id, location, size: isPhoto ? photoSize.size : undefined}), location}; + } + + public preloadPhoto(photoID: any, photoSize?: MTPhotoSize): CancellablePromise { + const photo = this.getPhoto(photoID); + + if(!photoSize) { + const fullWidth = this.windowW; + const fullHeight = this.windowH; + + photoSize = this.choosePhotoSize(photo, fullWidth, fullHeight); } - promise.then(blob => { + const cacheContext = this.getCacheContext(photo); + if(cacheContext.downloaded >= photoSize.size && cacheContext.url) { + return Promise.resolve() as any; + } + + const {url, location} = this.getPhotoURL(photo, photoSize); + const fileName = getFileNameByLocation(location); + + let download = appDownloadManager.getDownload(fileName); + if(download) { + return download; + } + + download = appDownloadManager.download(url, fileName); + download.then(blob => { if(!cacheContext.downloaded || cacheContext.downloaded < blob.size) { cacheContext.downloaded = blob.size; - //cacheContext.url = URL.createObjectURL(blob); - cacheContext.url = url; + cacheContext.url = URL.createObjectURL(blob); //console.log('wrote photo:', photo, photoSize, cacheContext, blob); } + + return blob; }); - return promise; + return download; + //return fetch(url).then(res => res.blob()); + } + + public getCacheContext(photo: any) { + return photo._ == 'document' ? this.getDocumentCachedThumb(photo.id) : photo; } public getDocumentCachedThumb(docID: string) { - return this.documentThumbsCache[docID]; + return this.documentThumbsCache[docID] ?? (this.documentThumbsCache[docID] = {downloaded: 0, url: ''}); } public getPhoto(photoID: any): MTPhoto { @@ -293,7 +300,7 @@ export class AppPhotosManager { }; } - public downloadPhoto(photoID: string) { + public savePhotoFile(photoID: string) { const photo = this.photos[photoID]; const fullWidth = this.windowW; const fullHeight = this.windowH; @@ -309,7 +316,7 @@ export class AppPhotosManager { 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); + appDownloadManager.downloadToDisc(fileName, url, 'photo' + photo.id + '.jpg'); } } diff --git a/src/lib/appManagers/appSidebarRight.ts b/src/lib/appManagers/appSidebarRight.ts index 88ed7ac4..869f585b 100644 --- a/src/lib/appManagers/appSidebarRight.ts +++ b/src/lib/appManagers/appSidebarRight.ts @@ -797,14 +797,12 @@ export class AppSidebarRight extends SidebarSlider { const url = (photo && photo.url) || appPhotosManager.getDocumentCachedThumb(media.id).url; if(url) { //if(needBlur) return; - const p = renderImageFromUrl(img, url); - if(needBlur) { - p.then(() => { - //void img.offsetLeft; // reflow - img.style.opacity = ''; - }); - } + const needBlurCallback = needBlur ? () => { + //void img.offsetLeft; // reflow + img.style.opacity = ''; + } : undefined; + renderImageFromUrl(img, url, needBlurCallback); } }); diff --git a/src/lib/polyfill.ts b/src/lib/polyfill.ts index 0b880ace..dcd774d0 100644 --- a/src/lib/polyfill.ts +++ b/src/lib/polyfill.ts @@ -8,13 +8,39 @@ export interface CancellablePromise extends Promise { resolve?: (...args: any[]) => void, reject?: (...args: any[]) => void, cancel?: () => void, + notify?: (...args: any[]) => void, + notifyAll?: (...args: any[]) => void, + lastNotify?: any, + listeners?: Array<(...args: any[]) => void>, + addNotifyListener?: (callback: (...args: any[]) => void) => void, + isFulfilled?: boolean, isRejected?: boolean } export function deferredPromise() { - let deferredHelper: any = {notify: () => {}, isFulfilled: false, isRejected: false}; + let deferredHelper: any = { + isFulfilled: false, + isRejected: false, + + notify: () => {}, + notifyAll: (...args: any[]) => { + deferredHelper.lastNotify = args; + deferredHelper.listeners.forEach((callback: any) => callback(...args)); + }, + + lastNotify: undefined, + listeners: [], + addNotifyListener: (callback: (...args: any[]) => void) => { + if(deferredHelper.lastNotify) { + callback(...deferredHelper.lastNotify); + } + + deferredHelper.listeners.push(callback); + } + }; + let deferred: CancellablePromise = new Promise((resolve, reject) => { deferredHelper.resolve = (value: T) => { if(deferred.isFulfilled) return; @@ -30,6 +56,13 @@ export function deferredPromise() { reject(...args); }; }); + + deferred.finally(() => { + deferred.notify = null; + deferred.listeners.length = 0; + deferred.lastNotify = null; + }); + Object.assign(deferred, deferredHelper); return deferred; diff --git a/src/lib/webp/webpWorkerController.ts b/src/lib/webp/webpWorkerController.ts index c00a25f7..3a211bb8 100644 --- a/src/lib/webp/webpWorkerController.ts +++ b/src/lib/webp/webpWorkerController.ts @@ -40,6 +40,10 @@ export class WebpWorkerController { } convert(fileName: string, bytes: Uint8Array) { + if(this.convertPromises.hasOwnProperty(fileName)) { + return this.convertPromises[fileName]; + } + const convertPromise = deferredPromise(); fileName = 'main-' + fileName; diff --git a/src/scss/partials/_scrollable.scss b/src/scss/partials/_scrollable.scss index 80d1d587..8ea94dd8 100644 --- a/src/scss/partials/_scrollable.scss +++ b/src/scss/partials/_scrollable.scss @@ -114,9 +114,14 @@ div.scrollable::-webkit-scrollbar-thumb { // BROWSER SCROLL div.scrollable-y::-webkit-scrollbar { width: .375rem; + opacity: 0; // for safari //height: 200px; } +div.scrollable:hover::-webkit-scrollbar { + opacity: 1; // for safari +} + /* div.scrollable-y::-webkit-scrollbar-thumb { border: 2px solid rgba(0, 0, 0, 0); background-clip: padding-box; diff --git a/src/types.d.ts b/src/types.d.ts index b7c21f6b..06c6a64a 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -12,7 +12,6 @@ export type MTDocument = { dc_id: number, attributes: any[], - thumb?: MTPhotoSize, type?: 'gif' | 'sticker' | 'audio' | 'voice' | 'video' | 'round' | 'photo', h?: number, w?: number, @@ -42,7 +41,9 @@ export type MTPhotoSize = { size?: number, type?: string, // i, m, x, y, w by asc location?: FileLocation, - bytes?: Uint8Array // if type == 'i' + bytes?: Uint8Array, // if type == 'i', + + url?: string }; export type InvokeApiOptions = Partial<{