Browse Source

Avatar & images fallback to blob due to speed

master
morethanwords 4 years ago
parent
commit
17410a7a59
  1. 2
      src/components/appAudio.ts
  2. 8
      src/components/audio.ts
  3. 5
      src/components/avatar.ts
  4. 14
      src/components/emoticonsDropdown.ts
  5. 48
      src/components/misc.ts
  6. 59
      src/components/preloader.ts
  7. 123
      src/components/preloader_new.ts
  8. 69
      src/components/wrappers.ts
  9. 2
      src/helpers/userAgent.ts
  10. 27
      src/lib/appManagers/appDocsManager.ts
  11. 45
      src/lib/appManagers/appDownloadManager.ts
  12. 31
      src/lib/appManagers/appImManager.ts
  13. 4
      src/lib/appManagers/appMediaViewer.ts
  14. 101
      src/lib/appManagers/appPhotosManager.ts
  15. 12
      src/lib/appManagers/appSidebarRight.ts
  16. 35
      src/lib/polyfill.ts
  17. 4
      src/lib/webp/webpWorkerController.ts
  18. 5
      src/scss/partials/_scrollable.scss
  19. 5
      src/types.d.ts

2
src/components/appAudio.ts

@ -61,7 +61,7 @@ class AppAudio {
audio.addEventListener('error', onError); audio.addEventListener('error', onError);
const downloadPromise: Promise<any> = !doc.supportsStreaming ? appDocsManager.downloadDocNew(doc.id).promise : Promise.resolve(); const downloadPromise: Promise<any> = !doc.supportsStreaming ? appDocsManager.downloadDocNew(doc.id) : Promise.resolve();
downloadPromise.then(() => { downloadPromise.then(() => {
this.container.append(audio); this.container.append(audio);

8
src/components/audio.ts

@ -1,7 +1,7 @@
import appDocsManager from "../lib/appManagers/appDocsManager"; import appDocsManager from "../lib/appManagers/appDocsManager";
import { RichTextProcessor } from "../lib/richtextprocessor"; import { RichTextProcessor } from "../lib/richtextprocessor";
import { formatDate } from "./wrappers"; import { formatDate } from "./wrappers";
import ProgressivePreloader from "./preloader_new"; import ProgressivePreloader from "./preloader";
import { MediaProgressLine } from "../lib/mediaPlayer"; import { MediaProgressLine } from "../lib/mediaPlayer";
import appAudio from "./appAudio"; import appAudio from "./appAudio";
import { MTDocument } from "../types"; import { MTDocument } from "../types";
@ -367,9 +367,9 @@ export default class AudioElement extends HTMLElement {
} }
download = appDocsManager.downloadDocNew(doc.id); download = appDocsManager.downloadDocNew(doc.id);
preloader.attach(downloadDiv, true, appDocsManager.getInputFileName(doc)); preloader.attach(downloadDiv, true, download);
download.promise.then(() => { download.then(() => {
downloadDiv.remove(); downloadDiv.remove();
this.removeEventListener('click', onClick); this.removeEventListener('click', onClick);
onLoad(); onLoad();
@ -383,7 +383,7 @@ export default class AudioElement extends HTMLElement {
downloadDiv.classList.add('downloading'); downloadDiv.classList.add('downloading');
} else { } else {
download.controller.abort(); download.cancel();
} }
}; };

5
src/components/avatar.ts

@ -38,8 +38,13 @@ export default class AvatarElement extends HTMLElement {
} }
attributeChangedCallback(name: string, oldValue: string, newValue: string) { attributeChangedCallback(name: string, oldValue: string, newValue: string) {
//console.log('avatar changed attribute:', name, oldValue, newValue);
// вызывается при изменении одного из перечисленных выше атрибутов // вызывается при изменении одного из перечисленных выше атрибутов
if(name == 'peer') { if(name == 'peer') {
if(this.peerID == +newValue) {
return;
}
this.peerID = +newValue; this.peerID = +newValue;
this.update(); this.update();
} else if(name == 'peer-title') { } else if(name == 'peer-title') {

14
src/components/emoticonsDropdown.ts

@ -11,7 +11,7 @@ import apiManager from '../lib/mtproto/mtprotoworker';
import LazyLoadQueue from "./lazyLoadQueue"; import LazyLoadQueue from "./lazyLoadQueue";
import { wrapSticker, wrapVideo } from "./wrappers"; import { wrapSticker, wrapVideo } from "./wrappers";
import appDocsManager from "../lib/appManagers/appDocsManager"; import appDocsManager from "../lib/appManagers/appDocsManager";
import ProgressivePreloader from "./preloader_new"; import ProgressivePreloader from "./preloader";
import Config, { touchSupport } from "../lib/config"; import Config, { touchSupport } from "../lib/config";
import { MTDocument } from "../types"; import { MTDocument } from "../types";
import animationIntersector from "./animationIntersector"; import animationIntersector from "./animationIntersector";
@ -377,9 +377,9 @@ class StickersTab implements EmoticonsTab {
}); });
} else { } else {
const image = new Image(); const image = new Image();
renderImageFromUrl(image, thumbURL).then(() => { renderImageFromUrl(image, thumbURL, () => {
li.append(image); li.append(image);
}) });
} }
} else { // as thumb will be used first sticker } else { // as thumb will be used first sticker
wrapSticker({ wrapSticker({
@ -636,7 +636,7 @@ class GifsTab implements EmoticonsTab {
}, {once: true}); }, {once: true});
}; };
((posterURL ? renderImageFromUrl(img, posterURL) : Promise.resolve()) as Promise<any>).then(() => { const afterRender = () => {
if(img) { if(img) {
div.append(img); div.append(img);
div.style.opacity = ''; div.style.opacity = '';
@ -658,7 +658,9 @@ class GifsTab implements EmoticonsTab {
div.append(img); div.append(img);
div.addEventListener('mouseover', onMouseOver, {once: true}); div.addEventListener('mouseover', onMouseOver, {once: true});
}); });
}); };
(posterURL ? renderImageFromUrl(img, posterURL, afterRender) : afterRender());
/* wrapVideo({ /* wrapVideo({
doc, doc,
@ -742,7 +744,7 @@ class EmoticonsDropdown {
if(firstTime) { if(firstTime) {
this.toggleEl.onmouseout = this.element.onmouseout = (e) => { this.toggleEl.onmouseout = this.element.onmouseout = (e) => {
const toElement = (e as any).toElement as Element; const toElement = (e as any).toElement as Element;
if(findUpClassName(toElement, 'emoji-dropdown')) { if(toElement && findUpClassName(toElement, 'emoji-dropdown')) {
return; return;
} }

48
src/components/misc.ts

@ -1,36 +1,40 @@
import Config, { touchSupport, isApple, mediaSizes } from "../lib/config"; import Config, { touchSupport, isApple, mediaSizes } from "../lib/config";
let loadedURLs: {[url: string]: boolean} = {}; export const loadedURLs: {[url: string]: boolean} = {};
let set = (elem: HTMLElement | HTMLImageElement | SVGImageElement | HTMLVideoElement, url: string) => { const set = (elem: HTMLElement | HTMLImageElement | SVGImageElement | HTMLVideoElement, url: string) => {
if(elem instanceof HTMLImageElement || elem instanceof HTMLVideoElement) elem.src = url; if(elem instanceof HTMLImageElement || elem instanceof HTMLVideoElement) elem.src = url;
else if(elem instanceof SVGImageElement) elem.setAttributeNS(null, 'href', url); else if(elem instanceof SVGImageElement) elem.setAttributeNS(null, 'href', url);
else elem.style.backgroundImage = 'url(' + url + ')'; else elem.style.backgroundImage = 'url(' + url + ')';
}; };
export async function renderImageFromUrl(elem: HTMLElement | HTMLImageElement | SVGImageElement | HTMLVideoElement, url: string): Promise<boolean> { // проблема функции в том, что она не подходит для ссылок, пригодна только для blob'ов, потому что обычным ссылкам нужен 'load' каждый раз.
if(loadedURLs[url]) { 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); set(elem, url);
callback && callback();
return true;
} else { } else {
if(elem instanceof HTMLVideoElement) { const isImage = elem instanceof HTMLImageElement;
set(elem, url); const loader = isImage ? elem as HTMLImageElement : new Image();
} else { //const loader = new Image();
await new Promise((resolve, reject) => { loader.src = url;
let loader = new Image(); //let perf = performance.now();
loader.src = url; loader.addEventListener('load', () => {
//let perf = performance.now(); if(!isImage) {
loader.addEventListener('load', () => { set(elem, url);
set(elem, url); }
loadedURLs[url] = true;
//console.log('onload:', url, performance.now() - perf); loadedURLs[url] = true;
resolve(false); //console.log('onload:', url, performance.now() - perf);
}); callback && callback();
loader.addEventListener('error', reject); });
});
if(callback) {
loader.addEventListener('error', callback);
} }
}
return !!loadedURLs[url]; return false;
}
} }
export function putPreloader(elem: Element, returnDiv = false) { export function putPreloader(elem: Element, returnDiv = false) {

59
src/components/preloader.ts

@ -1,13 +1,15 @@
import { isInDOM } from "../lib/utils"; import { isInDOM, cancelEvent } from "../lib/utils";
import { CancellablePromise } from "../lib/polyfill"; import { CancellablePromise } from "../lib/polyfill";
export default class ProgressivePreloader { export default class ProgressivePreloader {
public preloader: HTMLDivElement = null; public preloader: HTMLDivElement;
private circle: SVGCircleElement = null; private circle: SVGCircleElement;
private promise: CancellablePromise<any> = null;
private tempID = 0; private tempID = 0;
private detached = true; private detached = true;
private promise: CancellablePromise<any> = null;
constructor(elem?: Element, private cancelable = true) { constructor(elem?: Element, private cancelable = true) {
this.preloader = document.createElement('div'); this.preloader = document.createElement('div');
this.preloader.classList.add('preloader-container'); this.preloader.classList.add('preloader-container');
@ -36,7 +38,9 @@ export default class ProgressivePreloader {
} }
if(this.cancelable) { if(this.cancelable) {
this.preloader.addEventListener('click', () => { this.preloader.addEventListener('click', (e) => {
cancelEvent(e);
if(this.promise && this.promise.cancel) { if(this.promise && this.promise.cancel) {
this.promise.cancel(); this.promise.cancel();
this.detach(); this.detach();
@ -44,13 +48,14 @@ export default class ProgressivePreloader {
}); });
} }
} }
public attach(elem: Element, reset = true, promise?: CancellablePromise<any>, append = true) { public attach(elem: Element, reset = true, promise?: CancellablePromise<any>, append = true) {
if(promise) { if(promise) {
this.promise = promise; this.promise = promise;
let tempID = --this.tempID; const tempID = --this.tempID;
let onEnd = () => {
const onEnd = () => {
promise.notify = null; promise.notify = null;
if(tempID == this.tempID) { if(tempID == this.tempID) {
@ -58,37 +63,41 @@ export default class ProgressivePreloader {
this.promise = promise = null; this.promise = promise = null;
} }
}; };
//promise.catch(onEnd); //promise.catch(onEnd);
promise.finally(onEnd); promise.finally(onEnd);
promise.notify = (details: {done: number, total: number}) => { if(promise.addNotifyListener) {
/* if(details.done >= details.total) { promise.addNotifyListener((details: {done: number, total: number}) => {
onEnd(); /* if(details.done >= details.total) {
} */ onEnd();
} */
if(tempID != this.tempID) return;
if(tempID != this.tempID) return;
//console.log('preloader download', promise, details);
let percents = details.done / details.total * 100; //console.log('preloader download', promise, details);
this.setProgress(percents); const percents = details.done / details.total * 100;
}; this.setProgress(percents);
});
}
} }
if(this.cancelable && reset) {
this.setProgress(0);
}
this.detached = false; this.detached = false;
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
if(this.detached) return; if(this.detached) return;
this.detached = false; this.detached = false;
elem[append ? 'append' : 'prepend'](this.preloader); elem[append ? 'append' : 'prepend'](this.preloader);
if(this.cancelable && reset) {
this.setProgress(0);
}
}); });
} }
public detach() { public detach() {
this.detached = true; this.detached = true;
if(this.preloader.parentElement) { if(this.preloader.parentElement) {
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
if(!this.detached) return; if(!this.detached) return;
@ -112,7 +121,7 @@ export default class ProgressivePreloader {
} }
try { try {
let totalLength = this.circle.getTotalLength(); const totalLength = this.circle.getTotalLength();
//console.log('setProgress', (percents / 100 * totalLength)); //console.log('setProgress', (percents / 100 * totalLength));
this.circle.style.strokeDasharray = '' + Math.max(5, percents / 100 * totalLength) + ', 200'; this.circle.style.strokeDasharray = '' + Math.max(5, percents / 100 * totalLength) + ', 200';
} catch(err) {} } catch(err) {}

123
src/components/preloader_new.ts

@ -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 = `
<div class="you-spin-me-round">
<svg xmlns="http://www.w3.org/2000/svg" class="preloader-circular" viewBox="25 25 50 50">
<circle class="preloader-path-new" cx="50" cy="50" r="23" fill="none" stroke-miterlimit="10"/>
</svg>
</div>`;
if(cancelable) {
this.preloader.innerHTML += `
<svg xmlns="http://www.w3.org/2000/svg" class="preloader-close" viewBox="0 0 20 20">
<line x1="0" y1="20" x2="20" y2="0" stroke-width="2" stroke-linecap="round"></line>
<line x1="0" y1="0" x2="20" y2="20" stroke-width="2" stroke-linecap="round"></line>
</svg>`;
} 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) {}
}
}

