469 lines
15 KiB
TypeScript
469 lines
15 KiB
TypeScript
/*
|
||
* https://github.com/morethanwords/tweb
|
||
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||
*
|
||
* Originally from:
|
||
* https://github.com/zhukov/webogram
|
||
* Copyright (C) 2014 Igor Zhukov <igor.beatle@gmail.com>
|
||
* https://github.com/zhukov/webogram/blob/master/LICENSE
|
||
*/
|
||
|
||
import { FileURLType, getFileNameByLocation, getFileURL } from '../../helpers/fileName';
|
||
import { safeReplaceArrayInObject, defineNotNumerableProperties, isObject } from '../../helpers/object';
|
||
import { Document, InputFileLocation, InputMedia, PhotoSize } from '../../layer';
|
||
import referenceDatabase, { ReferenceContext } from '../mtproto/referenceDatabase';
|
||
import opusDecodeController from '../opusDecodeController';
|
||
import { RichTextProcessor } from '../richtextprocessor';
|
||
import appDownloadManager, { DownloadBlob } from './appDownloadManager';
|
||
import appPhotosManager from './appPhotosManager';
|
||
import blur from '../../helpers/blur';
|
||
import apiManager from '../mtproto/mtprotoworker';
|
||
import { MOUNT_CLASS_TO } from '../../config/debug';
|
||
import { getFullDate } from '../../helpers/date';
|
||
import rootScope from '../rootScope';
|
||
import IS_WEBP_SUPPORTED from '../../environment/webpSupport';
|
||
import IS_WEBM_SUPPORTED from '../../environment/webmSupport';
|
||
|
||
export type MyDocument = Document.document;
|
||
|
||
// TODO: если залить картинку файлом, а потом перезайти в диалог - превьюшка заново скачается
|
||
|
||
const EXTENSION_MIME_TYPE_MAP = {
|
||
mov: 'video/quicktime',
|
||
gif: 'image/gif',
|
||
pdf: 'application/pdf',
|
||
};
|
||
|
||
export class AppDocsManager {
|
||
private docs: {[docId: DocId]: MyDocument} = {};
|
||
private savingLottiePreview: {[docId: DocId]: true} = {};
|
||
public downloading: Map<DocId, DownloadBlob> = new Map();
|
||
|
||
constructor() {
|
||
apiManager.onServiceWorkerFail = this.onServiceWorkerFail;
|
||
}
|
||
|
||
public onServiceWorkerFail = () => {
|
||
for(const id in this.docs) {
|
||
const doc = this.docs[id];
|
||
|
||
if(doc.supportsStreaming) {
|
||
delete doc.supportsStreaming;
|
||
const cacheContext = appDownloadManager.getCacheContext(doc);
|
||
delete cacheContext.url;
|
||
}
|
||
}
|
||
};
|
||
|
||
public saveDoc(doc: Document, context?: ReferenceContext): MyDocument {
|
||
if(doc._ === 'documentEmpty') {
|
||
return undefined;
|
||
}
|
||
|
||
const oldDoc = this.docs[doc.id];
|
||
|
||
if(doc.file_reference) { // * because we can have a new object w/o the file_reference while sending
|
||
safeReplaceArrayInObject('file_reference', oldDoc, doc);
|
||
referenceDatabase.saveContext(doc.file_reference, context);
|
||
}
|
||
|
||
//console.log('saveDoc', apiDoc, this.docs[apiDoc.id]);
|
||
// if(oldDoc) {
|
||
// //if(doc._ !== 'documentEmpty' && doc._ === d._) {
|
||
// if(doc.thumbs) {
|
||
// if(!oldDoc.thumbs) oldDoc.thumbs = doc.thumbs;
|
||
// /* else if(apiDoc.thumbs[0].bytes && !d.thumbs[0].bytes) {
|
||
// d.thumbs.unshift(apiDoc.thumbs[0]);
|
||
// } else if(d.thumbs[0].url) { // fix for converted thumb in safari
|
||
// apiDoc.thumbs[0] = d.thumbs[0];
|
||
// } */
|
||
// }
|
||
|
||
// //}
|
||
|
||
// return oldDoc;
|
||
|
||
// //return Object.assign(d, apiDoc, context);
|
||
// //return context ? Object.assign(d, context) : d;
|
||
// }
|
||
|
||
if(!oldDoc) {
|
||
this.docs[doc.id] = doc;
|
||
}
|
||
|
||
// * exclude from state
|
||
// defineNotNumerableProperties(doc, [/* 'thumbs', */'type', 'h', 'w', 'file_name',
|
||
// 'file', 'duration', 'downloaded', 'url', 'audioTitle',
|
||
// 'audioPerformer', 'sticker', 'stickerEmoji', 'stickerEmojiRaw',
|
||
// 'stickerSetInput', 'stickerThumbConverted', 'animated', 'supportsStreaming']);
|
||
|
||
for(let i = 0, length = doc.attributes.length; i < length; ++i) {
|
||
const attribute = doc.attributes[i];
|
||
switch(attribute._) {
|
||
case 'documentAttributeFilename':
|
||
doc.file_name = RichTextProcessor.wrapPlainText(attribute.file_name);
|
||
doc.fileName = RichTextProcessor.wrapEmojiText(attribute.file_name);
|
||
break;
|
||
|
||
case 'documentAttributeAudio':
|
||
doc.duration = attribute.duration;
|
||
doc.audioTitle = RichTextProcessor.wrapEmojiText(attribute.title);
|
||
doc.audioPerformer = RichTextProcessor.wrapEmojiText(attribute.performer);
|
||
doc.type = attribute.pFlags.voice && doc.mime_type === 'audio/ogg' ? 'voice' : 'audio';
|
||
/* if(apiDoc.type === 'audio') {
|
||
apiDoc.supportsStreaming = true;
|
||
} */
|
||
break;
|
||
|
||
case 'documentAttributeVideo':
|
||
doc.duration = attribute.duration;
|
||
doc.w = attribute.w;
|
||
doc.h = attribute.h;
|
||
//apiDoc.supportsStreaming = attribute.pFlags?.supports_streaming/* && apiDoc.size > 524288 */;
|
||
if(/* apiDoc.thumbs && */attribute.pFlags.round_message) {
|
||
doc.type = 'round';
|
||
} else /* if(apiDoc.thumbs) */ {
|
||
doc.type = 'video';
|
||
}
|
||
break;
|
||
|
||
case 'documentAttributeSticker':
|
||
if(attribute.alt !== undefined) {
|
||
doc.stickerEmojiRaw = attribute.alt;
|
||
doc.stickerEmoji = RichTextProcessor.wrapRichText(doc.stickerEmojiRaw, {noLinks: true, noLinebreaks: true});
|
||
}
|
||
|
||
if(attribute.stickerset) {
|
||
if(attribute.stickerset._ === 'inputStickerSetEmpty') {
|
||
delete attribute.stickerset;
|
||
} else if(attribute.stickerset._ === 'inputStickerSetID') {
|
||
doc.stickerSetInput = attribute.stickerset;
|
||
}
|
||
}
|
||
|
||
// * there can be no thumbs, then it is a document
|
||
if(/* apiDoc.thumbs && */doc.mime_type === 'image/webp' && (doc.thumbs || IS_WEBP_SUPPORTED)) {
|
||
doc.type = 'sticker';
|
||
doc.sticker = 1;
|
||
} else if(doc.mime_type === 'video/webm') {
|
||
if(!IS_WEBM_SUPPORTED) {
|
||
return;
|
||
}
|
||
|
||
doc.type = 'sticker';
|
||
doc.sticker = 3;
|
||
doc.animated = true;
|
||
}
|
||
break;
|
||
|
||
case 'documentAttributeImageSize':
|
||
doc.type = 'photo';
|
||
doc.w = attribute.w;
|
||
doc.h = attribute.h;
|
||
break;
|
||
|
||
case 'documentAttributeAnimated':
|
||
if((doc.mime_type === 'image/gif' || doc.mime_type === 'video/mp4')/* && apiDoc.thumbs */) {
|
||
doc.type = 'gif';
|
||
}
|
||
|
||
doc.animated = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if(!doc.mime_type) {
|
||
const ext = (doc.file_name || '').split('.').pop();
|
||
// @ts-ignore
|
||
const mappedMimeType = ext && EXTENSION_MIME_TYPE_MAP[ext.toLowerCase()];
|
||
if(mappedMimeType) {
|
||
doc.mime_type = mappedMimeType;
|
||
} else {
|
||
switch(doc.type) {
|
||
case 'gif':
|
||
case 'video':
|
||
case 'round':
|
||
doc.mime_type = 'video/mp4';
|
||
break;
|
||
case 'sticker':
|
||
doc.mime_type = 'image/webp';
|
||
break;
|
||
case 'audio':
|
||
doc.mime_type = 'audio/mpeg';
|
||
break;
|
||
case 'voice':
|
||
doc.mime_type = 'audio/ogg';
|
||
break;
|
||
default:
|
||
doc.mime_type = 'application/octet-stream';
|
||
break;
|
||
}
|
||
}
|
||
} else if(doc.mime_type === EXTENSION_MIME_TYPE_MAP.pdf) {
|
||
doc.type = 'pdf';
|
||
} else if(doc.mime_type === EXTENSION_MIME_TYPE_MAP.gif) {
|
||
doc.type = 'gif';
|
||
}
|
||
|
||
if(doc.type === 'voice' || doc.type === 'round') {
|
||
// browser will identify extension
|
||
doc.file_name = doc.fileName = doc.type + '_' + getFullDate(new Date(doc.date * 1000), {monthAsNumber: true, leadingZero: true}).replace(/[:\.]/g, '-').replace(', ', '_');
|
||
}
|
||
|
||
if(apiManager.isServiceWorkerOnline()) {
|
||
if((doc.type === 'gif' && doc.size > 8e6) || doc.type === 'audio' || doc.type === 'video'/* || doc.mime_type.indexOf('video/') === 0 */) {
|
||
doc.supportsStreaming = true;
|
||
|
||
const cacheContext = appDownloadManager.getCacheContext(doc);
|
||
if(!cacheContext.url) {
|
||
cacheContext.url = this.getFileURL(doc);
|
||
}
|
||
}
|
||
}
|
||
|
||
// for testing purposes
|
||
// doc.supportsStreaming = false;
|
||
// doc.url = ''; // * this will break upload urls
|
||
|
||
if(!doc.file_name) {
|
||
doc.file_name = doc.fileName = '';
|
||
}
|
||
|
||
if(doc.mime_type === 'application/x-tgsticker' && doc.file_name === 'AnimatedSticker.tgs') {
|
||
doc.type = 'sticker';
|
||
doc.animated = true;
|
||
doc.sticker = 2;
|
||
}
|
||
|
||
/* if(!doc.url) {
|
||
doc.url = this.getFileURL(doc);
|
||
} */
|
||
|
||
if(oldDoc) {
|
||
return Object.assign(oldDoc, doc);
|
||
}
|
||
|
||
return doc;
|
||
}
|
||
|
||
public getDoc(docId: DocId | MyDocument): MyDocument {
|
||
return isObject<MyDocument>(docId) ? docId : this.docs[docId];
|
||
}
|
||
|
||
public getMediaInput(doc: MyDocument): InputMedia.inputMediaDocument {
|
||
return {
|
||
_: 'inputMediaDocument',
|
||
id: {
|
||
_: 'inputDocument',
|
||
id: doc.id,
|
||
access_hash: doc.access_hash,
|
||
file_reference: doc.file_reference
|
||
},
|
||
ttl_seconds: 0
|
||
};
|
||
}
|
||
|
||
public getInput(doc: MyDocument, thumbSize?: string): InputFileLocation.inputDocumentFileLocation {
|
||
return {
|
||
_: 'inputDocumentFileLocation',
|
||
id: doc.id,
|
||
access_hash: doc.access_hash,
|
||
file_reference: doc.file_reference,
|
||
thumb_size: thumbSize
|
||
};
|
||
}
|
||
|
||
public getFileDownloadOptions(doc: MyDocument, thumb?: PhotoSize.photoSize, queueId?: number, onlyCache?: boolean) {
|
||
const inputFileLocation = this.getInput(doc, thumb?.type);
|
||
|
||
let mimeType: string;
|
||
if(thumb) {
|
||
mimeType = doc.sticker ? 'image/webp' : 'image/jpeg'/* doc.mime_type */;
|
||
} else {
|
||
mimeType = doc.mime_type || 'application/octet-stream';
|
||
}
|
||
|
||
return {
|
||
dcId: doc.dc_id,
|
||
location: inputFileLocation,
|
||
size: thumb ? thumb.size : doc.size,
|
||
mimeType,
|
||
fileName: doc.file_name,
|
||
queueId,
|
||
onlyCache
|
||
};
|
||
}
|
||
|
||
public getFileURL(doc: MyDocument, download = false, thumb?: PhotoSize.photoSize) {
|
||
let type: FileURLType;
|
||
if(download) {
|
||
type = 'download';
|
||
} else if(thumb) {
|
||
type = 'thumb';
|
||
} else if(doc.supportsStreaming) {
|
||
type = 'stream';
|
||
} else {
|
||
type = 'document';
|
||
}
|
||
|
||
return getFileURL(type, this.getFileDownloadOptions(doc, thumb));
|
||
}
|
||
|
||
public getThumbURL(doc: MyDocument, thumb: PhotoSize.photoSize | PhotoSize.photoCachedSize | PhotoSize.photoStrippedSize) {
|
||
let promise: Promise<any> = Promise.resolve();
|
||
|
||
const cacheContext = appDownloadManager.getCacheContext(doc, thumb.type);
|
||
if(!cacheContext.url) {
|
||
if('bytes' in thumb) {
|
||
promise = blur(appPhotosManager.getPreviewURLFromBytes(thumb.bytes, !!doc.sticker)).then(url => {
|
||
cacheContext.url = url;
|
||
}) as any;
|
||
} else {
|
||
//return this.getFileURL(doc, false, thumb);
|
||
promise = appPhotosManager.preloadPhoto(doc, thumb) as any;
|
||
}
|
||
}
|
||
|
||
return {thumb, cacheContext, promise};
|
||
}
|
||
|
||
public getThumb(doc: MyDocument, tryNotToUseBytes = true) {
|
||
const thumb = appPhotosManager.choosePhotoSize(doc, 0, 0, !tryNotToUseBytes);
|
||
if(thumb._ === 'photoSizeEmpty') return null;
|
||
return this.getThumbURL(doc, thumb as any);
|
||
}
|
||
|
||
public getInputFileName(doc: MyDocument, thumbSize?: string) {
|
||
return getFileNameByLocation(this.getInput(doc, thumbSize), {fileName: doc.file_name});
|
||
}
|
||
|
||
public downloadDoc(doc: MyDocument, queueId?: number, onlyCache?: boolean): DownloadBlob {
|
||
const fileName = this.getInputFileName(doc);
|
||
|
||
let download: DownloadBlob = appDownloadManager.getDownload(fileName);
|
||
if(download) {
|
||
return download;
|
||
}
|
||
|
||
const downloadOptions = this.getFileDownloadOptions(doc, undefined, queueId, onlyCache);
|
||
download = appDownloadManager.download(downloadOptions);
|
||
this.downloading.set(doc.id, download);
|
||
rootScope.dispatchEvent('download_start', doc.id);
|
||
|
||
const cacheContext = appDownloadManager.getCacheContext(doc);
|
||
const originalPromise = download;
|
||
originalPromise.then((blob) => {
|
||
cacheContext.url = URL.createObjectURL(blob);
|
||
cacheContext.downloaded = blob.size;
|
||
}, () => {}).finally(() => {
|
||
this.downloading.delete(doc.id);
|
||
});
|
||
|
||
if(doc.type === 'voice' && !opusDecodeController.isPlaySupported()) {
|
||
download = originalPromise.then(async(blob) => {
|
||
const reader = new FileReader();
|
||
|
||
await new Promise<void>((resolve, reject) => {
|
||
reader.onloadend = (e) => {
|
||
const uint8 = new Uint8Array(e.target.result as ArrayBuffer);
|
||
//console.log('sending uint8 to decoder:', uint8);
|
||
opusDecodeController.decode(uint8).then(result => {
|
||
cacheContext.url = result.url;
|
||
resolve();
|
||
}, (err) => {
|
||
delete cacheContext.downloaded;
|
||
reject(err);
|
||
});
|
||
};
|
||
|
||
reader.readAsArrayBuffer(blob);
|
||
});
|
||
|
||
return blob;
|
||
});
|
||
}
|
||
|
||
download.then(() => {
|
||
rootScope.dispatchEvent('document_downloaded', doc);
|
||
});
|
||
|
||
return download;
|
||
}
|
||
|
||
public saveLottiePreview(doc: MyDocument, canvas: HTMLCanvasElement, toneIndex: number) {
|
||
const key = doc.id + '-' + toneIndex;
|
||
if(this.savingLottiePreview[key]/* || true */) return;
|
||
|
||
if(!doc.stickerCachedThumbs) {
|
||
defineNotNumerableProperties(doc, ['stickerCachedThumbs']);
|
||
doc.stickerCachedThumbs = {};
|
||
}
|
||
|
||
const thumb = doc.stickerCachedThumbs[toneIndex];
|
||
if(thumb && thumb.w >= canvas.width && thumb.h >= canvas.height) {
|
||
return;
|
||
}
|
||
|
||
/* if(doc.thumbs.find(t => t._ === 'photoStrippedSize')
|
||
|| (doc.stickerCachedThumb || (doc.stickerSavedThumbWidth >= canvas.width && doc.stickerSavedThumbHeight >= canvas.height))) {
|
||
return;
|
||
} */
|
||
|
||
this.savingLottiePreview[key] = true;
|
||
canvas.toBlob((blob) => {
|
||
//console.log('got lottie preview', doc, blob, URL.createObjectURL(blob));
|
||
|
||
const thumb = {
|
||
url: URL.createObjectURL(blob),
|
||
w: canvas.width,
|
||
h: canvas.height
|
||
};
|
||
|
||
doc.stickerCachedThumbs[toneIndex] = thumb;
|
||
|
||
delete this.savingLottiePreview[key];
|
||
|
||
/* const reader = new FileReader();
|
||
reader.onloadend = (e) => {
|
||
const uint8 = new Uint8Array(e.target.result as ArrayBuffer);
|
||
const thumb: PhotoSize.photoStrippedSize = {
|
||
_: 'photoStrippedSize',
|
||
bytes: uint8,
|
||
type: 'i'
|
||
};
|
||
|
||
doc.stickerSavedThumbWidth = canvas.width;
|
||
doc.stickerSavedThumbHeight = canvas.width;
|
||
|
||
defineNotNumerableProperties(thumb, ['url']);
|
||
thumb.url = URL.createObjectURL(blob);
|
||
doc.thumbs.findAndSplice(t => t._ === thumb._);
|
||
doc.thumbs.unshift(thumb);
|
||
|
||
if(!webpWorkerController.isWebpSupported()) {
|
||
doc.pFlags.stickerThumbConverted = true;
|
||
}
|
||
|
||
delete this.savingLottiePreview[doc.id];
|
||
};
|
||
reader.readAsArrayBuffer(blob); */
|
||
});
|
||
}
|
||
|
||
public saveDocFile(doc: MyDocument, queueId?: number) {
|
||
/* const options = this.getFileDownloadOptions(doc, undefined, queueId);
|
||
return appDownloadManager.downloadToDisc(options, doc.file_name); */
|
||
const promise = this.downloadDoc(doc, queueId);
|
||
promise.then(() => {
|
||
const cacheContext = appDownloadManager.getCacheContext(doc);
|
||
appDownloadManager.createDownloadAnchor(cacheContext.url, doc.file_name);
|
||
});
|
||
return promise;
|
||
}
|
||
}
|
||
|
||
const appDocsManager = new AppDocsManager();
|
||
MOUNT_CLASS_TO.appDocsManager = appDocsManager;
|
||
export default appDocsManager;
|