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.
241 lines
7.5 KiB
241 lines
7.5 KiB
/* |
|
* https://github.com/morethanwords/tweb |
|
* Copyright (C) 2019-2021 Eduard Kuzmenko |
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE |
|
*/ |
|
|
|
import animationIntersector from '../../components/animationIntersector'; |
|
import {MOUNT_CLASS_TO} from '../../config/debug'; |
|
import pause from '../../helpers/schedulers/pause'; |
|
import {logger, LogTypes} from '../logger'; |
|
import RLottiePlayer, {RLottieOptions} from './rlottiePlayer'; |
|
import QueryableWorker from './queryableWorker'; |
|
import blobConstruct from '../../helpers/blob/blobConstruct'; |
|
import rootScope from '../rootScope'; |
|
import apiManagerProxy from '../mtproto/mtprotoworker'; |
|
|
|
export type LottieAssetName = 'EmptyFolder' | 'Folders_1' | 'Folders_2' | |
|
'TwoFactorSetupMonkeyClose' | 'TwoFactorSetupMonkeyCloseAndPeek' | |
|
'TwoFactorSetupMonkeyCloseAndPeekToIdle' | 'TwoFactorSetupMonkeyIdle' | |
|
'TwoFactorSetupMonkeyPeek' | 'TwoFactorSetupMonkeyTracking' | |
|
'voice_outlined2' | 'voip_filled' | 'voice_mini'; |
|
|
|
export class LottieLoader { |
|
private isWebAssemblySupported = typeof(WebAssembly) !== 'undefined'; |
|
private loadPromise: Promise<void> = !this.isWebAssemblySupported ? Promise.reject() : undefined; |
|
private loaded = false; |
|
|
|
private workersLimit = 4; |
|
private players: {[reqId: number]: RLottiePlayer} = {}; |
|
|
|
private workers: QueryableWorker[] = []; |
|
private curWorkerNum = 0; |
|
|
|
private log = logger('LOTTIE', LogTypes.Error); |
|
|
|
public getAnimation(element: HTMLElement) { |
|
for(const i in this.players) { |
|
if(this.players[i].el === element) { |
|
return this.players[i]; |
|
} |
|
} |
|
|
|
return null; |
|
} |
|
|
|
public setLoop(loop: boolean) { |
|
for(const i in this.players) { |
|
const player = this.players[i]; |
|
player.loop = loop; |
|
player.autoplay = player._autoplay; |
|
} |
|
} |
|
|
|
public loadLottieWorkers() { |
|
if(this.loadPromise) { |
|
return this.loadPromise; |
|
} |
|
|
|
return this.loadPromise = new Promise((resolve, reject) => { |
|
let remain = this.workersLimit; |
|
for(let i = 0; i < this.workersLimit; ++i) { |
|
const worker = new Worker(new URL('./rlottie.worker.ts', import.meta.url)); |
|
const queryableWorker = this.workers[i] = new QueryableWorker(worker); |
|
|
|
queryableWorker.addEventListener('ready', () => { |
|
this.log('worker #' + i + ' ready'); |
|
|
|
queryableWorker.addEventListener('frame', this.onFrame); |
|
queryableWorker.addEventListener('loaded', this.onPlayerLoaded); |
|
queryableWorker.addEventListener('error', this.onPlayerError); |
|
|
|
--remain; |
|
if(!remain) { |
|
this.log('workers ready'); |
|
resolve(); |
|
this.loaded = true; |
|
} |
|
}, {once: true}); |
|
|
|
queryableWorker.addEventListener('workerError', (error) => { |
|
reject('rlottie load error: ' + error.message); |
|
this.loaded = false; |
|
}, {once: true}); |
|
} |
|
}); |
|
} |
|
|
|
public loadAnimationAsAsset(params: Omit<RLottieOptions, 'animationData' | 'name'>, name: LottieAssetName) { |
|
(params as RLottieOptions).name = name; |
|
return this.loadAnimationFromURL(params, 'assets/tgs/' + name + '.json'); |
|
} |
|
|
|
public loadAnimationFromURL(params: Omit<RLottieOptions, 'animationData'>, url: string): Promise<RLottiePlayer> { |
|
if(!this.isWebAssemblySupported) { |
|
return this.loadPromise as any; |
|
} |
|
|
|
if(!this.loaded) { |
|
this.loadLottieWorkers(); |
|
} |
|
|
|
return fetch(url) |
|
.then((res) => { |
|
if(!res.headers || res.headers.get('content-type') === 'application/octet-stream') { |
|
return res.arrayBuffer().then((data) => apiManagerProxy.invokeCrypto('gzipUncompress', data)).then((arr) => blobConstruct(arr as Uint8Array, '')) |
|
} else { |
|
return res.blob(); |
|
} |
|
}) |
|
/* .then((str) => { |
|
return new Promise<string>((resolve) => setTimeout(() => resolve(str), 2e3)); |
|
}) */ |
|
.then((blob) => { |
|
const newParams = Object.assign(params, {animationData: blob, needUpscale: true}); |
|
if(!newParams.name) newParams.name = url; |
|
return this.loadAnimationWorker(newParams); |
|
}); |
|
} |
|
|
|
public waitForFirstFrame(player: RLottiePlayer) { |
|
return Promise.race([ |
|
/* new Promise<void>((resolve) => { |
|
player.addEventListener('firstFrame', () => { |
|
setTimeout(() => resolve(), 1500); |
|
}, true); |
|
}) */ |
|
new Promise<void>((resolve) => { |
|
player.addEventListener('firstFrame', resolve, {once: true}); |
|
}), |
|
pause(2500) |
|
]).then(() => player); |
|
} |
|
|
|
public async loadAnimationWorker(params: RLottieOptions, group = params.group || '', middleware?: () => boolean): Promise<RLottiePlayer> { |
|
if(!this.isWebAssemblySupported) { |
|
return this.loadPromise as any; |
|
} |
|
// params.autoplay = true; |
|
|
|
if(!this.loaded) { |
|
await this.loadLottieWorkers(); |
|
} |
|
|
|
if(middleware && !middleware()) { |
|
throw new Error('middleware'); |
|
} |
|
|
|
if(!params.width || !params.height) { |
|
params.width = parseInt(params.container.style.width); |
|
params.height = parseInt(params.container.style.height); |
|
} |
|
|
|
if(!params.width || !params.height) { |
|
throw new Error('No size for sticker!'); |
|
} |
|
|
|
params.group = group; |
|
|
|
const player = this.initPlayer(params.container, params); |
|
|
|
if(group !== 'none') { |
|
animationIntersector.addAnimation(player, group); |
|
} |
|
|
|
return player; |
|
} |
|
|
|
private onPlayerLoaded = (reqId: number, frameCount: number, fps: number) => { |
|
const rlPlayer = this.players[reqId]; |
|
if(!rlPlayer) { |
|
this.log.warn('onPlayerLoaded on destroyed player:', reqId, frameCount); |
|
return; |
|
} |
|
|
|
this.log.debug('onPlayerLoaded'); |
|
rlPlayer.onLoad(frameCount, fps); |
|
// rlPlayer.addListener('firstFrame', () => { |
|
// animationIntersector.addAnimation(player, group); |
|
// }, true); |
|
}; |
|
|
|
private onFrame = (reqId: number, frameNo: number, frame: Uint8ClampedArray) => { |
|
const rlPlayer = this.players[reqId]; |
|
if(!rlPlayer) { |
|
this.log.warn('onFrame on destroyed player:', reqId, frameNo); |
|
return; |
|
} |
|
|
|
if(rlPlayer.clamped !== undefined) { |
|
rlPlayer.clamped = frame; |
|
} |
|
|
|
rlPlayer.renderFrame(frame, frameNo); |
|
}; |
|
|
|
private onPlayerError = (reqId: number, error: Error) => { |
|
const rlPlayer = this.players[reqId]; |
|
if(rlPlayer) { |
|
// ! will need refactoring later, this is not the best way to remove the animation |
|
const animations = animationIntersector.getAnimations(rlPlayer.el); |
|
animations.forEach((animation) => { |
|
animationIntersector.checkAnimation(animation, true, true); |
|
}); |
|
} |
|
}; |
|
|
|
public onDestroy(reqId: number) { |
|
delete this.players[reqId]; |
|
} |
|
|
|
public destroyWorkers() { |
|
this.workers.forEach((worker, idx) => { |
|
worker.terminate(); |
|
this.log('worker #' + idx + ' terminated'); |
|
}); |
|
|
|
this.log('workers destroyed'); |
|
this.workers.length = 0; |
|
} |
|
|
|
private initPlayer(el: HTMLElement, options: RLottieOptions) { |
|
const rlPlayer = new RLottiePlayer({ |
|
el, |
|
worker: this.workers[this.curWorkerNum++], |
|
options |
|
}); |
|
|
|
this.players[rlPlayer.reqId] = rlPlayer; |
|
if(this.curWorkerNum >= this.workers.length) { |
|
this.curWorkerNum = 0; |
|
} |
|
|
|
rlPlayer.loadFromData(options.animationData); |
|
|
|
return rlPlayer; |
|
} |
|
} |
|
|
|
const lottieLoader = new LottieLoader(); |
|
MOUNT_CLASS_TO.lottieLoader = lottieLoader; |
|
export default lottieLoader;
|
|
|