Browse Source

Fix stickers border

Stickers are now uncompress in SW
master
morethanwords 4 years ago
parent
commit
99203abe01
  1. 47
      src/components/emoticonsDropdown.ts
  2. 36
      src/components/misc.ts
  3. 83
      src/components/wrappers.ts
  4. 62
      src/lib/appManagers/appDocsManager.ts
  5. 15
      src/lib/appManagers/appDownloadManager.ts
  6. 4
      src/lib/appManagers/appStickersManager.ts
  7. 11
      src/lib/bin_utils.ts
  8. 18
      src/lib/filemanager.ts
  9. 66
      src/lib/mtproto/apiFileManager.ts
  10. 2
      src/lib/mtproto/mtproto.service.ts
  11. 5
      tsconfig.json

47
src/components/emoticonsDropdown.ts

@ -355,34 +355,27 @@ class StickersTab implements EmoticonsTab { @@ -355,34 +355,27 @@ class StickersTab implements EmoticonsTab {
//console.log('got stickerSet', stickerSet, li);
if(stickerSet.set.thumb) {
appStickersManager.getStickerSetThumb(stickerSet.set).then((blob) => {
//console.log('setting thumb', stickerSet, blob);
if(stickerSet.set.pFlags.animated) { // means animated
const reader = new FileReader();
reader.addEventListener('loadend', async(e) => {
// @ts-ignore
const text = e.srcElement.result;
let json = await apiManager.gzipUncompress<string>(text, true);
let animation = await lottieLoader.loadAnimationWorker({
container: li,
loop: true,
autoplay: false,
animationData: JSON.parse(json),
width: 32,
height: 32
}, EMOTICONSSTICKERGROUP);
});
reader.readAsArrayBuffer(blob);
} else {
let image = new Image();
renderImageFromUrl(image, URL.createObjectURL(blob));
const thumbURL = appStickersManager.getStickerSetThumbURL(stickerSet.set);
if(stickerSet.set.pFlags.animated) {
fetch(thumbURL)
.then(res => res.json())
.then(json => {
lottieLoader.loadAnimationWorker({
container: li,
loop: true,
autoplay: false,
animationData: json,
width: 32,
height: 32
}, EMOTICONSSTICKERGROUP);
});
} else {
const image = new Image();
renderImageFromUrl(image, thumbURL).then(() => {
li.append(image);
}
});
})
}
} else { // as thumb will be used first sticker
wrapSticker({
doc: stickerSet.documents[0],

36
src/components/misc.ts

@ -165,29 +165,29 @@ let set = (elem: HTMLElement | HTMLImageElement | SVGImageElement | HTMLSourceEl @@ -165,29 +165,29 @@ let set = (elem: HTMLElement | HTMLImageElement | SVGImageElement | HTMLSourceEl
else elem.style.backgroundImage = 'url(' + url + ')';
};
export function renderImageFromUrl(elem: HTMLElement | HTMLImageElement | SVGImageElement | HTMLSourceElement, url: string): Promise<boolean> {
export async function renderImageFromUrl(elem: HTMLElement | HTMLImageElement | SVGImageElement | HTMLSourceElement, url: string): Promise<boolean> {
if(loadedURLs[url]) {
set(elem, url);
return Promise.resolve(true);
}
if(elem instanceof HTMLSourceElement) {
elem.src = url;
return Promise.resolve(false);
} else {
return new Promise((resolve, reject) => {
let loader = new Image();
loader.src = url;
//let perf = performance.now();
loader.addEventListener('load', () => {
set(elem, url);
loadedURLs[url] = true;
//console.log('onload:', url, performance.now() - perf);
resolve(false);
if(elem instanceof HTMLSourceElement) {
elem.src = url;
} else {
await new Promise((resolve, reject) => {
let loader = new Image();
loader.src = url;
//let perf = performance.now();
loader.addEventListener('load', () => {
set(elem, url);
loadedURLs[url] = true;
//console.log('onload:', url, performance.now() - perf);
resolve(false);
});
loader.addEventListener('error', reject);
});
loader.addEventListener('error', reject);
});
}
}
return !!loadedURLs[url];
}
export function putPreloader(elem: Element, returnDiv = false) {

83
src/components/wrappers.ts

@ -1,6 +1,4 @@ @@ -1,6 +1,4 @@
import appPhotosManager from '../lib/appManagers/appPhotosManager';
//import CryptoWorker from '../lib/crypto/cryptoworker';
import apiManager from '../lib/mtproto/mtprotoworker';
import LottieLoader from '../lib/lottieLoader';
import appDocsManager from "../lib/appManagers/appDocsManager";
import { formatBytes, getEmojiToneIndex } from "../lib/utils";
@ -415,14 +413,19 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o @@ -415,14 +413,19 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o
let thumb = doc.thumbs[0];
//console.log('wrap sticker', thumb, div);
let img: HTMLImageElement;
const afterRender = () => {
if(!div.childElementCount) {
div.append(img);
}
};
if(thumb.bytes) {
let img = new Image();
img = new Image();
if((!isSafari || doc.stickerThumbConverted)/* && false */) {
renderImageFromUrl(img, appPhotosManager.getPreviewURLFromThumb(thumb, true));
div.append(img);
renderImageFromUrl(img, appPhotosManager.getPreviewURLFromThumb(thumb, true)).then(afterRender);
} else {
webpWorkerController.convert(doc.id, thumb.bytes).then(bytes => {
if(middleware && !middleware()) return;
@ -431,11 +434,7 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o @@ -431,11 +434,7 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o
doc.stickerThumbConverted = true;
if(!div.childElementCount) {
renderImageFromUrl(img, appPhotosManager.getPreviewURLFromThumb(thumb, true)).then(() => {
if(!div.childElementCount) {
div.append(img);
}
});
renderImageFromUrl(img, appPhotosManager.getPreviewURLFromThumb(thumb, true)).then(afterRender);
}
});
}
@ -444,26 +443,24 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o @@ -444,26 +443,24 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o
return Promise.resolve();
}
} else if(!onlyThumb && stickerType == 2 && withThumb && toneIndex <= 0) {
let img = new Image();
let load = () => appDocsManager.downloadDocThumb(doc, thumb.type).then(url => {
img = new Image();
const load = () => appDocsManager.downloadDocThumb(doc, thumb.type).then(url => {
if(div.childElementCount || (middleware && !middleware())) return;
let promise = renderImageFromUrl(img, url);
const promise = renderImageFromUrl(img, url);
if(!downloaded) {
promise.then(() => {
if(!div.childElementCount) {
div.append(img);
}
});
}
//if(!downloaded) {
promise.then(afterRender);
//}
});
let downloaded = appDocsManager.hasDownloadedThumb(doc.id, thumb.type);
/* let downloaded = appDocsManager.hasDownloadedThumb(doc.id, thumb.type);
if(downloaded) {
div.append(img);
}
} */
lazyLoadQueue && !downloaded ? lazyLoadQueue.push({div, load, wasSeen: group == 'chat'}) : load();
//lazyLoadQueue && !downloaded ? lazyLoadQueue.push({div, load, wasSeen: group == 'chat'}) : load();
load();
}
}
@ -482,33 +479,27 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o @@ -482,33 +479,27 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o
}
let downloaded = doc.downloaded;
let load = () => appDocsManager.downloadDocNew(doc.id).promise.then(blob => {
//console.log('loaded sticker:', doc, div);
let load = async() => {
if(middleware && !middleware()) return;
//return;
if(stickerType == 2) {
const reader = new FileReader();
reader.addEventListener('loadend', async(e) => {
//console.time('decompress sticker' + doc.id);
//console.time('render sticker' + doc.id);
// @ts-ignore
const text = e.srcElement.result;
let json = await apiManager.gzipUncompress<string>(text, true);
/* if(doc.id == '1860749763008266301') {
console.log('loaded sticker:', doc, div);
} */
//console.timeEnd('decompress sticker' + doc.id);
//console.time('download sticker' + doc.id);
/* if(doc.id == '1860749763008266301') {
console.log('loaded sticker:', doc, div);
} */
//appDocsManager.downloadDocNew(doc.id).promise.then(res => res.json()).then(async(json) => {
fetch(doc.url).then(res => res.json()).then(async(json) => {
//console.timeEnd('download sticker' + doc.id);
//console.log('loaded sticker:', doc, div);
if(middleware && !middleware()) return;
let animation = await LottieLoader.loadAnimationWorker/* loadAnimation */({
container: div,
loop: loop && !emoji,
autoplay: play,
animationData: JSON.parse(json),
animationData: json,
width,
height
}, group, toneIndex);
@ -530,11 +521,9 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o @@ -530,11 +521,9 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o
}
});
}
//console.timeEnd('render sticker' + doc.id);
});
reader.readAsArrayBuffer(blob);
//console.timeEnd('render sticker' + doc.id);
} else if(stickerType == 1) {
let img = new Image();
@ -543,6 +532,8 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o @@ -543,6 +532,8 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o
img.style.opacity = '0';
img.addEventListener('load', () => {
doc.downloaded = true;
window.requestAnimationFrame(() => {
img.style.opacity = '';
});
@ -557,7 +548,7 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o @@ -557,7 +548,7 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o
div.append(img);
});
}
});
};
return lazyLoadQueue && (!doc.downloaded || stickerType == 2) ? (lazyLoadQueue.push({div, load, wasSeen: group == 'chat' && stickerType != 2}), Promise.resolve()) : load();
}

62
src/lib/appManagers/appDocsManager.ts

@ -4,7 +4,7 @@ import { isObject, getFileURL } from '../utils'; @@ -4,7 +4,7 @@ import { isObject, getFileURL } from '../utils';
import opusDecodeController from '../opusDecodeController';
import { MTDocument, inputDocumentFileLocation } from '../../types';
import { getFileNameByLocation } from '../bin_utils';
import appDownloadManager, { Download } from './appDownloadManager';
import appDownloadManager, { Download, ResponseMethod } from './appDownloadManager';
class AppDocsManager {
private docs: {[docID: string]: MTDocument} = {};
@ -273,7 +273,7 @@ class AppDocsManager { @@ -273,7 +273,7 @@ class AppDocsManager {
return this.downloadPromises[doc.id] = deferred;
}
public downloadDocNew(docID: string | MTDocument, toFileEntry?: any): Download {
public downloadDocNew(docID: string | MTDocument/* , method: ResponseMethod = 'blob' */): Download {
const doc = this.getDoc(docID);
if(doc._ == 'documentEmpty') {
@ -287,39 +287,39 @@ class AppDocsManager { @@ -287,39 +287,39 @@ class AppDocsManager {
return download;
}
download = appDownloadManager.download(fileName, doc.url);
//const _download: Download = {...download};
download = appDownloadManager.download(fileName, doc.url/* , method */);
//_download.promise = _download.promise.then(async(blob) => {
download.promise = download.promise.then(async(blob) => {
if(blob) {
doc.downloaded = true;
if(doc.type == 'voice' && !opusDecodeController.isPlaySupported()) {
let reader = new FileReader();
const originalPromise = download.promise;
originalPromise.then(() => {
doc.downloaded = true;
});
await new Promise((resolve, reject) => {
reader.onloadend = (e) => {
let uint8 = new Uint8Array(e.target.result as ArrayBuffer);
//console.log('sending uint8 to decoder:', uint8);
opusDecodeController.decode(uint8).then(result => {
doc.url = result.url;
resolve();
}, (err) => {
delete doc.downloaded;
reject(err);
});
};
if(doc.type == 'voice' && !opusDecodeController.isPlaySupported()) {
download.promise = originalPromise.then(async(blob) => {
let reader = new FileReader();
reader.readAsArrayBuffer(blob);
});
}
}
return blob;
});
await new Promise((resolve, reject) => {
reader.onloadend = (e) => {
let uint8 = new Uint8Array(e.target.result as ArrayBuffer);
//console.log('sending uint8 to decoder:', uint8);
opusDecodeController.decode(uint8).then(result => {
doc.url = result.url;
resolve();
}, (err) => {
delete doc.downloaded;
reject(err);
});
};
reader.readAsArrayBuffer(blob);
});
return blob;
//return originalPromise;
//return new Response(blob);
});
}
//return this.downloadPromisesNew[doc.id] = _download;
return download;
}

15
src/lib/appManagers/appDownloadManager.ts

@ -1,7 +1,14 @@ @@ -1,7 +1,14 @@
import { $rootScope } from "../utils";
import apiManager from "../mtproto/mtprotoworker";
export type Download = {promise: Promise<Blob>, controller: AbortController};
export type ResponseMethodBlob = 'blob';
export type ResponseMethodJson = 'json';
export type ResponseMethod = ResponseMethodBlob | ResponseMethodJson;
export type DownloadBlob = {promise: Promise<Blob>, controller: AbortController};
export type DownloadJson = {promise: Promise<any>, controller: AbortController};
export type Download = DownloadBlob/* | DownloadJson */;
export type Progress = {done: number, fileName: string, total: number, offset: number};
export type ProgressCallback = (details: Progress) => void;
@ -22,12 +29,14 @@ export class AppDownloadManager { @@ -22,12 +29,14 @@ export class AppDownloadManager {
});
}
public download(fileName: string, url: string) {
public download(fileName: string, url: string, responseMethod?: ResponseMethodBlob): DownloadBlob;
public download(fileName: string, url: string, responseMethod?: ResponseMethodJson): DownloadJson;
public download(fileName: string, url: string, responseMethod: ResponseMethod = 'blob'): Download {
if(this.downloads.hasOwnProperty(fileName)) return this.downloads[fileName];
const controller = new AbortController();
const promise = fetch(url, {signal: controller.signal})
.then(res => res.blob())
.then(res => res[responseMethod]())
.catch(err => { // Только потому что event.request.signal не работает в SW, либо я кривой?
if(err.name === 'AbortError') {
//console.log('Fetch aborted');

4
src/lib/appManagers/appStickersManager.ts

@ -217,7 +217,7 @@ class AppStickersManager { @@ -217,7 +217,7 @@ class AppStickersManager {
}, 100);
}
public getStickerSetThumb(stickerSet: MTStickerSet) {
public getStickerSetThumbURL(stickerSet: MTStickerSet) {
const thumb = stickerSet.thumb;
const dcID = stickerSet.thumb_dc_id;
@ -231,7 +231,7 @@ class AppStickersManager { @@ -231,7 +231,7 @@ class AppStickersManager {
};
const url = getFileURL('document', {dcID, location: input, size: thumb.size, mimeType: isAnimated ? "application/x-tgsticker" : 'image/webp'});
return fetch(url).then(res => res.blob());
return url;
//return promise;
}

11
src/lib/bin_utils.ts

@ -134,15 +134,15 @@ export function dataUrlToBlob(url: string) { @@ -134,15 +134,15 @@ export function dataUrlToBlob(url: string) {
return blob;
}
export function blobConstruct(blobParts: any, mimeType: string = '') {
var blob;
var safeMimeType = blobSafeMimeType(mimeType);
export function blobConstruct(blobParts: any, mimeType: string = ''): Blob {
let blob;
const safeMimeType = blobSafeMimeType(mimeType);
try {
blob = new Blob(blobParts, {type: safeMimeType});
} catch(e) {
// @ts-ignore
var bb = new BlobBuilder;
blobParts.forEach(function(blobPart: any) {
let bb = new BlobBuilder;
blobParts.forEach((blobPart: any) => {
bb.append(blobPart);
});
blob = bb.getBlob(safeMimeType);
@ -163,6 +163,7 @@ export function blobSafeMimeType(mimeType: string) { @@ -163,6 +163,7 @@ export function blobSafeMimeType(mimeType: string) {
'audio/ogg',
'audio/mpeg',
'audio/mp4',
'application/json'
].indexOf(mimeType) === -1) {
return 'application/octet-stream';
}

18
src/lib/filemanager.ts

@ -15,12 +15,8 @@ export class FileManager { @@ -15,12 +15,8 @@ export class FileManager {
return this.blobSupported;
}
public write(fileWriter: ReturnType<FileManager['getFakeFileWriter']>, bytes: Uint8Array | Blob | {file: any}): Promise<void> {
if('file' in bytes) {
return bytes.file((file: any) => {
return fileWriter.write(file);
});
} else if(bytes instanceof Blob) { // is file bytes
public write(fileWriter: ReturnType<FileManager['getFakeFileWriter']>, bytes: Uint8Array | Blob | string): Promise<void> {
if(bytes instanceof Blob) { // is file bytes
return new Promise((resolve, reject) => {
let fileReader = new FileReader();
fileReader.onload = function(event) {
@ -39,20 +35,20 @@ export class FileManager { @@ -39,20 +35,20 @@ export class FileManager {
}
public getFakeFileWriter(mimeType: string, saveFileCallback: (blob: Blob) => Promise<Blob>) {
let blobParts: Array<Uint8Array> = [];
const blobParts: Array<Uint8Array | string> = [];
const fakeFileWriter = {
write: async(blob: Uint8Array) => {
write: async(part: Uint8Array | string) => {
if(!this.blobSupported) {
throw false;
}
blobParts.push(blob);
blobParts.push(part);
},
truncate: () => {
blobParts = [];
blobParts.length = 0;
},
finalize: () => {
const blob = blobConstruct(blobParts, mimeType) as Blob;
const blob = blobConstruct(blobParts, mimeType);
if(saveFileCallback) {
saveFileCallback(blob);
}

66
src/lib/mtproto/apiFileManager.ts

@ -4,9 +4,10 @@ import cacheStorage from "../cacheStorage"; @@ -4,9 +4,10 @@ import cacheStorage from "../cacheStorage";
import FileManager from "../filemanager";
import apiManager from "./apiManager";
import { deferredPromise, CancellablePromise } from "../polyfill";
import { logger } from "../logger";
import { logger, LogLevels } from "../logger";
import { InputFileLocation, FileLocation, UploadFile } from "../../types";
import { isSafari } from "../../helpers/userAgent";
import cryptoWorker from "../crypto/cryptoworker";
type Delayed = {
offset: number,
@ -43,7 +44,7 @@ export class ApiFileManager { @@ -43,7 +44,7 @@ export class ApiFileManager {
public webpConvertPromises: {[fileName: string]: CancellablePromise<Uint8Array>} = {};
private log: ReturnType<typeof logger> = logger('AFM');
private log: ReturnType<typeof logger> = logger('AFM', LogLevels.error);
public downloadRequest(dcID: 'upload', cb: () => Promise<void>, activeDelta: number): Promise<void>;
public downloadRequest(dcID: number, cb: () => Promise<UploadFile>, activeDelta: number): Promise<UploadFile>;
@ -144,6 +145,29 @@ export class ApiFileManager { @@ -144,6 +145,29 @@ export class ApiFileManager {
return bytes * 1024;
}
uncompressTGS = (bytes: Uint8Array, fileName: string) => {
//this.log('uncompressTGS', bytes, bytes.slice().buffer);
// slice нужен потому что в uint8array - 5053 length, в arraybuffer - 5084
return cryptoWorker.gzipUncompress<string>(bytes.slice().buffer, true);
};
convertWebp = (bytes: Uint8Array, fileName: string) => {
const convertPromise = deferredPromise<Uint8Array>();
(self as any as ServiceWorkerGlobalScope)
.clients
.matchAll({includeUncontrolled: false, type: 'window'})
.then((listeners) => {
if(!listeners.length) {
return;
}
listeners[0].postMessage({type: 'convertWebp', payload: {fileName, bytes}});
});
return this.webpConvertPromises[fileName] = convertPromise;
};
public downloadFile(options: DownloadOptions): CancellablePromise<Blob> {
if(!FileManager.isAvailable()) {
return Promise.reject({type: 'BROWSER_BLOB_NOT_SUPPORTED'});
@ -152,18 +176,21 @@ export class ApiFileManager { @@ -152,18 +176,21 @@ export class ApiFileManager {
let size = options.size ?? 0;
let {dcID, location} = options;
let processWebp = false;
let process: ApiFileManager['uncompressTGS'] | ApiFileManager['convertWebp'];
if(options.mimeType == 'image/webp' && isSafari) {
processWebp = true;
process = this.convertWebp;
options.mimeType = 'image/png';
} else if(options.mimeType == 'application/x-tgsticker') {
process = this.uncompressTGS;
options.mimeType = 'application/json';
}
// this.log('Dload file', dcID, location, size)
const fileName = getFileNameByLocation(location);
const cachedPromise = this.cachedDownloadPromises[fileName];
const fileStorage = this.getFileStorage();
//this.log('downloadFile', fileName, fileName.length, location, arguments);
//this.log('downloadFile', fileName, size, location, options.mimeType, process);
if(cachedPromise) {
if(options.processPart) {
@ -266,22 +293,11 @@ export class ApiFileManager { @@ -266,22 +293,11 @@ export class ApiFileManager {
await options.processPart(bytes, offset, delayed);
}
if(processWebp) {
const convertPromise = deferredPromise<Uint8Array>();
(self as any as ServiceWorkerGlobalScope)
.clients
.matchAll({includeUncontrolled: false, type: 'window'})
.then((listeners) => {
if(!listeners.length) {
return;
}
listeners[0].postMessage({type: 'convertWebp', payload: {fileName, bytes}});
});
return await (this.webpConvertPromises[fileName] = convertPromise);
//return appWebpManager.convertToPng(bytes);
if(process) {
//const perf = performance.now();
const processed = await process(bytes, fileName);
//this.log('downloadFile process downloaded time', performance.now() - perf, mimeType, process);
return processed;
}
return bytes;
@ -318,12 +334,12 @@ export class ApiFileManager { @@ -318,12 +334,12 @@ export class ApiFileManager {
superpuper();
}
//////////////////////////////////////////
//done += limit;
done += result.bytes.byteLength;
const processedResult = await processDownloaded(result.bytes, offset);
checkCancel();
//done += limit;
done += processedResult.byteLength;
const isFinal = offset + limit >= size;
//if(!isFinal) {
////this.log('deferred notify 2:', {done: offset + limit, total: size}, deferred);

2
src/lib/mtproto/mtproto.service.ts

@ -11,7 +11,7 @@ import { getFileNameByLocation } from '../bin_utils'; @@ -11,7 +11,7 @@ import { getFileNameByLocation } from '../bin_utils';
import { logger, LogLevels } from '../logger';
import { isSafari } from '../../helpers/userAgent';
const log = logger('SW'/* , LogLevels.error */);
const log = logger('SW', LogLevels.error);
const ctx = self as any as ServiceWorkerGlobalScope;

5
tsconfig.json

@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
"sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
// "outDir": "./dist/", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
//"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
@ -65,8 +65,9 @@ @@ -65,8 +65,9 @@
"node_modules",
"public",
"coverage",
"./public/*.js",
"./src/lib/crypto/crypto.worker.js",
"src/vendor/StackBlur.js",
"./src/vendor/StackBlur.js",
"./src/lib/*.js",
"./src/*.js",
"*.js",

Loading…
Cancel
Save