/* * https://github.com/morethanwords/tweb * Copyright (C) 2019-2021 Eduard Kuzmenko * https://github.com/morethanwords/tweb/blob/master/LICENSE */ import type Chat from './chat/chat'; import { getEmojiToneIndex } from '../vendor/emoji'; import { readBlobAsText } from '../helpers/blob'; import { deferredPromise } from '../helpers/cancellablePromise'; import { formatFullSentTime } from '../helpers/date'; import mediaSizes, { ScreenSize } from '../helpers/mediaSizes'; import { formatBytes } from '../helpers/number'; import { IS_SAFARI } from '../environment/userAgent'; import { Message, PhotoSize, StickerSet } from '../layer'; import appDocsManager, { MyDocument } from "../lib/appManagers/appDocsManager"; import appMessagesManager from '../lib/appManagers/appMessagesManager'; import appPhotosManager, { MyPhoto } from '../lib/appManagers/appPhotosManager'; import LottieLoader from '../lib/lottieLoader'; import webpWorkerController from '../lib/webp/webpWorkerController'; import animationIntersector from './animationIntersector'; import appMediaPlaybackController, { MediaSearchContext } from './appMediaPlaybackController'; import AudioElement, { findAudioTargets as findMediaTargets } from './audio'; import ReplyContainer from './chat/replyContainer'; import { Layouter, RectPart } from './groupedLayout'; import LazyLoadQueue from './lazyLoadQueue'; import PollElement from './poll'; import ProgressivePreloader from './preloader'; import './middleEllipsis'; import RichTextProcessor from '../lib/richtextprocessor'; import appImManager from '../lib/appManagers/appImManager'; import { SearchSuperContext } from './appSearchSuper.'; import rootScope from '../lib/rootScope'; import { onMediaLoad } from '../helpers/files'; import { animateSingle } from '../helpers/animation'; import renderImageFromUrl from '../helpers/dom/renderImageFromUrl'; import sequentialDom from '../helpers/sequentialDom'; import { fastRaf } from '../helpers/schedulers'; import appDownloadManager, { DownloadBlob, ThumbCache } from '../lib/appManagers/appDownloadManager'; import appStickersManager from '../lib/appManagers/appStickersManager'; import { cancelEvent } from '../helpers/dom/cancelEvent'; import { attachClickEvent, simulateClickEvent } from '../helpers/dom/clickEvent'; import isInDOM from '../helpers/dom/isInDOM'; import lottieLoader from '../lib/lottieLoader'; import { clearBadCharsAndTrim } from '../helpers/cleanSearchText'; import blur from '../helpers/blur'; import IS_WEBP_SUPPORTED from '../environment/webpSupport'; import MEDIA_MIME_TYPES_SUPPORTED from '../environment/mediaMimeTypesSupport'; import { MiddleEllipsisElement } from './middleEllipsis'; import { joinElementsWith } from '../lib/langPack'; import throttleWithRaf from '../helpers/schedulers/throttleWithRaf'; const MAX_VIDEO_AUTOPLAY_SIZE = 50 * 1024 * 1024; // 50 MB let roundVideoCircumference = 0; mediaSizes.addEventListener('changeScreen', (from, to) => { if(to === ScreenSize.mobile || from === ScreenSize.mobile) { const elements = Array.from(document.querySelectorAll('.media-round .progress-ring')) as SVGSVGElement[]; const width = mediaSizes.active.round.width; const halfSize = width / 2; const radius = halfSize - 7; roundVideoCircumference = 2 * Math.PI * radius; elements.forEach(element => { element.setAttributeNS(null, 'width', '' + width); element.setAttributeNS(null, 'height', '' + width); const circle = element.firstElementChild as SVGCircleElement; circle.setAttributeNS(null, 'cx', '' + halfSize); circle.setAttributeNS(null, 'cy', '' + halfSize); circle.setAttributeNS(null, 'r', '' + radius); circle.style.strokeDasharray = roundVideoCircumference + ' ' + roundVideoCircumference; circle.style.strokeDashoffset = '' + roundVideoCircumference; }); } }); export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTail, isOut, middleware, lazyLoadQueue, noInfo, group, onlyPreview, withoutPreloader, loadPromises, noPlayButton, noAutoDownload, size, searchContext}: { doc: MyDocument, container?: HTMLElement, message?: Message.message, boxWidth?: number, boxHeight?: number, withTail?: boolean, isOut?: boolean, middleware?: () => boolean, lazyLoadQueue?: LazyLoadQueue, noInfo?: true, noPlayButton?: boolean, group?: string, onlyPreview?: boolean, withoutPreloader?: boolean, loadPromises?: Promise[], noAutoDownload?: boolean, size?: PhotoSize, searchContext?: MediaSearchContext, }) { const isAlbumItem = !(boxWidth && boxHeight); const canAutoplay = (doc.type !== 'video' || (doc.size <= MAX_VIDEO_AUTOPLAY_SIZE && !isAlbumItem)) && (doc.type === 'gif' ? rootScope.settings.autoPlay.gifs : rootScope.settings.autoPlay.videos); let spanTime: HTMLElement, spanPlay: HTMLElement; if(!noInfo) { spanTime = document.createElement('span'); spanTime.classList.add('video-time'); container.append(spanTime); let needPlayButton = false; if(doc.type !== 'gif') { spanTime.innerText = (doc.duration + '').toHHMMSS(false); if(!noPlayButton && doc.type !== 'round') { if(canAutoplay && !noAutoDownload) { spanTime.classList.add('tgico', 'can-autoplay'); } else { needPlayButton = true; } } } else { spanTime.innerText = 'GIF'; if(!canAutoplay && !noPlayButton) { needPlayButton = true; noAutoDownload = undefined; } } if(needPlayButton) { spanPlay = document.createElement('span'); spanPlay.classList.add('video-play', 'tgico-largeplay', 'btn-circle', 'position-center'); container.append(spanPlay); } } let res: { thumb?: typeof photoRes, loadPromise: Promise } = {} as any; if(doc.mime_type === 'image/gif') { const photoRes = wrapPhoto({ photo: doc, message, container, boxWidth, boxHeight, withTail, isOut, lazyLoadQueue, middleware, withoutPreloader, loadPromises, noAutoDownload, size }); res.thumb = photoRes; res.loadPromise = photoRes.loadPromises.full; return res; } /* const video = doc.type === 'round' ? appMediaPlaybackController.addMedia(doc, message.mid) as HTMLVideoElement : document.createElement('video'); if(video.parentElement) { video.remove(); } */ const video = document.createElement('video'); video.classList.add('media-video'); video.setAttribute('playsinline', 'true'); video.muted = true; if(doc.type === 'round') { const divRound = document.createElement('div'); divRound.classList.add('media-round', 'z-depth-1'); divRound.dataset.mid = '' + message.mid; divRound.dataset.peerId = '' + message.peerId; (divRound as any).message = message; const size = mediaSizes.active.round; const halfSize = size.width / 2; const strokeWidth = 3.5; const radius = halfSize - (strokeWidth * 2); divRound.innerHTML = ` `; const circle = divRound.firstElementChild.firstElementChild as SVGCircleElement; if(!roundVideoCircumference) { roundVideoCircumference = 2 * Math.PI * radius; } circle.style.strokeDasharray = roundVideoCircumference + ' ' + roundVideoCircumference; circle.style.strokeDashoffset = '' + roundVideoCircumference; spanTime.classList.add('tgico'); const isUnread = message.pFlags.media_unread; if(isUnread) { divRound.classList.add('is-unread'); } const canvas = document.createElement('canvas'); canvas.width = canvas.height = doc.w/* * window.devicePixelRatio */; divRound.prepend(canvas, spanTime); divRound.append(video); container.append(divRound); const ctx = canvas.getContext('2d'); /* ctx.beginPath(); ctx.arc(canvas.width / 2, canvas.height / 2, canvas.width / 2, 0, Math.PI * 2); ctx.clip(); */ const onLoad = () => { const message: Message.message = (divRound as any).message; const globalVideo = appMediaPlaybackController.addMedia(message, !noAutoDownload) as HTMLVideoElement; const clear = () => { (appImManager.chat.setPeerPromise || Promise.resolve()).finally(() => { if(isInDOM(globalVideo)) { return; } globalVideo.removeEventListener('play', onPlay); globalVideo.removeEventListener('timeupdate', throttledTimeUpdate); globalVideo.removeEventListener('pause', onPaused); globalVideo.removeEventListener('ended', onEnded); }); }; const onFrame = () => { ctx.drawImage(globalVideo, 0, 0); const offset = roundVideoCircumference - globalVideo.currentTime / globalVideo.duration * roundVideoCircumference; circle.style.strokeDashoffset = '' + offset; return !globalVideo.paused; }; const onTimeUpdate = () => { if(!globalVideo.duration) { return; } if(!isInDOM(globalVideo)) { clear(); return; } if(globalVideo.paused) { onFrame(); } spanTime.innerText = (globalVideo.duration - globalVideo.currentTime + '').toHHMMSS(false); }; const throttledTimeUpdate = throttleWithRaf(onTimeUpdate); const onPlay = () => { video.classList.add('hide'); divRound.classList.remove('is-paused'); animateSingle(onFrame, canvas); if(preloader && preloader.preloader && preloader.preloader.classList.contains('manual')) { preloader.onClick(); } }; const onPaused = () => { if(!isInDOM(globalVideo)) { clear(); return; } divRound.classList.add('is-paused'); }; const onEnded = () => { video.classList.remove('hide'); divRound.classList.add('is-paused'); video.currentTime = 0; spanTime.innerText = ('' + globalVideo.duration).toHHMMSS(false); if(globalVideo.currentTime) { globalVideo.currentTime = 0; } }; globalVideo.addEventListener('play', onPlay); globalVideo.addEventListener('timeupdate', throttledTimeUpdate); globalVideo.addEventListener('pause', onPaused); globalVideo.addEventListener('ended', onEnded); attachClickEvent(canvas, (e) => { cancelEvent(e); // ! костыль if(preloader && !preloader.detached) { preloader.onClick(); } // ! can't use it here. on Safari iOS video won't start. /* if(globalVideo.readyState < 2) { return; } */ if(globalVideo.paused) { if(appMediaPlaybackController.setSearchContext(searchContext)) { const [prev, next] = findMediaTargets(divRound, searchContext.useSearch); appMediaPlaybackController.setTargets({peerId: message.peerId, mid: message.mid}, prev, next); } globalVideo.play(); } else { globalVideo.pause(); } }); if(globalVideo.paused) { if(globalVideo.duration && globalVideo.currentTime !== globalVideo.duration && globalVideo.currentTime > 0) { onFrame(); onTimeUpdate(); video.classList.add('hide'); } else { onPaused(); } } else { onPlay(); } }; if(message.pFlags.is_outgoing) { (divRound as any).onLoad = onLoad; divRound.dataset.isOutgoing = '1'; } else { onLoad(); } } else { video.autoplay = true; // для safari } let photoRes: ReturnType; if(message) { photoRes = wrapPhoto({ photo: doc, message, container, boxWidth, boxHeight, withTail, isOut, lazyLoadQueue, middleware, withoutPreloader: true, loadPromises, noAutoDownload, size }); res.thumb = photoRes; if((!canAutoplay && doc.type !== 'gif') || onlyPreview) { res.loadPromise = photoRes.loadPromises.full; return res; } if(withTail) { const foreignObject = (photoRes.images.thumb || photoRes.images.full).parentElement; video.width = +foreignObject.getAttributeNS(null, 'width'); video.height = +foreignObject.getAttributeNS(null, 'height'); foreignObject.append(video); } } else { // * gifs masonry const gotThumb = appDocsManager.getThumb(doc, false); if(gotThumb) { gotThumb.promise.then(() => { video.poster = gotThumb.cacheContext.url; }); } } if(!video.parentElement && container) { (photoRes?.aspecter || container).append(video); } const cacheContext = appDownloadManager.getCacheContext(doc); const isUpload = !!(message?.media as any)?.preloader; let preloader: ProgressivePreloader; if(isUpload) { // means upload preloader = (message.media as any).preloader as ProgressivePreloader; preloader.attach(container, false); noAutoDownload = undefined; } else if(!cacheContext.downloaded && !doc.supportsStreaming) { preloader = new ProgressivePreloader({ attachMethod: 'prepend' }); } else if(doc.supportsStreaming) { preloader = new ProgressivePreloader({ cancelable: false, attachMethod: 'prepend' }); } const renderDeferred = deferredPromise(); video.addEventListener('error', (e) => { if(video.error.code !== 4) { console.error("Error " + video.error.code + "; details: " + video.error.message); } if(preloader && !isUpload) { preloader.detach(); } if(!renderDeferred.isFulfilled) { renderDeferred.resolve(); } }, {once: true}); onMediaLoad(video).then(() => { if(group) { animationIntersector.addAnimation(video, group); } if(preloader && !isUpload) { preloader.detach(); } renderDeferred.resolve(); }); if(doc.type === 'video') { video.addEventListener('timeupdate', () => { spanTime.innerText = (video.duration - video.currentTime + '').toHHMMSS(false); }); } video.muted = true; video.loop = true; //video.play(); video.autoplay = true; let loadPhotoThumbFunc = noAutoDownload && photoRes?.preloader?.loadFunc; const load = () => { if(preloader && noAutoDownload && !withoutPreloader) { preloader.construct(); preloader.setManual(); } let loadPromise: Promise = Promise.resolve(); if(preloader && !isUpload) { if(!cacheContext.downloaded && !doc.supportsStreaming) { const promise = loadPromise = appDocsManager.downloadDoc(doc, lazyLoadQueue?.queueId, noAutoDownload); preloader.attach(container, false, promise); } else if(doc.supportsStreaming) { if(noAutoDownload) { loadPromise = Promise.reject(); } else if(!cacheContext.downloaded) { // * check for uploading video preloader.attach(container, false, null); video.addEventListener(IS_SAFARI ? 'timeupdate' : 'canplay', () => { preloader.detach(); }, {once: true}); } } } if(!noAutoDownload && loadPhotoThumbFunc) { loadPhotoThumbFunc(); loadPhotoThumbFunc = null; } noAutoDownload = undefined; loadPromise.then(() => { if(middleware && !middleware()) { renderDeferred.resolve(); return; } if(doc.type === 'round') { appMediaPlaybackController.resolveWaitingForLoadMedia(message.peerId, message.mid, message.pFlags.is_scheduled); } renderImageFromUrl(video, cacheContext.url); }, () => {}); return {download: loadPromise, render: renderDeferred}; }; if(preloader && !isUpload) { preloader.setDownloadFunction(load); } /* if(doc.size >= 20e6 && !doc.downloaded) { let downloadDiv = document.createElement('div'); downloadDiv.classList.add('download'); let span = document.createElement('span'); span.classList.add('btn-circle', 'tgico-download'); downloadDiv.append(span); downloadDiv.addEventListener('click', () => { downloadDiv.remove(); loadVideo(); }); container.prepend(downloadDiv); return; } */ if(doc.type === 'gif' && !canAutoplay) { attachClickEvent(container, (e) => { cancelEvent(e); spanPlay.remove(); load(); }, {capture: true, once: true}); } else { res.loadPromise = !lazyLoadQueue ? load().render : (lazyLoadQueue.push({div: container, load: () => load().render}), Promise.resolve()); } return res; } rootScope.addEventListener('download_start', (docId) => { const elements = Array.from(document.querySelectorAll(`.document[data-doc-id="${docId}"]`)) as HTMLElement[]; elements.forEach(element => { if(element.querySelector('.preloader-container.manual')) { simulateClickEvent(element); } }); }); export function wrapDocument({message, withTime, fontWeight, voiceAsMusic, showSender, searchContext, loadPromises, noAutoDownload, lazyLoadQueue}: { message: any, withTime?: boolean, fontWeight?: number, voiceAsMusic?: boolean, showSender?: boolean, searchContext?: MediaSearchContext, loadPromises?: Promise[], noAutoDownload?: boolean, lazyLoadQueue?: LazyLoadQueue }): HTMLElement { if(!fontWeight) fontWeight = 500; const doc = (message.media.document || message.media.webpage.document) as MyDocument; const uploading = message.pFlags.is_outgoing && message.media?.preloader; if(doc.type === 'audio' || doc.type === 'voice' || doc.type === 'round') { const audioElement = new AudioElement(); audioElement.withTime = withTime; audioElement.message = message; audioElement.noAutoDownload = noAutoDownload; audioElement.lazyLoadQueue = lazyLoadQueue; audioElement.loadPromises = loadPromises; if(voiceAsMusic) audioElement.voiceAsMusic = voiceAsMusic; if(searchContext) audioElement.searchContext = searchContext; if(showSender) audioElement.showSender = showSender; if(uploading) audioElement.preloader = message.media.preloader; audioElement.dataset.fontWeight = '' + fontWeight; audioElement.render(); return audioElement; } let extSplitted = doc.file_name ? doc.file_name.split('.') : ''; let ext = ''; ext = extSplitted.length > 1 && Array.isArray(extSplitted) ? clearBadCharsAndTrim(extSplitted.pop().split(' ', 1)[0].toLowerCase()) : 'file'; let docDiv = document.createElement('div'); docDiv.classList.add('document', `ext-${ext}`); docDiv.dataset.docId = '' + doc.id; const icoDiv = document.createElement('div'); icoDiv.classList.add('document-ico'); const cacheContext = appDownloadManager.getCacheContext(doc); if((doc.thumbs?.length || (message.pFlags.is_outgoing && cacheContext.url && doc.type === 'photo')) && doc.mime_type !== 'image/gif') { docDiv.classList.add('document-with-thumb'); let imgs: HTMLImageElement[] = []; if(uploading) { icoDiv.innerHTML = ``; imgs.push(icoDiv.firstElementChild as HTMLImageElement); } else { const wrapped = wrapPhoto({ photo: doc, message: null, container: icoDiv, boxWidth: 54, boxHeight: 54, loadPromises, withoutPreloader: true, lazyLoadQueue }); icoDiv.style.width = icoDiv.style.height = ''; if(wrapped.images.thumb) imgs.push(wrapped.images.thumb); if(wrapped.images.full) imgs.push(wrapped.images.full); } imgs.forEach(img => img.classList.add('document-thumb')); } else { icoDiv.innerText = ext; } //let fileName = stringMiddleOverflow(doc.file_name || 'Unknown.file', 26); let fileName = doc.fileName || 'Unknown.file'; const descriptionEl = document.createElement('div'); descriptionEl.classList.add('document-description'); const descriptionParts: (HTMLElement | string | DocumentFragment)[] = [formatBytes(doc.size)]; if(withTime) { descriptionParts.push(formatFullSentTime(message.date)); } if(showSender) { descriptionParts.push(appMessagesManager.wrapSenderToPeer(message)); } docDiv.innerHTML = ` ${cacheContext.downloaded && !uploading ? '' : `
`}
`; const nameDiv = docDiv.querySelector('.document-name') as HTMLElement; const middleEllipsisEl = new MiddleEllipsisElement(); middleEllipsisEl.dataset.fontWeight = '' + fontWeight; middleEllipsisEl.innerHTML = fileName; nameDiv.append(middleEllipsisEl); if(showSender) { nameDiv.append(appMessagesManager.wrapSentTime(message)); } const sizeDiv = docDiv.querySelector('.document-size') as HTMLElement; sizeDiv.append(...joinElementsWith(descriptionParts, ' · ')); docDiv.prepend(icoDiv); /* if(!uploading && message.pFlags.is_outgoing) { return docDiv; } */ let downloadDiv: HTMLElement, preloader: ProgressivePreloader = null; const onLoad = () => { if(downloadDiv) { downloadDiv.classList.add('downloaded'); const _downloadDiv = downloadDiv; setTimeout(() => { _downloadDiv.remove(); }, 200); downloadDiv = null; } if(preloader) { preloader = null; } }; const load = (e: Event) => { const save = !e || e.isTrusted; const doc = appDocsManager.getDoc(docDiv.dataset.docId); let download: DownloadBlob; const queueId = appImManager.chat.bubbles ? appImManager.chat.bubbles.lazyLoadQueue.queueId : undefined; if(!save) { download = appDocsManager.downloadDoc(doc, queueId); } else if(doc.type === 'pdf') { download = appDocsManager.downloadDoc(doc, queueId); download.then(() => { setTimeout(() => { // wait for preloader animation end const url = appDownloadManager.getCacheContext(doc).url; window.open(url); }, rootScope.settings.animationsEnabled ? 250 : 0); }); } else if(MEDIA_MIME_TYPES_SUPPORTED.has(doc.mime_type) && doc.thumbs?.length) { download = appDocsManager.downloadDoc(doc, queueId); } else { download = appDocsManager.saveDocFile(doc, queueId); } if(downloadDiv) { download.then(onLoad); preloader.attach(downloadDiv, true, download); } return {download}; }; if(appDocsManager.downloading.has(doc.id)) { downloadDiv = docDiv.querySelector('.document-download'); preloader = new ProgressivePreloader(); preloader.attach(downloadDiv, false, appDocsManager.downloading.get(doc.id)); } else if(!(cacheContext.downloaded && !uploading)) { downloadDiv = docDiv.querySelector('.document-download'); preloader = message.media.preloader as ProgressivePreloader; if(!preloader) { preloader = new ProgressivePreloader(); preloader.construct(); preloader.setManual(); preloader.attach(downloadDiv); preloader.setDownloadFunction(load); } else { preloader.attach(downloadDiv); message.media.promise.then(onLoad); } } attachClickEvent(docDiv, (e) => { if(preloader) { preloader.onClick(e); } else { load(e); } }); return docDiv; } /* function wrapMediaWithTail(photo: MyPhoto | MyDocument, message: {mid: number, message: string}, container: HTMLElement, boxWidth: number, boxHeight: number, isOut: boolean) { const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.classList.add('bubble__media-container', isOut ? 'is-out' : 'is-in'); const foreignObject = document.createElementNS("http://www.w3.org/2000/svg", 'foreignObject'); const gotThumb = appPhotosManager.getStrippedThumbIfNeeded(photo, true); if(gotThumb) { foreignObject.append(gotThumb.image); } appPhotosManager.setAttachmentSize(photo, foreignObject, boxWidth, boxHeight); const width = +foreignObject.getAttributeNS(null, 'width'); const height = +foreignObject.getAttributeNS(null, 'height'); svg.setAttributeNS(null, 'width', '' + width); svg.setAttributeNS(null, 'height', '' + height); svg.setAttributeNS(null, 'viewBox', '0 0 ' + width + ' ' + height); svg.setAttributeNS(null, 'preserveAspectRatio', 'none'); const clipId = 'clip' + message.mid + '_' + nextRandomInt(9999); svg.dataset.clipId = clipId; const defs = document.createElementNS("http://www.w3.org/2000/svg", 'defs'); let clipPathHTML: string = ''; if(message.message) { //clipPathHTML += ``; } else { if(isOut) { clipPathHTML += ` `; } else { clipPathHTML += ` `; } } defs.innerHTML = `${clipPathHTML}`; container.style.width = parseInt(container.style.width) - 9 + 'px'; container.classList.add('with-tail'); svg.append(defs, foreignObject); container.append(svg); let img = foreignObject.firstElementChild as HTMLImageElement; if(!img) { foreignObject.append(img = new Image()); } return img; } */ export function wrapPhoto({photo, message, container, boxWidth, boxHeight, withTail, isOut, lazyLoadQueue, middleware, size, withoutPreloader, loadPromises, noAutoDownload, noBlur, noThumb, noFadeIn, blurAfter}: { photo: MyPhoto | MyDocument, message?: any, container: HTMLElement, boxWidth?: number, boxHeight?: number, withTail?: boolean, isOut?: boolean, lazyLoadQueue?: LazyLoadQueue, middleware?: () => boolean, size?: PhotoSize, withoutPreloader?: boolean, loadPromises?: Promise[], noAutoDownload?: boolean, noBlur?: boolean, noThumb?: boolean, noFadeIn?: boolean, blurAfter?: boolean, }) { if(!((photo as MyPhoto).sizes || (photo as MyDocument).thumbs)) { if(boxWidth && boxHeight && !size && photo._ === 'document') { appPhotosManager.setAttachmentSize(photo, container, boxWidth, boxHeight, undefined, message); } return { loadPromises: { thumb: Promise.resolve(), full: Promise.resolve() }, images: { thumb: null, full: null }, preloader: null, aspecter: null }; } if(!size) { if(boxWidth === undefined) boxWidth = mediaSizes.active.regular.width; if(boxHeight === undefined) boxHeight = mediaSizes.active.regular.height; } container.classList.add('media-container'); let aspecter = container; let isFit = true; let loadThumbPromise: Promise = Promise.resolve(); let thumbImage: HTMLImageElement; let image: HTMLImageElement; let cacheContext: ThumbCache; // if(withTail) { // image = wrapMediaWithTail(photo, message, container, boxWidth, boxHeight, isOut); // } else { image = new Image(); if(boxWidth && boxHeight && !size) { // !album const set = appPhotosManager.setAttachmentSize(photo, container, boxWidth, boxHeight, undefined, message); size = set.photoSize; isFit = set.isFit; cacheContext = appDownloadManager.getCacheContext(photo, size.type); if(!isFit) { aspecter = document.createElement('div'); aspecter.classList.add('media-container-aspecter'); aspecter.style.width = set.size.width + 'px'; aspecter.style.height = set.size.height + 'px'; const gotThumb = appPhotosManager.getStrippedThumbIfNeeded(photo, cacheContext, !noBlur, true); if(gotThumb) { loadThumbPromise = gotThumb.loadPromise; const thumbImage = gotThumb.image; // local scope thumbImage.classList.add('media-photo'); container.append(thumbImage); } else { const res = wrapPhoto({ container, message, photo, boxWidth: 0, boxHeight: 0, size, lazyLoadQueue, isOut, loadPromises, middleware, withoutPreloader, withTail, noAutoDownload, noBlur, noThumb: true, blurAfter: true //noFadeIn: true }); const thumbImage = res.images.full; thumbImage.classList.add('media-photo', 'thumbnail'); //container.append(thumbImage); } container.classList.add('media-container-fitted'); container.append(aspecter); } } else { if(!size) { size = appPhotosManager.choosePhotoSize(photo, boxWidth, boxHeight, true); } cacheContext = appDownloadManager.getCacheContext(photo, size?.type); } if(!noThumb) { const gotThumb = appPhotosManager.getStrippedThumbIfNeeded(photo, cacheContext, !noBlur); if(gotThumb) { loadThumbPromise = Promise.all([loadThumbPromise, gotThumb.loadPromise]); thumbImage = gotThumb.image; thumbImage.classList.add('media-photo'); aspecter.append(thumbImage); } } // } image.classList.add('media-photo'); //console.log('wrapPhoto downloaded:', photo, photo.downloaded, container); const needFadeIn = (thumbImage || !cacheContext.downloaded) && rootScope.settings.animationsEnabled && !noFadeIn; let preloader: ProgressivePreloader; if(message?.media?.preloader && !withoutPreloader) { // means upload preloader = message.media.preloader; preloader.attach(container); noAutoDownload = undefined; } else if(!cacheContext.downloaded) { preloader = new ProgressivePreloader({ attachMethod: 'prepend' }); } const getDownloadPromise = () => { const promise = photo._ === 'document' && photo.mime_type === 'image/gif' ? appDocsManager.downloadDoc(photo, /* undefined, */lazyLoadQueue?.queueId) : appPhotosManager.preloadPhoto(photo, size, lazyLoadQueue?.queueId, noAutoDownload); return promise; }; const renderOnLoad = (url: string) => { return renderImageWithFadeIn(container, image, url, needFadeIn, aspecter, thumbImage); }; const onLoad = (): Promise => { if(middleware && !middleware()) return Promise.resolve(); if(blurAfter) { return blur(cacheContext.url, 12).then(url => { return renderOnLoad(url); }); } return renderOnLoad(cacheContext.url); }; let loadPromise: Promise; const canAttachPreloader = ( (size as PhotoSize.photoSize).w >= 150 && (size as PhotoSize.photoSize).h >= 150 ) || noAutoDownload; const load = () => { if(noAutoDownload && !withoutPreloader && preloader) { preloader.construct(); preloader.setManual(); } const promise = getDownloadPromise(); if(preloader && !cacheContext.downloaded && !withoutPreloader && canAttachPreloader ) { preloader.attach(container, false, promise); } noAutoDownload = undefined; const renderPromise = promise.then(onLoad); renderPromise.catch(() => {}); return {download: promise, render: renderPromise}; }; if(preloader) { preloader.setDownloadFunction(load); } if(cacheContext.downloaded) { loadThumbPromise = loadPromise = load().render; } else { if(!lazyLoadQueue) loadPromise = load().render; /* else if(noAutoDownload) { preloader.construct(); preloader.setManual(); preloader.attach(container); } */ else lazyLoadQueue.push({div: container, load: () => load().download}); } if(loadPromises && loadThumbPromise) { loadPromises.push(loadThumbPromise); } return { loadPromises: { thumb: loadThumbPromise, full: loadPromise || Promise.resolve() }, images: { thumb: thumbImage, full: image }, preloader, aspecter }; } export function renderImageWithFadeIn(container: HTMLElement, image: HTMLImageElement, url: string, needFadeIn: boolean, aspecter = container, thumbImage?: HTMLImageElement ) { if(needFadeIn) { image.classList.add('fade-in'); } return new Promise((resolve) => { /* if(photo._ === 'document') { console.error('wrapPhoto: will render document', photo, size, cacheContext); return resolve(); } */ renderImageFromUrl(image, url, () => { sequentialDom.mutateElement(container, () => { aspecter.append(image); fastRaf(() => { resolve(); }); if(needFadeIn) { image.addEventListener('animationend', () => { sequentialDom.mutate(() => { image.classList.remove('fade-in'); if(thumbImage) { thumbImage.remove(); } }); }, {once: true}); } }); }); }); } // export function renderImageWithFadeIn(container: HTMLElement, // image: HTMLImageElement, // url: string, // needFadeIn: boolean, // aspecter = container, // thumbImage?: HTMLImageElement // ) { // if(needFadeIn) { // // image.classList.add('fade-in-new', 'not-yet'); // image.classList.add('fade-in'); // } // return new Promise((resolve) => { // /* if(photo._ === 'document') { // console.error('wrapPhoto: will render document', photo, size, cacheContext); // return resolve(); // } */ // renderImageFromUrl(image, url, () => { // sequentialDom.mutateElement(container, () => { // aspecter.append(image); // // (needFadeIn ? getHeavyAnimationPromise() : Promise.resolve()).then(() => { // // fastRaf(() => { // resolve(); // // }); // if(needFadeIn) { // fastRaf(() => { // /* if(!image.isConnected) { // alert('aaaa'); // } */ // // fastRaf(() => { // image.classList.remove('not-yet'); // // }); // }); // image.addEventListener('transitionend', () => { // sequentialDom.mutate(() => { // image.classList.remove('fade-in-new'); // if(thumbImage) { // thumbImage.remove(); // } // }); // }, {once: true}); // } // // }); // }); // }); // }); // } export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, onlyThumb, emoji, width, height, withThumb, loop, loadPromises, needFadeIn}: { doc: MyDocument, div: HTMLElement, middleware?: () => boolean, lazyLoadQueue?: LazyLoadQueue, group?: string, play?: boolean, onlyThumb?: boolean, emoji?: string, width?: number, height?: number, withThumb?: boolean, loop?: boolean, loadPromises?: Promise[], needFadeIn?: boolean, }) { const stickerType = doc.sticker; if(!width) { width = !emoji ? 200 : undefined; } if(!height) { height = !emoji ? 200 : undefined; } if(stickerType === 2 && !LottieLoader.loaded) { //LottieLoader.loadLottie(); LottieLoader.loadLottieWorkers(); } if(!stickerType) { console.error('wrong doc for wrapSticker!', doc); throw new Error('wrong doc for wrapSticker!'); } div.dataset.docId = '' + doc.id; div.classList.add('media-sticker-wrapper'); //console.log('wrap sticker', doc, div, onlyThumb); const cacheContext = appDownloadManager.getCacheContext(doc); const toneIndex = emoji ? getEmojiToneIndex(emoji) : -1; const downloaded = cacheContext.downloaded && !needFadeIn; let loadThumbPromise = deferredPromise(); let haveThumbCached = false; if((doc.thumbs?.length || doc.stickerCachedThumbs) && !div.firstElementChild && (!downloaded || stickerType === 2 || onlyThumb)/* && doc.thumbs[0]._ !== 'photoSizeEmpty' */) { let thumb = doc.stickerCachedThumbs && doc.stickerCachedThumbs[toneIndex] || doc.thumbs[0]; //console.log('wrap sticker', thumb, div); let thumbImage: HTMLImageElement; const afterRender = () => { if(!div.childElementCount) { thumbImage.classList.add('media-sticker', 'thumbnail'); sequentialDom.mutateElement(div, () => { div.append(thumbImage); loadThumbPromise.resolve(); }); } }; if('url' in thumb) { thumbImage = new Image(); renderImageFromUrl(thumbImage, thumb.url, afterRender); haveThumbCached = true; } else if('bytes' in thumb) { if(thumb._ === 'photoPathSize') { if(thumb.bytes.length) { const d = appPhotosManager.getPathFromPhotoPathSize(thumb); div.innerHTML = ` `; } else { thumb = doc.thumbs.find(t => (t as PhotoSize.photoStrippedSize).bytes?.length) || thumb; } } if(thumb && thumb._ !== 'photoPathSize' && toneIndex <= 0) { thumbImage = new Image(); if((IS_WEBP_SUPPORTED || doc.pFlags.stickerThumbConverted || cacheContext.url)/* && false */) { renderImageFromUrl(thumbImage, appPhotosManager.getPreviewURLFromThumb(doc, thumb as PhotoSize.photoStrippedSize, true), afterRender); haveThumbCached = true; } else { webpWorkerController.convert('' + doc.id, (thumb as PhotoSize.photoStrippedSize).bytes as Uint8Array).then(bytes => { (thumb as PhotoSize.photoStrippedSize).bytes = bytes; doc.pFlags.stickerThumbConverted = true; if(middleware && !middleware()) return; if(!div.childElementCount) { renderImageFromUrl(thumbImage, appPhotosManager.getPreviewURLFromThumb(doc, thumb as PhotoSize.photoStrippedSize, true), afterRender); } }).catch(() => {}); } } } else if(stickerType === 2 && (withThumb || onlyThumb) && toneIndex <= 0) { thumbImage = new Image(); const load = () => { if(div.childElementCount || (middleware && !middleware())) return; const r = () => { if(div.childElementCount || (middleware && !middleware())) return; renderImageFromUrl(thumbImage, cacheContext.url, afterRender); }; if(cacheContext.url) { r(); return Promise.resolve(); } else { return appDocsManager.getThumbURL(doc, thumb as PhotoSize.photoStrippedSize).promise.then(r); } }; if(lazyLoadQueue && onlyThumb) { lazyLoadQueue.push({div, load}); return Promise.resolve(); } else { load(); if((thumb as any).url) { haveThumbCached = true; } } } } if(loadPromises && haveThumbCached) { loadPromises.push(loadThumbPromise); } if(onlyThumb) { // for sticker panel return Promise.resolve(); } const load = async() => { if(middleware && !middleware()) return; if(stickerType === 2) { /* if(doc.id === '1860749763008266301') { console.log('loaded sticker:', doc, div); } */ //await new Promise((resolve) => setTimeout(resolve, 500)); //return; //console.time('download sticker' + doc.id); //appDocsManager.downloadDocNew(doc.id).promise.then(res => res.json()).then(async(json) => { //fetch(doc.url).then(res => res.json()).then(async(json) => { /* return */ await appDocsManager.downloadDoc(doc, /* undefined, */lazyLoadQueue?.queueId) .then(readBlobAsText) //.then(JSON.parse) .then(async(json) => { //console.timeEnd('download sticker' + doc.id); //console.log('loaded sticker:', doc, div/* , blob */); if(middleware && !middleware()) return; let animation = await LottieLoader.loadAnimationWorker({ container: div, loop: loop && !emoji, autoplay: play, animationData: json, width, height }, group, toneIndex); //const deferred = deferredPromise(); animation.addEventListener('firstFrame', () => { const element = div.firstElementChild; needFadeIn = (needFadeIn || !element || element.tagName === 'svg') && rootScope.settings.animationsEnabled; const cb = () => { if(element && element !== animation.canvas) { element.remove(); } }; if(!needFadeIn) { if(element) { sequentialDom.mutate(cb); } } else { sequentialDom.mutate(() => { animation.canvas.classList.add('fade-in'); if(element) { element.classList.add('fade-out'); } animation.canvas.addEventListener('animationend', () => { sequentialDom.mutate(() => { animation.canvas.classList.remove('fade-in'); cb(); }); }, {once: true}); }); } appDocsManager.saveLottiePreview(doc, animation.canvas, toneIndex); //deferred.resolve(); }, {once: true}); if(emoji) { attachClickEvent(div, (e) => { cancelEvent(e); let animation = LottieLoader.getAnimation(div); if(animation.paused) { animation.autoplay = true; animation.restart(); } }); } //return deferred; //await new Promise((resolve) => setTimeout(resolve, 5e3)); }); //console.timeEnd('render sticker' + doc.id); } else if(stickerType === 1) { const image = new Image(); const thumbImage = div.firstElementChild !== image && div.firstElementChild; needFadeIn = (needFadeIn || !downloaded || thumbImage) && rootScope.settings.animationsEnabled; image.classList.add('media-sticker'); if(needFadeIn) { image.classList.add('fade-in'); } return new Promise((resolve, reject) => { const r = () => { if(middleware && !middleware()) return resolve(); renderImageFromUrl(image, cacheContext.url, () => { sequentialDom.mutateElement(div, () => { div.append(image); if(thumbImage) { thumbImage.classList.add('fade-out'); } resolve(); if(needFadeIn) { image.addEventListener('animationend', () => { image.classList.remove('fade-in'); if(thumbImage) { thumbImage.remove(); } }, {once: true}); } }); }); }; if(cacheContext.url) r(); else { appDocsManager.downloadDoc(doc, /* undefined, */lazyLoadQueue?.queueId).then(r, resolve); } }); } }; const loadPromise: Promise = lazyLoadQueue && (!downloaded || stickerType === 2) ? (lazyLoadQueue.push({div, load}), Promise.resolve()) : load(); if(downloaded && stickerType === 1) { loadThumbPromise = loadPromise; if(loadPromises) { loadPromises.push(loadThumbPromise); } } return loadPromise; } export async function wrapStickerSetThumb({set, lazyLoadQueue, container, group, autoplay, width, height}: { set: StickerSet.stickerSet, lazyLoadQueue: LazyLoadQueue, container: HTMLElement, group: string, autoplay: boolean, width: number, height: number }) { if(set.thumbs?.length) { container.classList.add('media-sticker-wrapper'); lazyLoadQueue.push({ div: container, load: () => { const downloadOptions = appStickersManager.getStickerSetThumbDownloadOptions(set); const promise = appDownloadManager.download(downloadOptions); if(set.pFlags.animated) { return promise .then(readBlobAsText) //.then(JSON.parse) .then(json => { lottieLoader.loadAnimationWorker({ container, loop: true, autoplay, animationData: json, width, height, needUpscale: true }, group); }); } else { const image = new Image(); image.classList.add('media-sticker'); return promise.then(blob => { renderImageFromUrl(image, URL.createObjectURL(blob), () => { container.append(image); }); }); } } }); return; } const promise = appStickersManager.getStickerSet(set); const stickerSet = await promise; if(stickerSet.documents[0]._ !== 'documentEmpty') { // as thumb will be used first sticker wrapSticker({ doc: stickerSet.documents[0], div: container, group: group, lazyLoadQueue }); // kostil } } export function wrapLocalSticker({emoji, width, height}: { doc?: MyDocument, url?: string, emoji?: string, width: number, height: number, }) { const container = document.createElement('div'); const doc = appStickersManager.getAnimatedEmojiSticker(emoji); if(doc) { wrapSticker({ doc, div: container, loop: false, play: true, width, height, emoji }).then(() => { // this.animation = player; }); } else { container.classList.add('media-sticker-wrapper'); } return {container}; } export function wrapReply(title: Parameters[0], subtitle: Parameters[1], message?: any) { const replyContainer = new ReplyContainer('reply'); replyContainer.fill(title, subtitle, message); /////////console.log('wrapReply', title, subtitle, media); return replyContainer.container; } export function prepareAlbum(options: { container: HTMLElement, items: {w: number, h: number}[], maxWidth: number, minWidth: number, spacing: number, maxHeight?: number, forMedia?: true }) { const layouter = new Layouter(options.items, options.maxWidth, options.minWidth, options.spacing, options.maxHeight); const layout = layouter.layout(); const widthItem = layout.find(item => item.sides & RectPart.Right); const width = widthItem.geometry.width + widthItem.geometry.x; const heightItem = layout.find(item => item.sides & RectPart.Bottom); const height = heightItem.geometry.height + heightItem.geometry.y; const container = options.container; container.style.width = width + 'px'; container.style.height = height + 'px'; const children = container.children; layout.forEach(({geometry, sides}, idx) => { let div: HTMLElement; div = children[idx] as HTMLElement; if(!div) { div = document.createElement('div'); container.append(div); } div.classList.add('album-item', 'grouped-item'); div.style.width = (geometry.width / width * 100) + '%'; div.style.height = (geometry.height / height * 100) + '%'; div.style.top = (geometry.y / height * 100) + '%'; div.style.left = (geometry.x / width * 100) + '%'; if(sides & RectPart.Left && sides & RectPart.Top) { div.style.borderTopLeftRadius = 'inherit'; } if(sides & RectPart.Left && sides & RectPart.Bottom) { div.style.borderBottomLeftRadius = 'inherit'; } if(sides & RectPart.Right && sides & RectPart.Top) { div.style.borderTopRightRadius = 'inherit'; } if(sides & RectPart.Right && sides & RectPart.Bottom) { div.style.borderBottomRightRadius = 'inherit'; } if(options.forMedia) { const mediaDiv = document.createElement('div'); mediaDiv.classList.add('album-item-media'); div.append(mediaDiv); } // @ts-ignore //div.style.backgroundColor = '#' + Math.floor(Math.random() * (2 ** 24 - 1)).toString(16).padStart(6, '0'); }); /* if(options.forMedia) { layout.forEach((_, i) => { const mediaDiv = document.createElement('div'); mediaDiv.classList.add('album-item-media'); options.container.children[i].append(mediaDiv); }); } */ } export function wrapAlbum({groupId, attachmentDiv, middleware, uploading, lazyLoadQueue, isOut, chat, loadPromises, noAutoDownload}: { groupId: string, attachmentDiv: HTMLElement, middleware?: () => boolean, lazyLoadQueue?: LazyLoadQueue, uploading?: boolean, isOut: boolean, chat: Chat, loadPromises?: Promise[], noAutoDownload?: boolean, }) { const items: {size: PhotoSize.photoSize, media: any, message: any}[] = []; // !lowest msgID will be the FIRST in album const storage = appMessagesManager.getMidsByAlbum(groupId); for(const mid of storage) { const m = chat.getMessage(mid); const media = m.media.photo || m.media.document; const size: any = media._ === 'photo' ? appPhotosManager.choosePhotoSize(media, 480, 480) : {w: media.w, h: media.h}; items.push({size, media, message: m}); } /* // * pending if(storage[0] < 0) { items.reverse(); } */ prepareAlbum({ container: attachmentDiv, items: items.map(i => ({w: i.size.w, h: i.size.h})), maxWidth: mediaSizes.active.album.width, minWidth: 100, spacing: 2, forMedia: true }); items.forEach((item, idx) => { const {size, media, message} = item; const div = attachmentDiv.children[idx] as HTMLElement; div.dataset.mid = '' + message.mid; div.dataset.peerId = '' + message.peerId; const mediaDiv = div.firstElementChild as HTMLElement; if(media._ === 'photo') { wrapPhoto({ photo: media, message, container: mediaDiv, boxWidth: 0, boxHeight: 0, isOut, lazyLoadQueue, middleware, size, loadPromises, noAutoDownload }); } else { wrapVideo({ doc: message.media.document, container: mediaDiv, message, boxWidth: 0, boxHeight: 0, withTail: false, isOut, lazyLoadQueue, middleware, loadPromises, noAutoDownload }); } }); } export function wrapGroupedDocuments({albumMustBeRenderedFull, message, bubble, messageDiv, chat, loadPromises, noAutoDownload, lazyLoadQueue, searchContext, useSearch}: { albumMustBeRenderedFull: boolean, message: any, messageDiv: HTMLElement, bubble: HTMLElement, uploading?: boolean, chat: Chat, loadPromises?: Promise[], noAutoDownload?: boolean, lazyLoadQueue?: LazyLoadQueue, searchContext?: MediaSearchContext, useSearch?: boolean, }) { let nameContainer: HTMLElement; const mids = albumMustBeRenderedFull ? chat.getMidsByMid(message.mid) : [message.mid]; /* if(isPending) { mids.reverse(); } */ mids.forEach((mid, idx) => { const message = chat.getMessage(mid); const div = wrapDocument({ message, loadPromises, noAutoDownload, lazyLoadQueue, searchContext }); const container = document.createElement('div'); container.classList.add('document-container'); container.dataset.mid = '' + mid; container.dataset.peerId = '' + message.peerId; const wrapper = document.createElement('div'); wrapper.classList.add('document-wrapper'); if(message.message) { const messageDiv = document.createElement('div'); messageDiv.classList.add('document-message'); const richText = RichTextProcessor.wrapRichText(message.message, { entities: message.totalEntities }); messageDiv.innerHTML = richText; wrapper.append(messageDiv); } if(mids.length > 1) { const selection = document.createElement('div'); selection.classList.add('document-selection'); container.append(selection); container.classList.add('grouped-item'); if(idx === 0) { nameContainer = wrapper; } } wrapper.append(div); container.append(wrapper); messageDiv.append(container); }); if(mids.length > 1) { bubble.classList.add('is-multiple-documents', 'is-grouped'); } return nameContainer; } export function wrapPoll(message: any) { const elem = new PollElement(); elem.message = message; elem.setAttribute('peer-id', '' + message.peerId); elem.setAttribute('poll-id', message.media.poll.id); elem.setAttribute('message-id', '' + message.mid); elem.render(); return elem; }