/* * https://github.com/morethanwords/tweb * Copyright (C) 2019-2021 Eduard Kuzmenko * https://github.com/morethanwords/tweb/blob/master/LICENSE */ import IS_TOUCH_SUPPORTED from '../environment/touchSupport'; import cancelEvent from '../helpers/dom/cancelEvent'; import {simulateClickEvent, attachClickEvent} from '../helpers/dom/clickEvent'; import findUpAsChild from '../helpers/dom/findUpAsChild'; import findUpClassName from '../helpers/dom/findUpClassName'; import getVisibleRect from '../helpers/dom/getVisibleRect'; import ListenerSetter from '../helpers/listenerSetter'; import {makeMediaSize} from '../helpers/mediaSize'; import {getMiddleware, Middleware} from '../helpers/middleware'; import {doubleRaf} from '../helpers/schedulers'; import pause from '../helpers/schedulers/pause'; import windowSize from '../helpers/windowSize'; import {DocumentAttribute} from '../layer'; import {MyDocument} from '../lib/appManagers/appDocsManager'; import getStickerEffectThumb from '../lib/appManagers/utils/stickers/getStickerEffectThumb'; import wrapEmojiText from '../lib/richTextProcessor/wrapEmojiText'; import {CustomEmojiElement} from '../lib/richTextProcessor/wrapRichText'; import lottieLoader from '../lib/rlottie/lottieLoader'; import RLottiePlayer from '../lib/rlottie/rlottiePlayer'; import rootScope from '../lib/rootScope'; import animationIntersector, {AnimationItemGroup} from './animationIntersector'; import SetTransition from './singleTransition'; import wrapSticker from './wrappers/sticker'; import {STICKER_EFFECT_MULTIPLIER} from './wrappers/sticker'; let hasViewer = false; export default function attachStickerViewerListeners({listenTo, listenerSetter, selector, findTarget: originalFindTarget}: { listenerSetter: ListenerSetter, listenTo: HTMLElement, selector?: string, findTarget?: (e: MouseEvent) => HTMLElement }) { if(IS_TOUCH_SUPPORTED) { return; } const findTarget = (e: MouseEvent, checkForParent?: boolean) => { let el: HTMLElement; if(originalFindTarget) el = originalFindTarget(e); else { const s = selector || `.media-sticker-wrapper`; el = (e.target as HTMLElement).closest(s) as HTMLElement; } return el && (!checkForParent || findUpAsChild(el, listenTo)) ? el : undefined; }; const managers = rootScope.managers; listenerSetter.add(listenTo)('mousedown', (e) => { if(hasViewer || e.buttons > 1 || e.button !== 0) return; let mediaContainer = findTarget(e); if(!mediaContainer) { return; } // const img: HTMLImageElement = mediaContainer.querySelector('img.media-sticker'); const docId = mediaContainer.dataset.docId; if(!docId) { return; } const className = 'sticker-viewer'; const group: AnimationItemGroup = 'STICKER-VIEWER'; const openDuration = 200; const switchDuration = 200; const previousGroup = animationIntersector.getOnlyOnePlayableGroup(); const _middleware = getMiddleware(); let container: HTMLElement, previousTransformer: HTMLElement; const doThatSticker = async({mediaContainer, doc, middleware, lockGroups, isSwitching}: { mediaContainer: HTMLElement, doc: MyDocument, middleware: Middleware, lockGroups?: boolean, isSwitching?: boolean }) => { const effectThumb = getStickerEffectThumb(doc); const mediaRect: DOMRect = mediaContainer.getBoundingClientRect(); const s = makeMediaSize(doc.w, doc.h); const size = effectThumb ? 280 : 360; const boxSize = makeMediaSize(size, size); const fitted = mediaRect.width === mediaRect.height ? boxSize : s.aspectFitted(boxSize); const bubble = findUpClassName(mediaContainer, 'bubble'); const isOut = bubble ? bubble.classList.contains('is-out') : true; const transformer = document.createElement('div'); transformer.classList.add(className + '-transformer'); const stickerContainer = document.createElement('div'); stickerContainer.classList.add(className + '-sticker'); /* transformer.style.width = */stickerContainer.style.width = fitted.width + 'px'; /* transformer.style.height = */stickerContainer.style.height = fitted.height + 'px'; const stickerEmoji = document.createElement('div'); stickerEmoji.classList.add(className + '-emoji'); stickerEmoji.append(wrapEmojiText(doc.stickerEmojiRaw)); if(effectThumb) { const margin = (size * STICKER_EFFECT_MULTIPLIER - size) / 3 * (isOut ? 1 : -1); transformer.classList.add('has-effect'); // const property = `--margin-${isOut ? 'right' : 'left'}`; // stickerContainer.style.setProperty(property, `${margin * 2}px`); transformer.style.setProperty('--translateX', `${margin}px`); stickerEmoji.style.setProperty('--translateX', `${-margin}px`); } const overflowElement = findUpClassName(mediaContainer, 'scrollable'); const visibleRect = getVisibleRect(mediaContainer, overflowElement, true, mediaRect); if(visibleRect.overflow.vertical || visibleRect.overflow.horizontal) { stickerContainer.classList.add('is-overflow'); } // if(img) { // const ratio = img.naturalWidth / img.naturalHeight; // if((mediaRect.width / mediaRect.height).toFixed(1) !== ratio.toFixed(1)) { // mediaRect = mediaRect.toJSON(); // } // } const rect = mediaContainer.getBoundingClientRect(); const scaleX = rect.width / fitted.width; const scaleY = rect.height / fitted.height; const transformX = rect.left - (windowSize.width - rect.width) / 2; const transformY = rect.top - (windowSize.height - rect.height) / 2; transformer.style.transform = `translate(${transformX}px, ${transformY}px) scale(${scaleX}, ${scaleY})`; if(isSwitching) transformer.classList.add('is-switching'); transformer.append(stickerContainer, stickerEmoji); container.append(transformer); const attribute = doc.attributes.find((attribute) => attribute._ === 'documentAttributeCustomEmoji') as DocumentAttribute.documentAttributeCustomEmoji; const o = await wrapSticker({ doc, div: stickerContainer, group, width: fitted.width, height: fitted.height, play: false, loop: true, middleware, managers, needFadeIn: false, isOut, withThumb: false, relativeEffect: true, loopEffect: true, textColor: attribute && attribute.pFlags.text_color ? 'primary-text-color' : undefined }).then(({render}) => render); if(!middleware()) return; if(!container.parentElement) { document.body.append(container); } const player = Array.isArray(o) ? o[0] : o; const firstFramePromise = player instanceof RLottiePlayer ? new Promise((resolve) => player.addEventListener('firstFrame', resolve, {once: true})) : Promise.resolve(); await Promise.all([firstFramePromise, doubleRaf()]); await pause(0); // ! need it because firstFrame will be called just from the loop if(!middleware()) return; if(lockGroups) { animationIntersector.setOnlyOnePlayableGroup(group); animationIntersector.checkAnimations2(true); } if(player instanceof RLottiePlayer) { const prevPlayer = mediaContainer instanceof CustomEmojiElement ? mediaContainer.player as RLottiePlayer : lottieLoader.getAnimation(mediaContainer); if(prevPlayer) { player.curFrame = prevPlayer.curFrame; player.play(); await new Promise((resolve) => { let i = 0; const c = () => { if(++i === 2) { resolve(); player.removeEventListener('enterFrame', c); } }; player.addEventListener('enterFrame', c); }); if(!middleware()) return; player.pause(); } } else if(player instanceof HTMLVideoElement) { const prevPlayer = mediaContainer.querySelector('video'); if(prevPlayer) { player.currentTime = prevPlayer.currentTime; } } return { ready: () => { if(player instanceof RLottiePlayer || player instanceof HTMLVideoElement) { player.play(); } if(effectThumb) { simulateClickEvent(stickerContainer); } }, transformer }; }; const timeout = window.setTimeout(async() => { document.removeEventListener('mousemove', onMousePreMove); container = document.createElement('div'); container.classList.add(className); hasViewer = true; const middleware = _middleware.get(); const doc = await managers.appDocsManager.getDoc(docId); if(!middleware()) return; let result: Awaited>; try { result = await doThatSticker({ doc, mediaContainer, middleware, lockGroups: true }); if(!result) return; } catch(err) { return; } const {ready, transformer} = result; previousTransformer = transformer; SetTransition({ element: container, className: 'is-visible', forwards: true, duration: openDuration, onTransitionEnd: () => { if(!middleware()) return; ready(); } }); document.addEventListener('mousemove', onMouseMove); }, 125); const onMouseMove = async(e: MouseEvent) => { const newMediaContainer = findTarget(e, true); if(!newMediaContainer || mediaContainer === newMediaContainer) { return; } const docId = newMediaContainer.dataset.docId; if(!docId) { return; } mediaContainer = newMediaContainer; _middleware.clean(); const middleware = _middleware.get(); const doc = await managers.appDocsManager.getDoc(docId); if(!middleware()) return; let r: Awaited>; try { r = await doThatSticker({ doc, mediaContainer, middleware, isSwitching: true }); if(!r) return; } catch(err) { console.error('sticker viewer error', err); return; } const {ready, transformer} = r; const _previousTransformer = previousTransformer; SetTransition({ element: _previousTransformer, className: 'is-switching', forwards: true, duration: switchDuration, onTransitionEnd: () => { _previousTransformer.remove(); } }); previousTransformer = transformer; SetTransition({ element: transformer, className: 'is-switching', forwards: false, duration: switchDuration, onTransitionEnd: () => { if(!middleware()) return; ready(); } }); }; const onMousePreMove = (e: MouseEvent) => { if(!findUpAsChild(e.target as HTMLElement, mediaContainer)) { onMouseUp(); } }; const onMouseUp = () => { clearTimeout(timeout); _middleware.clean(); if(container) { SetTransition({ element: container, className: 'is-visible', forwards: false, duration: openDuration, onTransitionEnd: () => { container.remove(); animationIntersector.setOnlyOnePlayableGroup(previousGroup); animationIntersector.checkAnimations2(false); hasViewer = false; } }); attachClickEvent(document.body, cancelEvent, {capture: true, once: true}); } document.removeEventListener('mousemove', onMousePreMove); document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp, {capture: true}); }; document.addEventListener('mousemove', onMousePreMove); document.addEventListener('mouseup', onMouseUp, {once: true, capture: true}); }); }