69
src/components/wrappers.ts

@ -3,11 +3,10 @@ import LottieLoader from '../lib/lottieLoader';
import appDocsManager from "../lib/appManagers/appDocsManager"; import appDocsManager from "../lib/appManagers/appDocsManager";
import { formatBytes, getEmojiToneIndex } from "../lib/utils"; import { formatBytes, getEmojiToneIndex } from "../lib/utils";
import ProgressivePreloader from './preloader'; import ProgressivePreloader from './preloader';
import ProgressivePreloaderNew from './preloader_new';
import LazyLoadQueue from './lazyLoadQueue'; import LazyLoadQueue from './lazyLoadQueue';
import VideoPlayer from '../lib/mediaPlayer'; import VideoPlayer from '../lib/mediaPlayer';
import { RichTextProcessor } from '../lib/richtextprocessor'; import { RichTextProcessor } from '../lib/richtextprocessor';
import { renderImageFromUrl } from './misc'; import { renderImageFromUrl, loadedURLs } from './misc';
import appMessagesManager from '../lib/appManagers/appMessagesManager'; import appMessagesManager from '../lib/appManagers/appMessagesManager';
import { Layouter, RectPart } from './groupedLayout'; import { Layouter, RectPart } from './groupedLayout';
import PollElement from './poll'; import PollElement from './poll';
@ -15,7 +14,7 @@ import { mediaSizes, isSafari } from '../lib/config';
import { MTDocument, MTPhotoSize } from '../types'; import { MTDocument, MTPhotoSize } from '../types';
import animationIntersector from './animationIntersector'; import animationIntersector from './animationIntersector';
import AudioElement from './audio'; import AudioElement from './audio';
import { Download } from '../lib/appManagers/appDownloadManager'; import appDownloadManager, { Download } from '../lib/appManagers/appDownloadManager';
import { webpWorkerController } from '../lib/webp/webpWorkerController'; import { webpWorkerController } from '../lib/webp/webpWorkerController';
export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTail, isOut, middleware, lazyLoadQueue, noInfo, group}: { 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) { if(!uploading) {
let downloadDiv = docDiv.querySelector('.document-download') as HTMLDivElement; let downloadDiv = docDiv.querySelector('.document-download') as HTMLDivElement;
let preloader: ProgressivePreloaderNew; let preloader: ProgressivePreloader;
let download: Download; let download: Download;
docDiv.addEventListener('click', () => { docDiv.addEventListener('click', () => {
@ -238,13 +237,13 @@ export function wrapDocument(doc: MTDocument, withTime = false, uploading = fals
} }
if(!preloader) { if(!preloader) {
preloader = new ProgressivePreloaderNew(null, true); preloader = new ProgressivePreloader(null, true);
} }
download = appDocsManager.saveDocFile(doc); download = appDocsManager.saveDocFile(doc);
preloader.attach(downloadDiv, true, appDocsManager.getInputFileName(doc)); preloader.attach(downloadDiv, true, download);
download.promise.then(() => { download.then(() => {
downloadDiv.remove(); downloadDiv.remove();
}).catch(err => { }).catch(err => {
if(err.name === 'AbortError') { if(err.name === 'AbortError') {
@ -256,7 +255,7 @@ export function wrapDocument(doc: MTDocument, withTime = false, uploading = fals
downloadDiv.classList.add('downloading'); downloadDiv.classList.add('downloading');
} else { } 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) { 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; let image: HTMLImageElement;
if(withTail) { if(withTail) {
@ -351,36 +350,39 @@ export function wrapPhoto(photoID: any, message: any, container: HTMLDivElement,
//console.log('wrapPhoto downloaded:', photo, photo.downloaded, container); //console.log('wrapPhoto downloaded:', photo, photo.downloaded, container);
// так нельзя делать, потому что может быть загружен неправильный размер картинки const cacheContext = appPhotosManager.getCacheContext(photo);
/* if(photo.downloaded && photo.url) {
renderImageFromUrl(image, photo.url);
return;
} */
let preloader: ProgressivePreloader; let preloader: ProgressivePreloader;
if(message.media.preloader) { // means upload if(message.media.preloader) { // means upload
message.media.preloader.attach(container); message.media.preloader.attach(container);
} else if(!photo.downloaded) { } else if(!cacheContext.downloaded) {
preloader = new ProgressivePreloader(container, false); preloader = new ProgressivePreloader(container, false);
} }
let load = () => { const load = () => {
let promise = appPhotosManager.preloadPhoto(photoID, size); const promise = appPhotosManager.preloadPhoto(photoID, size);
if(preloader) { if(preloader) {
preloader.attach(container, true, promise); 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(() => { return promise.then(() => {
if(middleware && !middleware()) return; 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 cacheContext.downloaded ? load() : lazyLoadQueue.push({div: container, load: load, wasSeen: true});
return photo.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}: { 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(); img = new Image();
if((!isSafari || doc.stickerThumbConverted)/* && false */) { if((!isSafari || doc.stickerThumbConverted)/* && false */) {
renderImageFromUrl(img, appPhotosManager.getPreviewURLFromThumb(thumb, true)).then(afterRender); renderImageFromUrl(img, appPhotosManager.getPreviewURLFromThumb(thumb, true), afterRender);
} else { } else {
webpWorkerController.convert(doc.id, thumb.bytes).then(bytes => { webpWorkerController.convert(doc.id, thumb.bytes).then(bytes => {
if(middleware && !middleware()) return; if(middleware && !middleware()) return;
@ -448,7 +450,7 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o
doc.stickerThumbConverted = true; doc.stickerThumbConverted = true;
if(!div.childElementCount) { 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 = () => { const load = () => {
if(div.childElementCount || (middleware && !middleware())) return; if(div.childElementCount || (middleware && !middleware())) return;
const promise = renderImageFromUrl(img, appDocsManager.getFileURL(doc, false, thumb)); renderImageFromUrl(img, appDocsManager.getFileURL(doc, false, thumb), afterRender);
//if(!downloaded) {
promise.then(afterRender);
//}
}; };
/* let downloaded = appDocsManager.hasDownloadedThumb(doc.id, thumb.type); /* 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 load = () => {
let img = new Image(); let img = new Image();
return renderImageFromUrl(img, appDocsManager.getFileURL(doc, false, thumb)).then(() => { renderImageFromUrl(img, appDocsManager.getFileURL(doc, false, thumb), () => {
if(middleware && !middleware()) return; if(middleware && !middleware()) return;
div.append(img); div.append(img);
}); });
return Promise.resolve();
}; };
return lazyLoadQueue ? (lazyLoadQueue.push({div, load}), Promise.resolve()) : load(); 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); //console.time('download sticker' + doc.id);
//appDocsManager.downloadDocNew(doc.id).promise.then(res => res.json()).then(async(json) => { //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.timeEnd('download sticker' + doc.id);
//console.log('loaded sticker:', doc, div); //console.log('loaded sticker:', doc, div);
if(middleware && !middleware()) return; if(middleware && !middleware()) return;
//await new Promise((resolve) => setTimeout(resolve, 5e3));
let animation = await LottieLoader.loadAnimationWorker/* loadAnimation */({ let animation = await LottieLoader.loadAnimationWorker/* loadAnimation */({
container: div, container: div,
loop: loop && !emoji, 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) { if(div.firstElementChild && div.firstElementChild != img) {
div.firstElementChild.remove(); div.firstElementChild.remove();
} }

2
src/helpers/userAgent.ts

@ -12,4 +12,4 @@ export const isAndroid = navigator.userAgent.toLowerCase().indexOf('android') !=
*/ */
const ctx = typeof(window) !== 'undefined' ? window : self; 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')))); export const isSafari = !!('safari' in ctx) || !!(userAgent && (/\b(iPad|iPhone|iPod)\b/.test(userAgent) || (!!userAgent.match('Safari') && !userAgent.match('Chrome'))))/* || true */;

27
src/lib/appManagers/appDocsManager.ts

@ -14,16 +14,21 @@ class AppDocsManager {
public saveDoc(apiDoc: MTDocument, context?: any) { public saveDoc(apiDoc: MTDocument, context?: any) {
//console.log('saveDoc', apiDoc, this.docs[apiDoc.id]); //console.log('saveDoc', apiDoc, this.docs[apiDoc.id]);
if(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(apiDoc.thumbs) {
if(!d.thumbs) d.thumbs = 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]); 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; //return context ? Object.assign(d, context) : d;
} }
@ -33,10 +38,6 @@ class AppDocsManager {
this.docs[apiDoc.id] = apiDoc; this.docs[apiDoc.id] = apiDoc;
if(apiDoc.thumb && apiDoc.thumb._ == 'photoSizeEmpty') {
delete apiDoc.thumb;
}
apiDoc.attributes.forEach((attribute: any) => { apiDoc.attributes.forEach((attribute: any) => {
switch(attribute._) { switch(attribute._) {
case 'documentAttributeFilename': case 'documentAttributeFilename':
@ -216,7 +217,7 @@ class AppDocsManager {
const thumb = doc.thumbs.find(t => !t.bytes); const thumb = doc.thumbs.find(t => !t.bytes);
if(thumb) { if(thumb) {
const url = appDocsManager.getFileURL(doc, false, thumb); const url = this.getFileURL(doc, false, thumb);
return url; return url;
} }
} }
@ -304,15 +305,15 @@ class AppDocsManager {
return download; 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(() => { originalPromise.then(() => {
doc.downloaded = true; doc.downloaded = true;
}); });
if(doc.type == 'voice' && !opusDecodeController.isPlaySupported()) { if(doc.type == 'voice' && !opusDecodeController.isPlaySupported()) {
download.promise = originalPromise.then(async(blob) => { download = originalPromise.then(async(blob) => {
let reader = new FileReader(); let reader = new FileReader();
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
@ -344,7 +345,7 @@ class AppDocsManager {
const url = this.getFileURL(doc, true); const url = this.getFileURL(doc, true);
const fileName = this.getInputFileName(doc); const fileName = this.getInputFileName(doc);
return appDownloadManager.downloadToDisc(fileName, url); return appDownloadManager.downloadToDisc(fileName, url, doc.file_name);
} }
} }

45
src/lib/appManagers/appDownloadManager.ts

@ -1,12 +1,16 @@
import { $rootScope } from "../utils"; import { $rootScope } from "../utils";
import apiManager from "../mtproto/mtprotoworker"; import apiManager from "../mtproto/mtprotoworker";
import { deferredPromise, CancellablePromise } from "../polyfill";
export type ResponseMethodBlob = 'blob'; export type ResponseMethodBlob = 'blob';
export type ResponseMethodJson = 'json'; export type ResponseMethodJson = 'json';
export type ResponseMethod = ResponseMethodBlob | ResponseMethodJson; export type ResponseMethod = ResponseMethodBlob | ResponseMethodJson;
export type DownloadBlob = {promise: Promise<Blob>, controller: AbortController}; /* export type DownloadBlob = {promise: Promise<Blob>, controller: AbortController};
export type DownloadJson = {promise: Promise<any>, controller: AbortController}; export type DownloadJson = {promise: Promise<any>, controller: AbortController}; */
export type DownloadBlob = CancellablePromise<Blob>;
export type DownloadJson = CancellablePromise<any>;
//export type Download = DownloadBlob/* | DownloadJson */;
export type Download = DownloadBlob/* | DownloadJson */; export type Download = DownloadBlob/* | DownloadJson */;
export type Progress = {done: number, fileName: string, total: number, offset: number}; export type Progress = {done: number, fileName: string, total: number, offset: number};
@ -26,17 +30,25 @@ export class AppDownloadManager {
if(callbacks) { if(callbacks) {
callbacks.forEach(callback => callback(details)); 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(url: string, fileName: string, responseMethod?: ResponseMethodBlob): DownloadBlob;
public download(fileName: string, url: string, responseMethod?: ResponseMethodJson): DownloadJson; public download(url: string, fileName: string, responseMethod?: ResponseMethodJson): DownloadJson;
public download(fileName: string, url: string, responseMethod: ResponseMethod = 'blob'): Download { public download(url: string, fileName: string, responseMethod: ResponseMethod = 'blob'): Download {
if(this.downloads.hasOwnProperty(fileName)) return this.downloads[fileName]; if(this.downloads.hasOwnProperty(fileName)) return this.downloads[fileName];
const deferred = deferredPromise<Blob>();
const controller = new AbortController(); const controller = new AbortController();
const promise = fetch(url, {signal: controller.signal}) const promise = fetch(url, {signal: controller.signal})
.then(res => res[responseMethod]()) .then(res => res[responseMethod]())
.then(res => deferred.resolve(res))
.catch(err => { // Только потому что event.request.signal не работает в SW, либо я кривой? .catch(err => { // Только потому что event.request.signal не работает в SW, либо я кривой?
if(err.name === 'AbortError') { if(err.name === 'AbortError') {
//console.log('Fetch aborted'); //console.log('Fetch aborted');
@ -48,6 +60,7 @@ export class AppDownloadManager {
//console.error('Uh oh, an error!', err); //console.error('Uh oh, an error!', err);
} }
deferred.reject(err);
throw err; throw err;
}); });
@ -57,7 +70,13 @@ export class AppDownloadManager {
delete this.progressCallbacks[fileName]; 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) { 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'); const a = document.createElement('a');
a.href = url; a.href = url;
a.download = fileName;
a.target = '_blank';
a.style.position = 'absolute'; a.style.position = 'absolute';
a.style.top = '1px'; a.style.top = '1px';
a.style.left = '1px'; a.style.left = '1px';
@ -108,11 +129,11 @@ export class AppDownloadManager {
return this.download(fileName, url); return this.download(fileName, url);
} */ } */
public downloadToDisc(fileName: string, url: string) { public downloadToDisc(fileName: string, url: string, discFileName: string) {
const download = this.download(fileName, url); const download = this.download(url, fileName);
download.promise.then(blob => { download/* .promise */.then(blob => {
const objectURL = URL.createObjectURL(blob); const objectURL = URL.createObjectURL(blob);
this.createDownloadAnchor(objectURL, () => { this.createDownloadAnchor(objectURL, discFileName, () => {
URL.revokeObjectURL(objectURL); URL.revokeObjectURL(objectURL);
}); });
}); });

31
src/lib/appManagers/appImManager.ts

@ -553,7 +553,7 @@ export class AppImManager {
private closeBtn = this.topbar.querySelector('.sidebar-close-button') as HTMLButtonElement; private closeBtn = this.topbar.querySelector('.sidebar-close-button') as HTMLButtonElement;
constructor() { 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.chatInputC = new ChatInput();
this.preloader = new ProgressivePreloader(null, false); this.preloader = new ProgressivePreloader(null, false);
this.selectTab(0); this.selectTab(0);
@ -1624,6 +1624,7 @@ export class AppImManager {
this.peerChanged = true; this.peerChanged = true;
this.avatarEl.setAttribute('peer', '' + this.peerID); this.avatarEl.setAttribute('peer', '' + this.peerID);
this.avatarEl.update();
const isAnyGroup = appPeersManager.isAnyGroup(peerID); const isAnyGroup = appPeersManager.isAnyGroup(peerID);
const isChannel = appPeersManager.isChannel(peerID); const isChannel = appPeersManager.isChannel(peerID);
@ -1822,8 +1823,7 @@ export class AppImManager {
resolve(); resolve();
// lol // lol
el.removeEventListener('canplay', onLoad); el.removeEventListener(el instanceof HTMLVideoElement ? 'canplay' : 'load', onLoad);
el.removeEventListener('load', onLoad);
}; };
if(el instanceof HTMLVideoElement) { if(el instanceof HTMLVideoElement) {
@ -2286,20 +2286,26 @@ export class AppImManager {
bubble.classList.add('hide-name', 'photo'); bubble.classList.add('hide-name', 'photo');
const tailSupported = !isAndroid; const tailSupported = !isAndroid;
if(tailSupported) bubble.classList.add('with-media-tail'); if(tailSupported) bubble.classList.add('with-media-tail');
if(message.grouped_id) { if(message.grouped_id) {
bubble.classList.add('is-album'); bubble.classList.add('is-album');
wrapAlbum({ let storage = appMessagesManager.groupedMessagesStorage[message.grouped_id];
groupID: message.grouped_id, if(Object.keys(storage).length != 1) {
attachmentDiv, wrapAlbum({
middleware: this.getMiddleware(), groupID: message.grouped_id,
isOut: our, attachmentDiv,
lazyLoadQueue: this.lazyLoadQueue middleware: this.getMiddleware(),
}); isOut: our,
} else { lazyLoadQueue: this.lazyLoadQueue
wrapPhoto(photo.id, message, attachmentDiv, undefined, undefined, tailSupported, isOut, this.lazyLoadQueue, this.getMiddleware()); });
break;
}
} }
wrapPhoto(photo.id, message, attachmentDiv, undefined, undefined, tailSupported, isOut, this.lazyLoadQueue, this.getMiddleware());
break; break;
} }
@ -2645,6 +2651,7 @@ export class AppImManager {
} }
avatarElem.setAttribute('peer', '' + ((message.fwd_from && this.peerID == this.myID ? message.fwdFromID : message.fromID) || 0)); avatarElem.setAttribute('peer', '' + ((message.fwd_from && this.peerID == this.myID ? message.fwdFromID : message.fromID) || 0));
avatarElem.update();
//this.log('exec loadDialogPhoto', message); //this.log('exec loadDialogPhoto', message);

4
src/lib/appManagers/appMediaViewer.ts

@ -132,7 +132,7 @@ export class AppMediaViewer {
const download = (e: MouseEvent) => { const download = (e: MouseEvent) => {
let message = appMessagesManager.getMessage(this.currentMessageID); let message = appMessagesManager.getMessage(this.currentMessageID);
if(message.media.photo) { if(message.media.photo) {
appPhotosManager.downloadPhoto(message.media.photo.id); appPhotosManager.savePhotoFile(message.media.photo.id);
} else { } else {
let document: any = null; let document: any = null;
@ -895,7 +895,7 @@ export class AppMediaViewer {
//this.log('will renderImageFromUrl:', image, div, target); //this.log('will renderImageFromUrl:', image, div, target);
renderImageFromUrl(image, url).then(() => { renderImageFromUrl(image, url, () => {
div.append(image); div.append(image);
}); });
} }

101
src/lib/appManagers/appPhotosManager.ts

@ -1,10 +1,9 @@
import appUsersManager from "./appUsersManager";
import { calcImageInBox, isObject, getFileURL } from "../utils"; import { calcImageInBox, isObject, getFileURL } from "../utils";
import { bytesFromHex, getFileNameByLocation } from "../bin_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 { 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 = { export type MTPhoto = {
_: 'photo' | 'photoEmpty' | string, _: 'photo' | 'photoEmpty' | string,
@ -98,7 +97,7 @@ export class AppPhotosManager {
return bestPhotoSize; return bestPhotoSize;
} }
public getUserPhotos(userID: number, maxID: number, limit: number) { /* public getUserPhotos(userID: number, maxID: number, limit: number) {
var inputUser = appUsersManager.getUserInput(userID); var inputUser = appUsersManager.getUserInput(userID);
return apiManager.invokeApi('photos.getUserPhotos', { return apiManager.invokeApi('photos.getUserPhotos', {
user_id: inputUser, user_id: inputUser,
@ -120,7 +119,7 @@ export class AppPhotosManager {
photos: photoIDs photos: photoIDs
}; };
}); });
} } */
public getPreviewURLFromBytes(bytes: Uint8Array | number[], isSticker = false) { public getPreviewURLFromBytes(bytes: Uint8Array | number[], isSticker = false) {
let arr: Uint8Array; let arr: Uint8Array;
@ -131,17 +130,15 @@ export class AppPhotosManager {
} else { } else {
arr = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes); arr = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
} }
//console.log('getPreviewURLFromBytes', bytes, arr, div, isSticker);
/* let reader = new FileReader(); let mimeType: string;
reader.onloadend = () => { if(isSticker) {
let src = reader.result; mimeType = isSafari ? 'image/png' : 'image/webp';
}; } else {
reader.readAsDataURL(blob); */ mimeType = 'image/jpeg';
}
let blob = new Blob([arr], {type: "image/jpeg"});
const blob = new Blob([arr], {type: mimeType});
return URL.createObjectURL(blob); return URL.createObjectURL(blob);
} }
@ -215,32 +212,18 @@ export class AppPhotosManager {
return photoSize; return photoSize;
} }
public preloadPhoto(photoID: any, photoSize?: MTPhotoSize): Promise<Blob | void> {
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 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') { if(!photoSize || photoSize._ == 'photoSizeEmpty') {
//console.error('no photoSize by photo:', photo); //console.error('no photoSize by photo:', photo);
return Promise.reject('no photoSize'); throw new Error('photoSizeEmpty!');
} }
// maybe it's a thumb // maybe it's a thumb
let isPhoto = photoSize.size && photo.access_hash && photo.file_reference; const isPhoto = photoSize.size && photo.access_hash && photo.file_reference;
let location: inputPhotoFileLocation | inputDocumentFileLocation | FileLocation = isPhoto ? { const location: inputPhotoFileLocation | inputDocumentFileLocation | FileLocation = isPhoto ? {
_: isDocument ? 'inputDocumentFileLocation' : 'inputPhotoFileLocation', _: isDocument ? 'inputDocumentFileLocation' : 'inputPhotoFileLocation',
id: photo.id, id: photo.id,
access_hash: photo.access_hash, access_hash: photo.access_hash,
@ -248,30 +231,54 @@ export class AppPhotosManager {
thumb_size: photoSize.type thumb_size: photoSize.type
} : photoSize.location; } : photoSize.location;
const url = getFileURL('photo', {dcID: photo.dc_id, location, size: isPhoto ? photoSize.size : undefined}); return {url: getFileURL('photo', {dcID: photo.dc_id, location, size: isPhoto ? photoSize.size : undefined}), location};
let promise: Promise<Blob>; }
if(isPhoto/* && photoSize.size >= 1e6 */) {
promise = fetch(url).then(res => res.blob()); public preloadPhoto(photoID: any, photoSize?: MTPhotoSize): CancellablePromise<Blob> {
} else { const photo = this.getPhoto(photoID);
//console.log('Photos downloadSmallFile exec', photo, location);
promise = fetch(url).then(res => res.blob()); 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) { if(!cacheContext.downloaded || cacheContext.downloaded < blob.size) {
cacheContext.downloaded = blob.size; cacheContext.downloaded = blob.size;
//cacheContext.url = URL.createObjectURL(blob); cacheContext.url = URL.createObjectURL(blob);
cacheContext.url = url;
//console.log('wrote photo:', photo, photoSize, cacheContext, 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) { public getDocumentCachedThumb(docID: string) {
return this.documentThumbsCache[docID]; return this.documentThumbsCache[docID] ?? (this.documentThumbsCache[docID] = {downloaded: 0, url: ''});
} }
public getPhoto(photoID: any): MTPhoto { 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 photo = this.photos[photoID];
const fullWidth = this.windowW; const fullWidth = this.windowW;
const fullHeight = this.windowH; 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 url = getFileURL('download', {dcID: photo.dc_id, location, size: fullPhotoSize.size, fileName: 'photo' + photo.id + '.jpg'});
const fileName = getFileNameByLocation(location); const fileName = getFileNameByLocation(location);
appDownloadManager.downloadToDisc(fileName, url); appDownloadManager.downloadToDisc(fileName, url, 'photo' + photo.id + '.jpg');
} }
} }

12
src/lib/appManagers/appSidebarRight.ts

@ -797,14 +797,12 @@ export class AppSidebarRight extends SidebarSlider {
const url = (photo && photo.url) || appPhotosManager.getDocumentCachedThumb(media.id).url; const url = (photo && photo.url) || appPhotosManager.getDocumentCachedThumb(media.id).url;
if(url) { if(url) {
//if(needBlur) return; //if(needBlur) return;
const p = renderImageFromUrl(img, url);
if(needBlur) { const needBlurCallback = needBlur ? () => {
p.then(() => { //void img.offsetLeft; // reflow
//void img.offsetLeft; // reflow img.style.opacity = '';
img.style.opacity = ''; } : undefined;
}); renderImageFromUrl(img, url, needBlurCallback);
}
} }
}); });

35
src/lib/polyfill.ts

@ -8,13 +8,39 @@ export interface CancellablePromise<T> extends Promise<T> {
resolve?: (...args: any[]) => void, resolve?: (...args: any[]) => void,
reject?: (...args: any[]) => void, reject?: (...args: any[]) => void,
cancel?: () => void, cancel?: () => void,
notify?: (...args: any[]) => void, notify?: (...args: any[]) => void,
notifyAll?: (...args: any[]) => void,
lastNotify?: any,
listeners?: Array<(...args: any[]) => void>,
addNotifyListener?: (callback: (...args: any[]) => void) => void,
isFulfilled?: boolean, isFulfilled?: boolean,
isRejected?: boolean isRejected?: boolean
} }
export function deferredPromise<T>() { export function deferredPromise<T>() {
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<T> = new Promise<T>((resolve, reject) => { let deferred: CancellablePromise<T> = new Promise<T>((resolve, reject) => {
deferredHelper.resolve = (value: T) => { deferredHelper.resolve = (value: T) => {
if(deferred.isFulfilled) return; if(deferred.isFulfilled) return;
@ -30,6 +56,13 @@ export function deferredPromise<T>() {
reject(...args); reject(...args);
}; };
}); });
deferred.finally(() => {
deferred.notify = null;
deferred.listeners.length = 0;
deferred.lastNotify = null;
});
Object.assign(deferred, deferredHelper); Object.assign(deferred, deferredHelper);
return deferred; return deferred;

4
src/lib/webp/webpWorkerController.ts

@ -40,6 +40,10 @@ export class WebpWorkerController {
} }
convert(fileName: string, bytes: Uint8Array) { convert(fileName: string, bytes: Uint8Array) {
if(this.convertPromises.hasOwnProperty(fileName)) {
return this.convertPromises[fileName];
}
const convertPromise = deferredPromise<Uint8Array>(); const convertPromise = deferredPromise<Uint8Array>();
fileName = 'main-' + fileName; fileName = 'main-' + fileName;

5
src/scss/partials/_scrollable.scss

@ -114,9 +114,14 @@ div.scrollable::-webkit-scrollbar-thumb {
// BROWSER SCROLL // BROWSER SCROLL
div.scrollable-y::-webkit-scrollbar { div.scrollable-y::-webkit-scrollbar {
width: .375rem; width: .375rem;
opacity: 0; // for safari
//height: 200px; //height: 200px;
} }
div.scrollable:hover::-webkit-scrollbar {
opacity: 1; // for safari
}
/* div.scrollable-y::-webkit-scrollbar-thumb { /* div.scrollable-y::-webkit-scrollbar-thumb {
border: 2px solid rgba(0, 0, 0, 0); border: 2px solid rgba(0, 0, 0, 0);
background-clip: padding-box; background-clip: padding-box;

5
src/types.d.ts vendored

@ -12,7 +12,6 @@ export type MTDocument = {
dc_id: number, dc_id: number,
attributes: any[], attributes: any[],
thumb?: MTPhotoSize,
type?: 'gif' | 'sticker' | 'audio' | 'voice' | 'video' | 'round' | 'photo', type?: 'gif' | 'sticker' | 'audio' | 'voice' | 'video' | 'round' | 'photo',
h?: number, h?: number,
w?: number, w?: number,
@ -42,7 +41,9 @@ export type MTPhotoSize = {
size?: number, size?: number,
type?: string, // i, m, x, y, w by asc type?: string, // i, m, x, y, w by asc
location?: FileLocation, location?: FileLocation,
bytes?: Uint8Array // if type == 'i' bytes?: Uint8Array, // if type == 'i',
url?: string
}; };
export type InvokeApiOptions = Partial<{ export type InvokeApiOptions = Partial<{

Loading…
Cancel
Save