Telegram Web K with changes to work inside I2P https://web.telegram.i2p/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

390 lines
14 KiB

/*
* 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 type { DownloadOptions } from "../mtproto/apiFileManager";
import { bytesFromHex } from "../../helpers/bytes";
import { CancellablePromise } from "../../helpers/cancellablePromise";
import { getFileNameByLocation } from "../../helpers/fileName";
import { safeReplaceArrayInObject, defineNotNumerableProperties, isObject } from "../../helpers/object";
import { isSafari } from "../../helpers/userAgent";
4 years ago
import { InputFileLocation, InputMedia, Photo, PhotoSize, PhotosPhotos } from "../../layer";
import apiManager from "../mtproto/mtprotoworker";
import referenceDatabase, { ReferenceContext } from "../mtproto/referenceDatabase";
import { calcImageInBox } from "../../helpers/dom";
import { MyDocument } from "./appDocsManager";
import appDownloadManager from "./appDownloadManager";
import appUsersManager from "./appUsersManager";
import blur from "../../helpers/blur";
4 years ago
import { MOUNT_CLASS_TO } from "../../config/debug";
import renderImageFromUrl from "../../helpers/dom/renderImageFromUrl";
export type MyPhoto = Photo.photo;
5 years ago
// TIMES = 2 DUE TO SIDEBAR AND CHAT
//let TEST_FILE_REFERENCE = "5440692274120994569", TEST_FILE_REFERENCE_TIMES = 2;
5 years ago
export class AppPhotosManager {
private photos: {
[id: string]: MyPhoto
5 years ago
} = {};
public windowW = 0;
public windowH = 0;
5 years ago
private static jpegHeader = bytesFromHex('ffd8ffe000104a46494600010100000100010000ffdb004300281c1e231e19282321232d2b28303c64413c37373c7b585d4964918099968f808c8aa0b4e6c3a0aadaad8a8cc8ffcbdaeef5ffffff9bc1fffffffaffe6fdfff8ffdb0043012b2d2d3c353c76414176f8a58ca5f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8ffc00011080000000003012200021101031101ffc4001f0000010501010101010100000000000000000102030405060708090a0bffc400b5100002010303020403050504040000017d01020300041105122131410613516107227114328191a1082342b1c11552d1f02433627282090a161718191a25262728292a3435363738393a434445464748494a535455565758595a636465666768696a737475767778797a838485868788898a92939495969798999aa2a3a4a5a6a7a8a9aab2b3b4b5b6b7b8b9bac2c3c4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae1e2e3e4e5e6e7e8e9eaf1f2f3f4f5f6f7f8f9faffc4001f0100030101010101010101010000000000000102030405060708090a0bffc400b51100020102040403040705040400010277000102031104052131061241510761711322328108144291a1b1c109233352f0156272d10a162434e125f11718191a262728292a35363738393a434445464748494a535455565758595a636465666768696a737475767778797a82838485868788898a92939495969798999aa2a3a4a5a6a7a8a9aab2b3b4b5b6b7b8b9bac2c3c4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae2e3e4e5e6e7e8e9eaf2f3f4f5f6f7f8f9faffda000c03010002110311003f00');
private static jpegTail = bytesFromHex('ffd9');
5 years ago
constructor() {
// @ts-ignore
const w: any = 'visualViewport' in window ? window.visualViewport : window;
const set = () => {
this.windowW = w.width || w.innerWidth;
this.windowH = w.height || w.innerHeight;
};
w.addEventListener('resize', set);
set();
5 years ago
}
public savePhoto(photo: Photo, context?: ReferenceContext) {
if(photo._ === 'photoEmpty') return undefined;
/* if(photo.id === TEST_FILE_REFERENCE) {
console.warn('Testing FILE_REFERENCE_EXPIRED');
const bytes = [2, 67, 175, 43, 190, 0, 0, 20, 62, 95, 111, 33, 45, 99, 220, 116, 218, 11, 167, 127, 213, 18, 127, 32, 243, 202, 117, 80, 30];
//photo.file_reference = new Uint8Array(bytes);
photo.file_reference = bytes;
if(!--TEST_FILE_REFERENCE_TIMES) {
TEST_FILE_REFERENCE = '';
}
} */
5 years ago
const oldPhoto = this.photos[photo.id];
if(photo.file_reference) { // * because we can have a new object w/o the file_reference while sending
safeReplaceArrayInObject('file_reference', oldPhoto, photo);
referenceDatabase.saveContext(photo.file_reference, context);
}
if(photo.sizes?.length) {
const size = photo.sizes[photo.sizes.length - 1];
if(size._ === 'photoSizeProgressive') {
size.size = size.sizes[size.sizes.length - 1];
}
}
if(oldPhoto) {
return Object.assign(oldPhoto, photo);
}
5 years ago
return this.photos[photo.id] = photo;
5 years ago
}
public choosePhotoSize(photo: MyPhoto | MyDocument, width = 0, height = 0, useBytes = false) {
if(window.devicePixelRatio > 1) {
5 years ago
width *= 2;
height *= 2;
}
5 years ago
/*
s box 100x100
m box 320x320
x box 800x800
y box 1280x1280
w box 2560x2560
a crop 160x160
b crop 320x320
c crop 640x640
d crop 1280x1280 */
let bestPhotoSize: PhotoSize = {_: 'photoSizeEmpty', type: ''};
const sizes = ((photo as MyPhoto).sizes || (photo as MyDocument).thumbs) as PhotoSize[];
if(sizes?.length) {
for(const photoSize of sizes) {
if(!('w' in photoSize) && !('h' in photoSize)) continue;
bestPhotoSize = photoSize;
const {w, h} = calcImageInBox(photoSize.w, photoSize.h, width, height);
if(w >= width || h >= height) {
break;
}
}
if(useBytes && bestPhotoSize._ === 'photoSizeEmpty' && sizes[0]._ === 'photoStrippedSize') {
bestPhotoSize = sizes[0];
}
}
5 years ago
return bestPhotoSize;
}
public getUserPhotos(userId: number, maxId: string = '0', limit: number = 20) {
const inputUser = appUsersManager.getUserInput(userId);
4 years ago
return apiManager.invokeApiCacheable('photos.getUserPhotos', {
5 years ago
user_id: inputUser,
offset: 0,
limit,
max_id: maxId
4 years ago
}, {cacheSeconds: 60}).then((photosResult) => {
5 years ago
appUsersManager.saveApiUsers(photosResult.users);
const photoIds: string[] = photosResult.photos.map((photo, idx) => {
photosResult.photos[idx] = this.savePhoto(photo, {type: 'profilePhoto', peerId: userId});
return photo.id;
});
5 years ago
return {
count: (photosResult as PhotosPhotos.photosPhotosSlice).count || photosResult.photos.length,
photos: photoIds
5 years ago
};
});
}
public getPreviewURLFromBytes(bytes: Uint8Array | number[], isSticker = false) {
5 years ago
let arr: Uint8Array;
if(!isSticker) {
arr = new Uint8Array(AppPhotosManager.jpegHeader.concat(Array.from(bytes.slice(3)), AppPhotosManager.jpegTail));
5 years ago
arr[164] = bytes[1];
arr[166] = bytes[2];
} else {
arr = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
5 years ago
}
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);
}
/**
* https://core.telegram.org/api/files#vector-thumbnails
*/
public getPathFromPhotoPathSize(size: PhotoSize.photoPathSize) {
const bytes = size.bytes;
const lookup = "AACAAAAHAAALMAAAQASTAVAAAZaacaaaahaaalmaaaqastava.az0123456789-,";
let path = 'M';
for(let i = 0, length = bytes.length; i < length; ++i) {
const num = bytes[i];
if(num >= (128 + 64)) {
path += lookup[num - 128 - 64];
} else {
if(num >= 128) {
path += ',';
} else if(num >= 64) {
path += '-';
}
path += '' + (num & 63);
}
}
path += 'z';
return path;
}
public getPreviewURLFromThumb(photo: MyPhoto | MyDocument, thumb: PhotoSize.photoCachedSize | PhotoSize.photoStrippedSize, isSticker = false) {
const cacheContext = appDownloadManager.getCacheContext(photo, thumb.type);
return cacheContext.url || (cacheContext.url = this.getPreviewURLFromBytes(thumb.bytes, isSticker));
}
public getImageFromStrippedThumb(photo: MyPhoto | MyDocument, thumb: PhotoSize.photoCachedSize | PhotoSize.photoStrippedSize, useBlur: boolean) {
const url = this.getPreviewURLFromThumb(photo, thumb, false);
5 years ago
const image = new Image();
image.classList.add('thumbnail');
4 years ago
const loadPromise = (useBlur ? blur(url) : Promise.resolve(url)).then(url => {
return new Promise<any>((resolve) => {
renderImageFromUrl(image, url, resolve);
});
});
return {image, loadPromise};
5 years ago
}
public setAttachmentSize(photo: MyPhoto | MyDocument, element: HTMLElement | SVGForeignObjectElement, boxWidth: number, boxHeight: number, noZoom = true, hasText?: boolean) {
const photoSize = this.choosePhotoSize(photo, boxWidth, boxHeight);
5 years ago
//console.log('setAttachmentSize', photo, photo.sizes[0].bytes, div);
let width: number;
let height: number;
if(photo._ === 'document') {
width = photo.w || 512;
height = photo.h || 512;
5 years ago
} else {
width = 'w' in photoSize ? photoSize.w : 100;
height = 'h' in photoSize ? photoSize.h : 100;
}
let {w, h} = calcImageInBox(width, height, boxWidth, boxHeight, noZoom);
/* if(hasText) {
w = Math.max(boxWidth, w);
} */
if(element instanceof SVGForeignObjectElement) {
element.setAttributeNS(null, 'width', '' + w);
element.setAttributeNS(null, 'height', '' + h);
//console.log('set dimensions to svg element:', element, w, h);
} else {
element.style.width = w + 'px';
element.style.height = h + 'px';
5 years ago
}
return photoSize;
}
4 years ago
public getStrippedThumbIfNeeded(photo: MyPhoto | MyDocument, useBlur: boolean): ReturnType<AppPhotosManager['getImageFromStrippedThumb']> {
const cacheContext = appDownloadManager.getCacheContext(photo);
if(!cacheContext.downloaded || (photo as MyDocument).type === 'video' || (photo as MyDocument).type === 'gif') {
if(photo._ === 'document' && cacheContext.downloaded) {
return null;
}
const sizes = (photo as MyPhoto).sizes || (photo as MyDocument).thumbs;
const thumb = sizes?.length ? sizes.find(size => size._ === 'photoStrippedSize') : null;
if(thumb && ('bytes' in thumb)) {
return appPhotosManager.getImageFromStrippedThumb(photo, thumb as any, useBlur);
}
}
return null;
}
public getPhotoDownloadOptions(photo: MyPhoto | MyDocument, photoSize: PhotoSize, queueId?: number, onlyCache?: boolean): DownloadOptions {
const isDocument = photo._ === 'document';
5 years ago
if(!photoSize || photoSize._ === 'photoSizeEmpty') {
4 years ago
//console.error('no photoSize by photo:', photo);
throw new Error('photoSizeEmpty!');
}
// maybe it's a thumb
const isPhoto = (photoSize._ === 'photoSize' || photoSize._ === 'photoSizeProgressive') && photo.access_hash && photo.file_reference;
4 years ago
const location: InputFileLocation.inputPhotoFileLocation | InputFileLocation.inputDocumentFileLocation = {
_: isDocument ? 'inputDocumentFileLocation' : 'inputPhotoFileLocation',
id: photo.id,
access_hash: photo.access_hash,
file_reference: photo.file_reference,
thumb_size: photoSize.type
4 years ago
};
return {
dcId: photo.dc_id,
location,
size: isPhoto ? (photoSize as PhotoSize.photoSize).size : undefined,
queueId,
onlyCache
};
}
/* public getPhotoURL(photo: MTPhoto | MTMyDocument, photoSize: MTPhotoSize) {
const downloadOptions = this.getPhotoDownloadOptions(photo, photoSize);
return {url: getFileURL('photo', downloadOptions), location: downloadOptions.location};
} */
/* public isDownloaded(media: any) {
const isPhoto = media._ === 'photo';
const photo = isPhoto ? this.getPhoto(media.id) : null;
let isDownloaded: boolean;
if(photo) {
isDownloaded = photo.downloaded > 0;
} else {
const cachedThumb = this.getDocumentCachedThumb(media.id);
isDownloaded = cachedThumb?.downloaded > 0;
}
return isDownloaded;
} */
public preloadPhoto(photoId: MyPhoto | MyDocument | string, photoSize?: PhotoSize, queueId?: number, onlyCache?: boolean): CancellablePromise<Blob> {
const photo = this.getPhoto(photoId);
// @ts-ignore
if(!photo || photo._ === 'photoEmpty') {
throw new Error('preloadPhoto photoEmpty!');
}
if(!photoSize) {
const fullWidth = this.windowW;
const fullHeight = this.windowH;
photoSize = this.choosePhotoSize(photo, fullWidth, fullHeight);
}
5 years ago
const cacheContext = appDownloadManager.getCacheContext(photo, photoSize.type);
if(cacheContext.downloaded >= ('size' in photoSize ? photoSize.size : 0) && cacheContext.url) {
return Promise.resolve() as any;
}
const downloadOptions = this.getPhotoDownloadOptions(photo, photoSize, queueId, onlyCache);
const fileName = getFileNameByLocation(downloadOptions.location);
let download = appDownloadManager.getDownload(fileName);
if(download) {
return download;
}
download = appDownloadManager.download(downloadOptions);
download.then(blob => {
if(!cacheContext.downloaded || cacheContext.downloaded < blob.size) {
const url = URL.createObjectURL(blob);
cacheContext.downloaded = blob.size;
cacheContext.url = url;
//console.log('wrote photo:', photo, photoSize, cacheContext, blob);
}
return blob;
}).catch(() => {});
5 years ago
return download;
}
5 years ago
public getPhoto(photoId: any/* MyPhoto | string */): MyPhoto {
return isObject(photoId) ? photoId as MyPhoto : this.photos[photoId as any as string];
5 years ago
}
5 years ago
public getInput(photo: MyPhoto): InputMedia.inputMediaPhoto {
return {
_: 'inputMediaPhoto',
id: {
_: 'inputPhoto',
id: photo.id,
access_hash: photo.access_hash,
file_reference: photo.file_reference
},
ttl_seconds: 0
};
}
public savePhotoFile(photo: MyPhoto | MyDocument, queueId?: number) {
const fullPhotoSize = this.choosePhotoSize(photo, 0xFFFF, 0xFFFF);
if(!(fullPhotoSize._ === 'photoSize' || fullPhotoSize._ === 'photoSizeProgressive')) {
return;
}
const downloadOptions = this.getPhotoDownloadOptions(photo, fullPhotoSize, queueId);
downloadOptions.fileName = 'photo' + photo.id + '.jpg';
appDownloadManager.downloadToDisc(downloadOptions, downloadOptions.fileName);
5 years ago
}
}
const appPhotosManager = new AppPhotosManager();
MOUNT_CLASS_TO.appPhotosManager = appPhotosManager;
export default appPhotosManager;