2023-01-06 20:27:29 +01:00
|
|
|
|
/*
|
|
|
|
|
* https://github.com/morethanwords/tweb
|
|
|
|
|
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
|
|
|
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
|
|
|
|
*/
|
|
|
|
|
|
2023-03-01 11:20:49 +01:00
|
|
|
|
import type {LiteModeKey} from '../helpers/liteMode';
|
2023-01-06 20:27:29 +01:00
|
|
|
|
import {CustomEmojiElement, CustomEmojiRendererElement} from '../lib/richTextProcessor/wrapRichText';
|
|
|
|
|
import rootScope from '../lib/rootScope';
|
|
|
|
|
import {IS_SAFARI} from '../environment/userAgent';
|
|
|
|
|
import {MOUNT_CLASS_TO} from '../config/debug';
|
|
|
|
|
import isInDOM from '../helpers/dom/isInDOM';
|
|
|
|
|
import RLottiePlayer from '../lib/rlottie/rlottiePlayer';
|
|
|
|
|
import indexOfAndSplice from '../helpers/array/indexOfAndSplice';
|
|
|
|
|
import forEachReverse from '../helpers/array/forEachReverse';
|
|
|
|
|
import idleController from '../helpers/idleController';
|
|
|
|
|
import appMediaPlaybackController from './appMediaPlaybackController';
|
|
|
|
|
import {fastRaf} from '../helpers/schedulers';
|
2023-01-25 15:21:38 +01:00
|
|
|
|
import {Middleware} from '../helpers/middleware';
|
2023-01-06 20:27:29 +01:00
|
|
|
|
|
|
|
|
|
export type AnimationItemGroup = '' | 'none' | 'chat' | 'lock' |
|
|
|
|
|
'STICKERS-POPUP' | 'emoticons-dropdown' | 'STICKERS-SEARCH' | 'GIFS-SEARCH' |
|
|
|
|
|
`CHAT-MENU-REACTIONS-${number}` | 'INLINE-HELPER' | 'GENERAL-SETTINGS' | 'STICKER-VIEWER' | 'EMOJI' |
|
|
|
|
|
'EMOJI-STATUS' | `chat-${number}`;
|
|
|
|
|
export interface AnimationItem {
|
|
|
|
|
el: HTMLElement,
|
|
|
|
|
group: AnimationItemGroup,
|
|
|
|
|
animation: AnimationItemWrapper,
|
2023-03-01 11:20:49 +01:00
|
|
|
|
liteModeKey?: LiteModeKey,
|
2023-01-25 15:21:38 +01:00
|
|
|
|
controlled?: boolean | Middleware
|
2023-01-06 20:27:29 +01:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export interface AnimationItemWrapper {
|
|
|
|
|
remove: () => void;
|
|
|
|
|
paused: boolean;
|
|
|
|
|
pause: () => any;
|
|
|
|
|
play: () => any;
|
|
|
|
|
autoplay: boolean;
|
2023-03-01 11:20:49 +01:00
|
|
|
|
loop: boolean | number;
|
2023-01-06 20:27:29 +01:00
|
|
|
|
// onVisibilityChange?: (visible: boolean) => boolean;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export class AnimationIntersector {
|
|
|
|
|
private observer: IntersectionObserver;
|
|
|
|
|
private visible: Set<AnimationItem>;
|
|
|
|
|
|
|
|
|
|
private overrideIdleGroups: Set<string>;
|
|
|
|
|
private byGroups: {[group in AnimationItemGroup]?: AnimationItem[]};
|
|
|
|
|
private byPlayer: Map<AnimationItem['animation'], AnimationItem>;
|
|
|
|
|
private lockedGroups: {[group in AnimationItemGroup]?: true};
|
|
|
|
|
private onlyOnePlayableGroup: AnimationItemGroup;
|
|
|
|
|
|
|
|
|
|
private intersectionLockedGroups: {[group in AnimationItemGroup]?: true};
|
|
|
|
|
private videosLocked: boolean;
|
|
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
this.observer = new IntersectionObserver((entries) => {
|
|
|
|
|
// if(rootScope.idle.isIDLE) return;
|
|
|
|
|
|
|
|
|
|
for(const entry of entries) {
|
|
|
|
|
const target = entry.target;
|
|
|
|
|
|
|
|
|
|
for(const group in this.byGroups) {
|
|
|
|
|
if(this.intersectionLockedGroups[group as AnimationItemGroup]) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const animation = this.byGroups[group as AnimationItemGroup].find((p) => p.el === target);
|
|
|
|
|
if(!animation) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(entry.isIntersecting) {
|
|
|
|
|
this.visible.add(animation);
|
|
|
|
|
this.checkAnimation(animation, false);
|
|
|
|
|
|
|
|
|
|
/* if(animation instanceof HTMLVideoElement && animation.dataset.src) {
|
|
|
|
|
animation.src = animation.dataset.src;
|
|
|
|
|
animation.load();
|
|
|
|
|
} */
|
|
|
|
|
} else {
|
|
|
|
|
this.visible.delete(animation);
|
|
|
|
|
this.checkAnimation(animation, true);
|
|
|
|
|
|
|
|
|
|
const _animation = animation.animation;
|
|
|
|
|
if(_animation instanceof RLottiePlayer/* && animation.cachingDelta === 2 */) {
|
|
|
|
|
// console.warn('will clear cache', player);
|
|
|
|
|
_animation.clearCache();
|
|
|
|
|
}/* else if(animation instanceof HTMLVideoElement && animation.src) {
|
|
|
|
|
animation.dataset.src = animation.src;
|
|
|
|
|
animation.src = '';
|
|
|
|
|
animation.load();
|
|
|
|
|
} */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.visible = new Set();
|
|
|
|
|
|
|
|
|
|
this.overrideIdleGroups = new Set();
|
|
|
|
|
this.byGroups = {};
|
|
|
|
|
this.byPlayer = new Map();
|
|
|
|
|
this.lockedGroups = {};
|
|
|
|
|
this.onlyOnePlayableGroup = '';
|
|
|
|
|
|
|
|
|
|
this.intersectionLockedGroups = {};
|
|
|
|
|
this.videosLocked = false;
|
|
|
|
|
|
|
|
|
|
appMediaPlaybackController.addEventListener('play', ({doc}) => {
|
|
|
|
|
if(doc.type === 'round') {
|
|
|
|
|
this.videosLocked = true;
|
|
|
|
|
this.checkAnimations2();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
appMediaPlaybackController.addEventListener('pause', () => {
|
|
|
|
|
if(this.videosLocked) {
|
|
|
|
|
this.videosLocked = false;
|
|
|
|
|
this.checkAnimations2();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
idleController.addEventListener('change', (idle) => {
|
|
|
|
|
this.checkAnimations2(idle);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public setOverrideIdleGroup(group: string, override: boolean) {
|
|
|
|
|
if(override) this.overrideIdleGroups.add(group);
|
|
|
|
|
else this.overrideIdleGroups.delete(group);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public getAnimations(element: HTMLElement) {
|
|
|
|
|
const found: AnimationItem[] = [];
|
|
|
|
|
for(const group in this.byGroups) {
|
|
|
|
|
for(const player of this.byGroups[group as AnimationItemGroup]) {
|
|
|
|
|
if(player.el === element) {
|
|
|
|
|
found.push(player);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return found;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public removeAnimation(player: AnimationItem) {
|
|
|
|
|
const {el, animation} = player;
|
2023-02-03 10:14:08 +01:00
|
|
|
|
if(!(animation instanceof HTMLVideoElement)) {
|
|
|
|
|
animation.remove();
|
|
|
|
|
}
|
2023-01-06 20:27:29 +01:00
|
|
|
|
|
|
|
|
|
if(animation instanceof HTMLVideoElement && IS_SAFARI) {
|
|
|
|
|
setTimeout(() => { // TODO: очистка по очереди, а не все вместе с этим таймаутом
|
|
|
|
|
animation.src = '';
|
|
|
|
|
animation.load();
|
|
|
|
|
}, 1e3);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const group = this.byGroups[player.group];
|
|
|
|
|
if(group) {
|
|
|
|
|
indexOfAndSplice(group, player);
|
|
|
|
|
if(!group.length) {
|
|
|
|
|
delete this.byGroups[player.group];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.observer.unobserve(el);
|
|
|
|
|
this.visible.delete(player);
|
|
|
|
|
this.byPlayer.delete(animation);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public removeAnimationByPlayer(player: AnimationItemWrapper) {
|
|
|
|
|
const item = this.byPlayer.get(player);
|
|
|
|
|
if(item) {
|
|
|
|
|
this.removeAnimation(item);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-01 11:20:49 +01:00
|
|
|
|
public addAnimation(options: {
|
2023-01-06 20:27:29 +01:00
|
|
|
|
animation: AnimationItem['animation'],
|
2023-03-01 11:20:49 +01:00
|
|
|
|
group?: AnimationItemGroup,
|
2023-01-06 20:27:29 +01:00
|
|
|
|
observeElement?: HTMLElement,
|
2023-03-01 11:20:49 +01:00
|
|
|
|
controlled?: AnimationItem['controlled'],
|
|
|
|
|
liteModeKey?: LiteModeKey
|
|
|
|
|
}) {
|
|
|
|
|
let {animation, group = '', observeElement, controlled, liteModeKey} = options;
|
2023-01-06 20:27:29 +01:00
|
|
|
|
if(group === 'none' || this.byPlayer.has(animation)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(!observeElement) {
|
|
|
|
|
if(animation instanceof RLottiePlayer) {
|
|
|
|
|
observeElement = animation.el[0];
|
|
|
|
|
} else if(animation instanceof CustomEmojiRendererElement) {
|
|
|
|
|
observeElement = animation.canvas;
|
|
|
|
|
} else if(animation instanceof CustomEmojiElement) {
|
|
|
|
|
observeElement = animation.placeholder ?? animation;
|
|
|
|
|
} else if(animation instanceof HTMLElement) {
|
|
|
|
|
observeElement = animation;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const item: AnimationItem = {
|
|
|
|
|
el: observeElement,
|
|
|
|
|
animation: animation,
|
|
|
|
|
group,
|
2023-03-01 11:20:49 +01:00
|
|
|
|
controlled,
|
|
|
|
|
liteModeKey
|
2023-01-06 20:27:29 +01:00
|
|
|
|
};
|
|
|
|
|
|
2023-01-25 15:21:38 +01:00
|
|
|
|
if(controlled && typeof(controlled) !== 'boolean') {
|
|
|
|
|
controlled.onClean(() => {
|
|
|
|
|
this.removeAnimationByPlayer(animation);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-06 20:27:29 +01:00
|
|
|
|
if(animation instanceof RLottiePlayer) {
|
|
|
|
|
if(!rootScope.settings.stickers.loop && animation.loop) {
|
|
|
|
|
animation.loop = rootScope.settings.stickers.loop;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
(this.byGroups[group as AnimationItemGroup] ??= []).push(item);
|
|
|
|
|
this.observer.observe(item.el);
|
|
|
|
|
this.byPlayer.set(animation, item);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public checkAnimations(
|
|
|
|
|
blurred?: boolean,
|
|
|
|
|
group?: AnimationItemGroup,
|
|
|
|
|
destroy?: boolean,
|
|
|
|
|
imitateIntersection?: boolean
|
|
|
|
|
) {
|
|
|
|
|
// if(rootScope.idle.isIDLE) return;
|
|
|
|
|
|
|
|
|
|
if(group !== undefined && !this.byGroups[group]) {
|
|
|
|
|
// console.warn('no animation group:', group);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const groups = group !== undefined /* && false */ ? [group] : Object.keys(this.byGroups) as AnimationItemGroup[];
|
|
|
|
|
|
|
|
|
|
for(const group of groups) {
|
|
|
|
|
if(imitateIntersection && this.intersectionLockedGroups[group]) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const animations = this.byGroups[group];
|
|
|
|
|
|
|
|
|
|
forEachReverse(animations, (animation) => {
|
|
|
|
|
this.checkAnimation(animation, blurred, destroy);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public checkAnimations2(blurred?: boolean) {
|
|
|
|
|
this.checkAnimations(blurred, undefined, undefined, true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public checkAnimation(player: AnimationItem, blurred?: boolean, destroy?: boolean) {
|
|
|
|
|
const {el, animation, group} = player;
|
|
|
|
|
// return;
|
|
|
|
|
if(destroy || (!this.lockedGroups[group] && !isInDOM(el))) {
|
|
|
|
|
if(!player.controlled || destroy) {
|
|
|
|
|
this.removeAnimation(player);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(blurred ||
|
|
|
|
|
(this.onlyOnePlayableGroup && this.onlyOnePlayableGroup !== group) ||
|
|
|
|
|
(animation instanceof HTMLVideoElement && this.videosLocked)
|
|
|
|
|
) {
|
|
|
|
|
if(!animation.paused) {
|
|
|
|
|
// console.warn('pause animation:', animation);
|
|
|
|
|
animation.pause();
|
|
|
|
|
}
|
|
|
|
|
} else if(animation.paused &&
|
|
|
|
|
this.visible.has(player) &&
|
|
|
|
|
animation.autoplay &&
|
|
|
|
|
(!this.onlyOnePlayableGroup || this.onlyOnePlayableGroup === group) &&
|
|
|
|
|
(!idleController.isIdle || this.overrideIdleGroups.has(player.group))
|
|
|
|
|
) {
|
|
|
|
|
// console.warn('play animation:', animation);
|
|
|
|
|
animation.play();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public getOnlyOnePlayableGroup() {
|
|
|
|
|
return this.onlyOnePlayableGroup;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public setOnlyOnePlayableGroup(group: AnimationItemGroup = '') {
|
|
|
|
|
this.onlyOnePlayableGroup = group;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public lockGroup(group: AnimationItemGroup) {
|
|
|
|
|
this.lockedGroups[group] = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public unlockGroup(group: AnimationItemGroup) {
|
|
|
|
|
delete this.lockedGroups[group];
|
|
|
|
|
this.checkAnimations(undefined, group);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public refreshGroup(group: AnimationItemGroup) {
|
|
|
|
|
const animations = this.byGroups[group];
|
|
|
|
|
if(!animations?.length) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
animations.forEach((animation) => {
|
|
|
|
|
this.observer.unobserve(animation.el);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
fastRaf(() => {
|
|
|
|
|
animations.forEach((animation) => {
|
|
|
|
|
this.observer.observe(animation.el);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public lockIntersectionGroup(group: AnimationItemGroup) {
|
|
|
|
|
this.intersectionLockedGroups[group] = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public unlockIntersectionGroup(group: AnimationItemGroup) {
|
|
|
|
|
delete this.intersectionLockedGroups[group];
|
|
|
|
|
this.refreshGroup(group);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public toggleIntersectionGroup(group: AnimationItemGroup, lock: boolean) {
|
|
|
|
|
if(lock) this.lockIntersectionGroup(group);
|
|
|
|
|
else this.unlockIntersectionGroup(group);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const animationIntersector = new AnimationIntersector();
|
|
|
|
|
MOUNT_CLASS_TO && (MOUNT_CLASS_TO.animationIntersector = animationIntersector);
|
|
|
|
|
export default animationIntersector;
|