import type Chat from './chat/chat' ;
import { getEmojiToneIndex } from '../emoji' ;
import { readBlobAsText } from '../helpers/blob' ;
import { deferredPromise } from '../helpers/cancellablePromise' ;
import { formatDateAccordingToToday , months } from '../helpers/date' ;
import mediaSizes from '../helpers/mediaSizes' ;
import { formatBytes } from '../helpers/number' ;
import { isAppleMobile , isSafari } from '../helpers/userAgent' ;
import { PhotoSize } 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 VideoPlayer from '../lib/mediaPlayer' ;
import { attachClickEvent , cancelEvent , isInDOM } from "../helpers/dom" ;
import webpWorkerController from '../lib/webp/webpWorkerController' ;
import animationIntersector from './animationIntersector' ;
import appMediaPlaybackController from './appMediaPlaybackController' ;
import AudioElement from './audio' ;
import ReplyContainer from './chat/replyContainer' ;
import { Layouter , RectPart } from './groupedLayout' ;
import LazyLoadQueue from './lazyLoadQueue' ;
import { renderImageFromUrl } from './misc' ;
import PollElement from './poll' ;
import ProgressivePreloader from './preloader' ;
import './middleEllipsis' ;
import { nextRandomInt } from '../helpers/random' ;
import RichTextProcessor from '../lib/richtextprocessor' ;
import appImManager from '../lib/appManagers/appImManager' ;
import { SearchSuperContext } from './appSearchSuper.' ;
import rootScope from '../lib/rootScope' ;
import { onVideoLoad } from '../helpers/files' ;
const MAX_VIDEO_AUTOPLAY_SIZE = 50 * 1024 * 1024 ; // 50 MB
export function wrapVideo ( { doc , container , message , boxWidth , boxHeight , withTail , isOut , middleware , lazyLoadQueue , noInfo , group , onlyPreview , withoutPreloader , loadPromises , noPlayButton } : {
doc : MyDocument ,
container? : HTMLElement ,
message? : any ,
boxWidth? : number ,
boxHeight? : number ,
withTail? : boolean ,
isOut? : boolean ,
middleware ? : ( ) = > boolean ,
lazyLoadQueue? : LazyLoadQueue ,
noInfo? : true ,
noPlayButton? : boolean ,
group? : string ,
onlyPreview? : boolean ,
withoutPreloader? : boolean ,
loadPromises? : Promise < any > [ ]
} ) {
const isAlbumItem = ! ( boxWidth && boxHeight ) ;
const canAutoplay = doc . type != 'video' || ( doc . size <= MAX_VIDEO_AUTOPLAY_SIZE && ! isAlbumItem ) ;
let spanTime : HTMLElement ;
if ( ! noInfo ) {
if ( doc . type !== 'round' ) {
spanTime = document . createElement ( 'span' ) ;
spanTime . classList . add ( 'video-time' ) ;
container . append ( spanTime ) ;
if ( doc . type !== 'gif' ) {
spanTime . innerText = ( doc . duration + '' ) . toHHMMSS ( false ) ;
if ( ! noPlayButton ) {
if ( canAutoplay ) {
spanTime . classList . add ( 'tgico' , 'can-autoplay' ) ;
} else {
const spanPlay = document . createElement ( 'span' ) ;
spanPlay . classList . add ( 'video-play' , 'tgico-largeplay' , 'btn-circle' , 'position-center' ) ;
container . append ( spanPlay ) ;
}
}
} else {
spanTime . innerText = 'GIF' ;
}
}
}
let res : {
thumb? : typeof photoRes ,
loadPromise : Promise < any >
} = { } as any ;
if ( doc . mime_type === 'image/gif' ) {
const photoRes = wrapPhoto ( {
photo : doc ,
message ,
container ,
boxWidth ,
boxHeight ,
withTail ,
isOut ,
lazyLoadQueue ,
middleware ,
withoutPreloader ,
loadPromises
} ) ;
res . thumb = photoRes ;
res . loadPromise = photoRes . loadPromises . full ;
return res ;
}
/ * c o n s t v i d e o = d o c . t y p e = = ' r o u n d ' ? a p p M e d i a P l a y b a c k C o n t r o l l e r . a d d M e d i a ( d o c , m e s s a g e . m i d ) a s H T M L V i d e o E l e m e n t : d o c u m e n t . c r e a t e E l e m e n t ( ' v i d e o ' ) ;
if ( video . parentElement ) {
video . remove ( ) ;
} * /
const video = /* doc.type === 'round' ? appMediaPlaybackController.addMedia(message.peerId, doc, message.mid) as HTMLVideoElement : */ document . createElement ( 'video' ) ;
video . classList . add ( 'media-video' ) ;
video . muted = true ;
video . setAttribute ( 'playsinline' , 'true' ) ;
if ( doc . type === 'round' ) {
//video.classList.add('z-depth-1');
const globalVideo = appMediaPlaybackController . addMedia ( message . peerId , doc , message . mid ) ;
video . classList . add ( 'z-depth-1' ) ;
onVideoLoad ( video ) . then ( ( ) = > {
if ( globalVideo . currentTime !== globalVideo . duration ) {
video . currentTime = globalVideo . currentTime ;
}
if ( ! globalVideo . paused ) {
// с закоментированными настройками - хром выключал видео при скролле, для этого нужно было включить видео - выйти из диалога, зайти заново и проскроллить вверх
//video.autoplay = true;
//video.loop = false;
video . play ( ) ;
}
} ) ;
const clear = ( ) = > {
//console.log('clearing video');
globalVideo . removeEventListener ( 'timeupdate' , onGlobalTimeUpdate ) ;
globalVideo . removeEventListener ( 'play' , onGlobalPlay ) ;
globalVideo . removeEventListener ( 'pause' , onGlobalPause ) ;
video . removeEventListener ( 'play' , onVideoPlay ) ;
video . removeEventListener ( 'pause' , onVideoPause ) ;
} ;
const onGlobalTimeUpdate = ( e : Event ) = > {
//console.log('video global timeupdate event', e, globalVideo.currentTime, globalVideo.duration);
if ( ! isInDOM ( video ) ) {
clear ( ) ;
}
} ;
const onGlobalPlay = ( e : Event ) = > {
//console.log('video global play event', e);
video . play ( ) ;
} ;
const onGlobalPause = ( e : Event ) = > {
//console.trace('video global pause event', e, globalVideo.paused, e.eventPhase);
video . pause ( ) ;
} ;
const onVideoPlay = ( e : Event ) = > {
//console.log('video play event', e);
if ( globalVideo . paused ) {
globalVideo . currentTime = video . currentTime ;
globalVideo . play ( ) ;
}
} ;
// * this will fire when video unmounts
const onVideoPause = ( e : Event ) = > {
//console.trace('video pause event', e);
if ( isInDOM ( video ) ) {
globalVideo . pause ( ) ;
globalVideo . currentTime = video . currentTime ;
} else {
clear ( ) ;
}
} ;
globalVideo . addEventListener ( 'timeupdate' , onGlobalTimeUpdate ) ;
globalVideo . addEventListener ( 'play' , onGlobalPlay ) ;
globalVideo . addEventListener ( 'pause' , onGlobalPause ) ;
video . addEventListener ( 'play' , onVideoPlay ) ;
video . addEventListener ( 'pause' , onVideoPause ) ;
} else {
video . autoplay = true ; // для safari
}
let photoRes : ReturnType < typeof wrapPhoto > ;
if ( message ) {
photoRes = wrapPhoto ( {
photo : doc ,
message ,
container ,
boxWidth ,
boxHeight ,
withTail ,
isOut ,
lazyLoadQueue ,
middleware ,
withoutPreloader : true ,
loadPromises
} ) ;
res . thumb = photoRes ;
if ( ! canAutoplay || 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 . thumb . url ;
} ) ;
}
}
if ( ! video . parentElement && container ) {
container . append ( video ) ;
}
const loadVideo = async ( ) = > {
if ( middleware && ! middleware ( ) ) {
return ;
}
let loadPromise : Promise < any > = Promise . resolve ( ) ;
let preloader : ProgressivePreloader ;
if ( message ? . media ? . preloader ) { // means upload
preloader = message . media . preloader as ProgressivePreloader ;
preloader . attach ( container , false ) ;
} else if ( ! doc . downloaded && ! doc . supportsStreaming ) {
const promise = loadPromise = appDocsManager . downloadDoc ( doc , lazyLoadQueue ? . queueId ) ;
preloader = new ProgressivePreloader ( {
attachMethod : 'prepend'
} ) ;
preloader . attach ( container , true , promise ) ;
//if(doc.type !== 'round') {
await promise ;
if ( middleware && ! middleware ( ) ) {
return ;
}
//}
} else if ( doc . supportsStreaming ) {
preloader = new ProgressivePreloader ( {
cancelable : false ,
attachMethod : 'prepend'
} ) ;
preloader . attach ( container , false , null ) ;
video . addEventListener ( isSafari ? 'timeupdate' : 'canplay' , ( ) = > {
preloader . detach ( ) ;
} , { once : true } ) ;
}
/ * i f ( d o c . t y p e = = = ' r o u n d ' ) {
return ;
} * /
//console.log('loaded doc:', doc, doc.url, container);
const deferred = deferredPromise < void > ( ) ;
//if(doc.type == 'gif'/* || true */) {
onVideoLoad ( video ) . then ( ( ) = > {
/ * i f ( ! v i d e o . p a u s e d ) {
video . pause ( ) ;
} * /
if ( doc . type !== 'round' && group ) {
animationIntersector . addAnimation ( video , group ) ;
}
// test lazyLoadQueue
//setTimeout(() => {
deferred . resolve ( ) ;
//}, 5000);
} ) ;
//}
if ( doc . type === 'video' ) {
video . addEventListener ( 'timeupdate' , ( ) = > {
spanTime . innerText = ( video . duration - video . currentTime + '' ) . toHHMMSS ( false ) ;
} ) ;
}
video . addEventListener ( 'error' , ( e ) = > {
deferred . resolve ( ) ;
} ) ;
if ( doc . type !== 'round' ) {
video . muted = true ;
video . loop = true ;
//video.play();
video . autoplay = true ;
}
renderImageFromUrl ( video , doc . url ) ;
return Promise . all ( [ loadPromise , deferred ] ) ;
} ;
if ( doc . type === 'round' ) {
video . dataset . ckin = 'circle' ;
video . dataset . overlay = '1' ;
new VideoPlayer ( video , undefined , undefined , doc . duration ) ;
}
/ * i f ( d o c . s i z e > = 2 0 e 6 & & ! d o c . d o w n l o a d e d ) {
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 ;
} * /
res . loadPromise = ! lazyLoadQueue ? loadVideo ( ) : ( lazyLoadQueue . push ( { div : container , load : loadVideo } ) , Promise . resolve ( ) ) ;
return res ;
}
export const formatDate = ( timestamp : number , monthShort = false , withYear = true ) = > {
const date = new Date ( timestamp * 1000 ) ;
let month = months [ date . getMonth ( ) ] ;
if ( monthShort ) month = month . slice ( 0 , 3 ) ;
let str = month + ' ' + date . getDate ( ) ;
if ( withYear ) {
str += ', ' + date . getFullYear ( ) ;
}
return str + ' at ' + date . getHours ( ) + ':' + ( '0' + date . getMinutes ( ) ) . slice ( - 2 ) ;
} ;
export function wrapDocument ( { message , withTime , fontWeight , voiceAsMusic , showSender , searchContext , loadPromises } : {
message : any ,
withTime? : boolean ,
fontWeight? : number ,
voiceAsMusic? : boolean ,
showSender? : boolean ,
searchContext? : SearchSuperContext ,
loadPromises? : Promise < any > [ ]
} ) : HTMLElement {
if ( ! fontWeight ) fontWeight = 500 ;
const uploading = message . pFlags . is_outgoing ;
const doc = ( message . media . document || message . media . webpage . document ) as MyDocument ;
if ( doc . type === 'audio' || doc . type === 'voice' ) {
const audioElement = new AudioElement ( ) ;
audioElement . setAttribute ( 'message-id' , '' + message . mid ) ;
audioElement . setAttribute ( 'peer-id' , '' + message . peerId ) ;
audioElement . withTime = withTime ;
audioElement . message = message ;
if ( voiceAsMusic ) audioElement . voiceAsMusic = voiceAsMusic ;
if ( searchContext ) audioElement . searchContext = searchContext ;
if ( showSender ) audioElement . showSender = showSender ;
const isPending = message . pFlags . is_outgoing ;
if ( isPending ) {
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 ) ? extSplitted . pop ( ) . toLowerCase ( ) : 'file' ;
let docDiv = document . createElement ( 'div' ) ;
docDiv . classList . add ( 'document' , ` ext- ${ ext } ` ) ;
const icoDiv = document . createElement ( 'div' ) ;
icoDiv . classList . add ( 'document-ico' ) ;
if ( doc . thumbs ? . length || ( uploading && doc . url && doc . type == 'photo' ) ) {
docDiv . classList . add ( 'document-with-thumb' ) ;
if ( uploading ) {
icoDiv . innerHTML = ` <img src=" ${ doc . url } "> ` ;
} else {
wrapPhoto ( {
photo : doc ,
message : null ,
container : icoDiv ,
boxWidth : 54 ,
boxHeight : 54 ,
loadPromises ,
withoutPreloader : true
} ) ;
icoDiv . style . width = icoDiv . style . height = '' ;
}
const img = icoDiv . querySelector ( 'img' ) ;
if ( img ) img . classList . add ( 'document-thumb' ) ;
} else {
icoDiv . innerText = ext ;
}
//let fileName = stringMiddleOverflow(doc.file_name || 'Unknown.file', 26);
let fileName = doc . file_name || 'Unknown.file' ;
let size = formatBytes ( doc . size ) ;
if ( withTime ) {
size += ' · ' + formatDate ( doc . date ) ;
}
if ( showSender ) {
size += ' · ' + appMessagesManager . getSenderToPeerText ( message ) ;
}
let titleAdditionHTML = '' ;
if ( showSender ) {
titleAdditionHTML = ` <div class="sent-time"> ${ formatDateAccordingToToday ( new Date ( message . date * 1000 ) ) } </div> ` ;
}
docDiv . innerHTML = `
$ { ! uploading ? ` <div class="document-download"></div> ` : '' }
< div class = "document-name" > < middle - ellipsis - element data - font - weight = "${fontWeight}" > $ { fileName } < / m i d d l e - e l l i p s i s - e l e m e n t > $ { t i t l e A d d i t i o n H T M L } < / d i v >
< div class = "document-size" > $ { size } < / div >
` ;
docDiv . prepend ( icoDiv ) ;
if ( ! uploading ) {
const downloadDiv = docDiv . querySelector ( '.document-download' ) as HTMLDivElement ;
const preloader = new ProgressivePreloader ( ) ;
const load = ( ) = > {
const download = appDocsManager . saveDocFile ( doc , appImManager . chat . bubbles ? appImManager.chat.bubbles.lazyLoadQueue.queueId : 0 ) ;
download . then ( ( ) = > {
downloadDiv . classList . add ( 'downloaded' ) ;
setTimeout ( ( ) = > {
downloadDiv . remove ( ) ;
} , 200 ) ;
} ) ;
preloader . attach ( downloadDiv , true , download ) ;
return { download } ;
} ;
preloader . construct ( ) ;
preloader . setManual ( ) ;
preloader . attach ( downloadDiv ) ;
preloader . setDownloadFunction ( load ) ;
attachClickEvent ( docDiv , ( e ) = > {
preloader . onClick ( e ) ;
} ) ;
/ * i f ( d o c . d o w n l o a d e d ) {
downloadDiv . classList . add ( 'downloaded' ) ;
} * /
} else if ( message . media ? . preloader ) {
const icoDiv = docDiv . querySelector ( '.document-ico' ) ;
message . media . preloader . attach ( icoDiv , false ) ;
}
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 ) ;
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 += `<rect width="${width}" height="${height}"></rect>`;
} else {
if ( isOut ) {
clipPathHTML += `
< use href = "#message-tail" transform = "translate(${width - 2}, ${height}) scale(-1, -1)" > < / use >
< path / >
` ;
} else {
clipPathHTML += `
< use href = "#message-tail" transform = "translate(2, ${height}) scale(1, -1)" > < / use >
< path / >
` ;
}
}
defs . innerHTML = ` <clipPath id=" ${ clipId } "> ${ clipPathHTML } </clipPath> ` ;
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 } : {
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 < any > [ ]
} ) {
if ( ! ( ( photo as MyPhoto ) . sizes || ( photo as MyDocument ) . thumbs ) ) {
if ( boxWidth && boxHeight && photo . _ === 'document' ) {
size = appPhotosManager . setAttachmentSize ( photo , container , boxWidth , boxHeight ) ;
}
return {
loadPromises : {
thumb : Promise.resolve ( ) ,
full : Promise.resolve ( )
} ,
images : {
thumb : null ,
full : null
}
} ;
}
if ( boxWidth === undefined ) boxWidth = mediaSizes . active . regular . width ;
if ( boxHeight === undefined ) boxHeight = mediaSizes . active . regular . height ;
let loadThumbPromise : Promise < any > ;
let thumbImage : HTMLImageElement ;
let image : HTMLImageElement ;
if ( withTail ) {
image = wrapMediaWithTail ( photo , message , container , boxWidth , boxHeight , isOut ) ;
} else {
image = new Image ( ) ;
if ( boxWidth && boxHeight ) { // !album
size = appPhotosManager . setAttachmentSize ( photo , container , boxWidth , boxHeight ) ;
}
const gotThumb = appPhotosManager . getStrippedThumbIfNeeded ( photo ) ;
if ( gotThumb ) {
loadThumbPromise = gotThumb . loadPromise ;
thumbImage = gotThumb . image ;
thumbImage . classList . add ( 'media-photo' ) ;
container . append ( thumbImage ) ;
}
}
image . classList . add ( 'media-photo' ) ;
//console.log('wrapPhoto downloaded:', photo, photo.downloaded, container);
const cacheContext = appPhotosManager . getCacheContext ( photo ) ;
const needFadeIn = ( thumbImage || ! cacheContext . downloaded ) && rootScope . settings . animationsEnabled ;
if ( needFadeIn ) {
image . classList . add ( 'fade-in' ) ;
}
let preloader : ProgressivePreloader ;
if ( message ? . media ? . preloader ) { // means upload
preloader = message . media . preloader ;
preloader . attach ( container ) ;
} else {
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 ) ;
return promise ;
} ;
const onLoad = ( ) : Promise < void > = > {
if ( middleware && ! middleware ( ) ) return Promise . resolve ( ) ;
return new Promise ( ( resolve ) = > {
renderImageFromUrl ( image , cacheContext . url || photo . url , ( ) = > {
container . append ( image ) ;
window . requestAnimationFrame ( ( ) = > {
resolve ( ) ;
} ) ;
//resolve();
if ( needFadeIn ) {
image . addEventListener ( 'animationend' , ( ) = > {
image . classList . remove ( 'fade-in' ) ;
if ( thumbImage ) {
thumbImage . remove ( ) ;
}
} , { once : true } ) ;
}
} ) ;
} ) ;
} ;
let loadPromise : Promise < any > ;
const load = ( ) = > {
const promise = getDownloadPromise ( ) ;
if ( ! cacheContext . downloaded && ! withoutPreloader && ( size as PhotoSize . photoSize ) . w >= 150 && ( size as PhotoSize . photoSize ) . h >= 150 ) {
preloader . attach ( container , false , promise ) ;
}
return { download : promise , render : promise.then ( onLoad ) } ;
} ;
preloader . setDownloadFunction ( load ) ;
if ( cacheContext . downloaded ) {
loadThumbPromise = loadPromise = load ( ) . render ;
} else {
if ( ! lazyLoadQueue ) loadPromise = load ( ) . render ;
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
}
} ;
}
export function wrapSticker ( { doc , div , middleware , lazyLoadQueue , group , play , onlyThumb , emoji , width , height , withThumb , loop , loadPromises } : {
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 < any > [ ]
} ) {
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 ;
//console.log('wrap sticker', doc, div, onlyThumb);
const toneIndex = emoji ? getEmojiToneIndex ( emoji ) : - 1 ;
let loadThumbPromise = deferredPromise < void > ( ) ;
let haveThumbCached = false ;
if ( ( doc . thumbs ? . length || doc . stickerCachedThumbs ) && ! div . firstElementChild && ( ! doc . 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' ) ;
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 = ` <svg class="rlottie-vector media-sticker thumbnail" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 ${ doc . w || 512 } ${ doc . h || 512 } " xml:space="preserve">
< path d = "${d}" / >
< / svg > ` ;
} else {
thumb = doc . thumbs . find ( t = > ( t as PhotoSize . photoStrippedSize ) . bytes ? . length ) || thumb ;
}
}
if ( thumb && thumb . _ !== 'photoPathSize' && toneIndex <= 0 ) {
thumbImage = new Image ( ) ;
if ( ( webpWorkerController . isWebpSupported ( ) || doc . pFlags . stickerThumbConverted || thumb . url ) /* && false */ ) {
renderImageFromUrl ( thumbImage , appPhotosManager . getPreviewURLFromThumb ( 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 ( 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 , ( thumb as PhotoSize . photoStrippedSize ) . url , afterRender ) ;
} ;
if ( ( thumb as PhotoSize . photoStrippedSize ) . 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 ( ) ;
}
let downloaded = doc . downloaded ;
let load = async ( ) = > {
if ( middleware && ! middleware ( ) ) return ;
if ( stickerType == 2 ) {
/ * i f ( d o c . i d = = ' 1 8 6 0 7 4 9 7 6 3 0 0 8 2 6 6 3 0 1 ' ) {
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<void>();
animation . addListener ( 'firstFrame' , ( ) = > {
const element = div . firstElementChild ;
const needFadeIn = ! element || element . tagName === 'svg' ;
const cb = ( ) = > {
if ( element && element !== animation . canvas ) {
element . remove ( ) ;
}
} ;
if ( ! needFadeIn ) {
cb ( ) ;
} else {
animation . canvas . classList . add ( 'fade-in' ) ;
if ( element ) {
element . classList . add ( 'fade-out' ) ;
}
animation . canvas . addEventListener ( 'animationend' , ( ) = > {
animation . canvas . classList . remove ( 'fade-in' ) ;
cb ( ) ;
} , { once : true } ) ;
}
appDocsManager . saveLottiePreview ( doc , animation . canvas , toneIndex ) ;
//deferred.resolve();
} , 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 && div . firstElementChild !== image ? div.firstElementChild : null ;
const needFadeIn = ! downloaded || thumbImage ;
image . classList . add ( 'media-sticker' ) ;
if ( needFadeIn ) {
image . classList . add ( 'fade-in' ) ;
}
return new Promise < void > ( ( resolve , reject ) = > {
const r = ( ) = > {
if ( middleware && ! middleware ( ) ) return resolve ( ) ;
renderImageFromUrl ( image , doc . url , ( ) = > {
div . append ( image ) ;
if ( thumbImage ) {
thumbImage . classList . add ( 'fade-out' ) ;
}
window . requestAnimationFrame ( ( ) = > {
resolve ( ) ;
} ) ;
if ( needFadeIn ) {
image . addEventListener ( 'animationend' , ( ) = > {
image . classList . remove ( 'fade-in' ) ;
if ( thumbImage ) {
thumbImage . remove ( ) ;
}
} , { once : true } ) ;
}
} ) ;
} ;
if ( doc . url ) r ( ) ;
else {
appDocsManager . downloadDoc ( doc , /* undefined, */ lazyLoadQueue ? . queueId ) . then ( r , resolve ) ;
}
} ) ;
}
} ;
const loadPromise : Promise < any > = lazyLoadQueue && ( ! doc . downloaded || stickerType == 2 ) ?
( lazyLoadQueue . push ( { div , load } ) , Promise . resolve ( ) ) :
load ( ) ;
if ( doc . downloaded && stickerType === 1 ) {
loadThumbPromise = loadPromise ;
if ( loadPromises ) {
loadPromises . push ( loadThumbPromise ) ;
}
}
return loadPromise ;
}
export function wrapReply ( title : string , subtitle : string , 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');
} ) ;
/ * i f ( o p t i o n s . f o r M e d i a ) {
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 } : {
groupId : string ,
attachmentDiv : HTMLElement ,
middleware ? : ( ) = > boolean ,
lazyLoadQueue? : LazyLoadQueue ,
uploading? : boolean ,
isOut : boolean ,
chat : Chat ,
loadPromises? : Promise < any > [ ]
} ) {
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 ;
const mediaDiv = div . firstElementChild as HTMLElement ;
if ( media . _ == 'photo' ) {
wrapPhoto ( {
photo : media ,
message ,
container : mediaDiv ,
boxWidth : 0 ,
boxHeight : 0 ,
isOut ,
lazyLoadQueue ,
middleware ,
size ,
loadPromises
} ) ;
} else {
wrapVideo ( {
doc : message.media.document ,
container : mediaDiv ,
message ,
boxWidth : 0 ,
boxHeight : 0 ,
withTail : false ,
isOut ,
lazyLoadQueue ,
middleware ,
loadPromises
} ) ;
}
} ) ;
}
export function wrapGroupedDocuments ( { albumMustBeRenderedFull , message , bubble , messageDiv , chat , loadPromises } : {
albumMustBeRenderedFull : boolean ,
message : any ,
messageDiv : HTMLElement ,
bubble : HTMLElement ,
uploading? : boolean ,
chat : Chat ,
loadPromises? : Promise < any > [ ]
} ) {
let nameContainer : HTMLElement ;
const mids = albumMustBeRenderedFull ? chat . getMidsByMid ( message . mid ) : [ message . mid ] ;
/ * i f ( i s P e n d i n g ) {
mids . reverse ( ) ;
} * /
mids . forEach ( ( mid , idx ) = > {
const message = chat . getMessage ( mid ) ;
const div = wrapDocument ( {
message ,
loadPromises
} ) ;
const container = document . createElement ( 'div' ) ;
container . classList . add ( 'document-container' ) ;
container . dataset . mid = '' + mid ;
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 ;
}