2022-04-25 16:54:30 +02:00
/ *
* https : //github.com/morethanwords/tweb
* Copyright ( C ) 2019 - 2021 Eduard Kuzmenko
* https : //github.com/morethanwords/tweb/blob/master/LICENSE
* /
2022-08-04 08:49:54 +02:00
import type { EMOJI_VERSION } from '../../environment/emojiVersionsSupport' ;
import { SITE_HASHTAGS } from '.' ;
import { EmojiVersions } from '../../config/emoji' ;
import IS_EMOJI_SUPPORTED from '../../environment/emojiSupport' ;
import buildURLHash from '../../helpers/buildURLHash' ;
import copy from '../../helpers/object/copy' ;
import encodeEntities from '../../helpers/string/encodeEntities' ;
import { MessageEntity } from '../../layer' ;
import encodeSpoiler from './encodeSpoiler' ;
import parseEntities from './parseEntities' ;
import setBlankToAnchor from './setBlankToAnchor' ;
import wrapUrl from './wrapUrl' ;
import EMOJI_VERSIONS_SUPPORTED from '../../environment/emojiVersionsSupport' ;
2022-08-09 17:35:11 +02:00
import { CLICK_EVENT_NAME } from '../../helpers/dom/clickEvent' ;
2022-08-31 06:22:16 +02:00
import IS_CUSTOM_EMOJI_SUPPORTED from '../../environment/customEmojiSupport' ;
import rootScope from '../rootScope' ;
import mediaSizes from '../../helpers/mediaSizes' ;
2022-11-01 18:39:23 +01:00
import wrapSticker , { videosCache } from '../../components/wrappers/sticker' ;
import RLottiePlayer , { getLottiePixelRatio } from '../rlottie/rlottiePlayer' ;
2022-09-13 14:27:28 +02:00
import animationIntersector , { AnimationItemGroup } from '../../components/animationIntersector' ;
2022-08-31 06:22:16 +02:00
import type { MyDocument } from '../appManagers/appDocsManager' ;
import LazyLoadQueue from '../../components/lazyLoadQueue' ;
import { Awaited } from '../../types' ;
import { MediaSize } from '../../helpers/mediaSize' ;
import IS_WEBM_SUPPORTED from '../../environment/webmSupport' ;
2022-09-13 14:27:28 +02:00
import assumeType from '../../helpers/assumeType' ;
import noop from '../../helpers/noop' ;
import findUpClassName from '../../helpers/dom/findUpClassName' ;
import getViewportSlice from '../../helpers/dom/getViewportSlice' ;
2022-11-01 18:39:23 +01:00
import { getMiddleware , Middleware , MiddlewareHelper } from '../../helpers/middleware' ;
import replaceContent from '../../helpers/dom/replaceContent' ;
import BOM from '../../helpers/string/bom' ;
import framesCache from '../../helpers/framesCache' ;
2023-01-06 20:27:29 +01:00
import wrapTelegramUrlToAnchor from './wrapTelegramUrlToAnchor' ;
2022-08-31 06:22:16 +02:00
const resizeObserver = new ResizeObserver ( ( entries ) = > {
for ( const entry of entries ) {
const renderer = entry . target . parentElement as CustomEmojiRendererElement ;
renderer . setDimensionsFromRect ( entry . contentRect ) ;
}
} ) ;
2022-11-01 18:39:23 +01:00
const globalLazyLoadQueue = new LazyLoadQueue ( ) ;
export class CustomEmojiElement extends HTMLElement {
public elements : CustomEmojiElements ;
2022-09-13 14:27:28 +02:00
public renderer : CustomEmojiRendererElement ;
public player : RLottiePlayer | HTMLVideoElement ;
public paused : boolean ;
public syncedPlayer : SyncedPlayer ;
2022-11-01 18:39:23 +01:00
public clean : boolean ;
public lastChildWas : Node ;
// public docId: DocId;
public placeholder : HTMLImageElement ;
public middlewareHelper : MiddlewareHelper ;
2022-09-13 14:27:28 +02:00
constructor ( ) {
super ( ) ;
this . paused = true ;
2022-11-01 18:39:23 +01:00
this . classList . add ( 'custom-emoji' ) ;
}
public get docId() {
return this . dataset . docId ;
}
public static create ( docId : DocId ) {
const element = new CustomEmojiElement ( ) ;
// element.docId = docId;
element . dataset . docId = '' + docId ;
return element ;
}
public get isConnected() {
return this . placeholder ? . isConnected ? ? super . isConnected ;
2022-09-13 14:27:28 +02:00
}
public connectedCallback() {
2022-11-01 18:39:23 +01:00
// if(this.isConnected) {
// return;
// }
2022-09-13 14:27:28 +02:00
if ( this . player ) {
2023-03-01 11:20:49 +01:00
animationIntersector . addAnimation ( {
animation : this ,
group : this.renderer.animationGroup ,
controlled : true
} ) ;
2022-09-13 14:27:28 +02:00
}
2022-11-01 18:39:23 +01:00
// this.connectedCallback = undefined;
2022-09-13 14:27:28 +02:00
}
public disconnectedCallback() {
2023-01-06 20:27:29 +01:00
if ( this . isConnected || ! this . renderer ? . isSelectable ) { // prepend on sibling can invoke disconnectedCallback
2022-09-20 19:34:08 +02:00
return ;
}
2022-11-01 18:39:23 +01:00
this . clear ( ) ;
}
2023-01-06 20:27:29 +01:00
public destroy() {
this . clear ( ) ;
}
2022-11-01 18:39:23 +01:00
public clear ( replaceChildren = true ) {
if ( this . clean ) {
return ;
}
// if(this.docId === '5399836826758290421') {
// console.log('clear', this, this.isConnected);
// }
this . clean = true ;
this . pause ( ) ;
const { syncedPlayer } = this ;
if ( syncedPlayer ) {
syncedPlayer . pausedElements . delete ( this ) ;
2022-09-13 14:27:28 +02:00
}
2022-11-01 18:39:23 +01:00
this . middlewareHelper ? . clean ( ) ;
2022-09-13 14:27:28 +02:00
2022-11-01 18:39:23 +01:00
if ( this . renderer ) {
const elements = this . renderer . customEmojis . get ( this . docId ) ;
if ( elements ? . delete ( this ) && ! elements . size ) {
this . renderer . customEmojis . delete ( this . docId ) ;
this . renderer . playersSynced . delete ( elements ) ;
}
if ( replaceChildren ) {
if ( this . renderer . isSelectable ) {
this . replaceChildren ( createCustomFiller ( true ) ) ;
} else {
// otherwise https://bugs.chromium.org/p/chromium/issues/detail?id=1144736#c27 will happen
this . replaceChildren ( ) ;
}
}
}
if ( this . player ) {
animationIntersector . removeAnimationByPlayer ( this ) ;
}
if ( globalLazyLoadQueue ) {
2022-11-27 15:50:02 +01:00
globalLazyLoadQueue . delete ( { div : this } ) ;
2022-11-01 18:39:23 +01:00
}
/* this.disconnectedCallback = */ this . elements =
this . renderer =
this . player =
this . syncedPlayer =
undefined ;
2022-09-13 14:27:28 +02:00
}
public pause() {
if ( this . paused ) {
return ;
}
this . paused = true ;
2022-08-31 06:22:16 +02:00
2022-11-01 18:39:23 +01:00
if ( this . player instanceof HTMLVideoElement && ! this . syncedPlayer ) {
2022-09-13 14:27:28 +02:00
this . renderer . lastPausedVideo = this . player ;
this . player . pause ( ) ;
}
if ( this . syncedPlayer && ! this . syncedPlayer . pausedElements . has ( this ) ) {
this . syncedPlayer . pausedElements . add ( this ) ;
2022-11-01 18:39:23 +01:00
if ( this . syncedPlayer . player && this . syncedPlayer . pausedElements . size === this . syncedPlayer . middlewares . size ) {
2022-09-13 14:27:28 +02:00
this . syncedPlayer . player . pause ( ) ;
}
}
}
public play() {
if ( ! this . paused ) {
return ;
}
this . paused = false ;
if ( this . player instanceof HTMLVideoElement ) {
2023-03-01 11:20:49 +01:00
this . player . currentTime = this . renderer . lastPausedVideo ? . currentTime ? ? this . player . currentTime ;
2022-09-13 14:27:28 +02:00
this . player . play ( ) . catch ( noop ) ;
}
if ( this . syncedPlayer && this . syncedPlayer . pausedElements . has ( this ) ) {
this . syncedPlayer . pausedElements . delete ( this ) ;
2022-11-01 18:39:23 +01:00
if ( this . syncedPlayer . pausedElements . size !== this . syncedPlayer . middlewares . size ) {
2022-09-13 14:27:28 +02:00
this . player . play ( ) ;
}
}
}
public remove() {
2022-11-01 18:39:23 +01:00
this . clear ( ) ;
// this.elements = this.renderer = this.player = undefined;
2022-09-13 14:27:28 +02:00
}
public get autoplay() {
return true ;
}
2023-03-01 11:20:49 +01:00
public get loop() {
return true ;
}
2022-08-31 06:22:16 +02:00
}
2022-11-01 18:39:23 +01:00
type CustomEmojiElements = Set < CustomEmojiElement > ;
2022-08-31 06:22:16 +02:00
export class CustomEmojiRendererElement extends HTMLElement {
public canvas : HTMLCanvasElement ;
public context : CanvasRenderingContext2D ;
2022-11-01 18:39:23 +01:00
public playersSynced : Map < CustomEmojiElements , RLottiePlayer | HTMLVideoElement > ;
public clearedElements : WeakSet < CustomEmojiElements > ;
public customEmojis : Parameters < typeof wrapRichText > [ 1 ] [ 'customEmojis' ] ;
2022-09-13 14:27:28 +02:00
public lastPausedVideo : HTMLVideoElement ;
2022-08-31 06:22:16 +02:00
2022-09-03 20:45:00 +02:00
public lastRect : DOMRect ;
public isDimensionsSet : boolean ;
2022-09-13 14:27:28 +02:00
public animationGroup : AnimationItemGroup ;
public size : MediaSize ;
2022-11-01 18:39:23 +01:00
public isSelectable : boolean ;
public isCanvasClean : boolean ;
public ignoreSettingDimensions : boolean ;
2022-09-13 14:27:28 +02:00
2022-11-07 15:25:27 +01:00
public forceRenderAfterSize : boolean ;
2023-01-06 20:27:29 +01:00
public middlewareHelper : MiddlewareHelper ;
public auto : boolean ;
2022-08-31 06:22:16 +02:00
constructor ( ) {
super ( ) ;
this . classList . add ( 'custom-emoji-renderer' ) ;
this . canvas = document . createElement ( 'canvas' ) ;
this . canvas . classList . add ( 'custom-emoji-canvas' ) ;
this . context = this . canvas . getContext ( '2d' ) ;
this . append ( this . canvas ) ;
2022-09-13 14:27:28 +02:00
this . playersSynced = new Map ( ) ;
2022-11-01 18:39:23 +01:00
this . clearedElements = new WeakSet ( ) ;
this . customEmojis = new Map ( ) ;
2022-09-13 14:27:28 +02:00
this . animationGroup = 'EMOJI' ;
2022-11-01 18:39:23 +01:00
this . isCanvasClean = false ;
2022-08-31 06:22:16 +02:00
}
public connectedCallback() {
2022-11-01 18:39:23 +01:00
if ( emojiRenderers . has ( this ) ) {
return ;
}
2022-08-31 06:22:16 +02:00
// this.setDimensions();
2022-09-13 14:27:28 +02:00
// animationIntersector.addAnimation(this, this.animationGroup);
2022-08-31 06:22:16 +02:00
resizeObserver . observe ( this . canvas ) ;
2022-11-01 18:39:23 +01:00
emojiRenderers . add ( this ) ;
2022-08-31 06:22:16 +02:00
this . connectedCallback = undefined ;
}
public disconnectedCallback() {
2023-01-06 20:27:29 +01:00
if ( this . isConnected || ! this . auto ) {
2022-09-20 19:34:08 +02:00
return ;
}
2023-01-13 12:10:41 +01:00
this . destroy ? . ( ) ;
2023-01-06 20:27:29 +01:00
this . disconnectedCallback = undefined ;
}
public destroy() {
// if(this.isConnected) {
// return;
// }
2022-08-31 06:22:16 +02:00
resizeObserver . unobserve ( this . canvas ) ;
2022-11-01 18:39:23 +01:00
this . customEmojis . forEach ( ( elements ) = > {
elements . forEach ( ( element ) = > {
element . clear ( ) ;
} ) ;
} ) ;
emojiRenderers . delete ( this ) ;
2022-09-13 14:27:28 +02:00
this . playersSynced . clear ( ) ;
this . middlewareHelper ? . clean ( ) ;
2022-11-01 18:39:23 +01:00
this . customEmojis . clear ( ) ;
2022-09-13 14:27:28 +02:00
2023-01-06 20:27:29 +01:00
this . destroy =
2022-11-01 18:39:23 +01:00
this . lastPausedVideo =
undefined ;
2022-08-31 06:22:16 +02:00
}
2022-11-07 15:25:27 +01:00
public getOffsets ( offsetsMap : Map < CustomEmojiElements , { top : number , left : number , width : number } [ ] > = new Map ( ) ) {
2022-09-13 14:27:28 +02:00
if ( ! this . playersSynced . size ) {
return offsetsMap ;
}
const overflowElement = findUpClassName ( this , 'scrollable' ) || this . offsetParent as HTMLElement ;
2022-09-20 19:34:08 +02:00
if ( ! overflowElement ) {
return offsetsMap ;
}
2022-09-13 14:27:28 +02:00
const overflowRect = overflowElement . getBoundingClientRect ( ) ;
const rect = this . getBoundingClientRect ( ) ;
for ( const elements of this . playersSynced . keys ( ) ) {
2022-11-01 18:39:23 +01:00
const elementsArr = Array . from ( elements ) ;
const placeholders = this . isSelectable ? elementsArr . map ( ( element ) = > element . placeholder ) : elementsArr ;
2022-09-13 14:27:28 +02:00
const { visible } = getViewportSlice ( {
overflowElement ,
overflowRect ,
2022-11-01 18:39:23 +01:00
elements : placeholders ,
2022-09-13 14:27:28 +02:00
extraSize : this.size.height * 2.5 // let's add some margin
} ) ;
const offsets = visible . map ( ( { rect : elementRect } ) = > {
const top = elementRect . top - rect . top ;
const left = elementRect . left - rect . left ;
2022-11-07 15:25:27 +01:00
return { top , left , width : elementRect.width } ;
2022-08-31 06:22:16 +02:00
} ) ;
2022-09-13 14:27:28 +02:00
if ( offsets . length ) {
offsetsMap . set ( elements , offsets ) ;
}
2022-08-31 06:22:16 +02:00
}
2022-09-13 14:27:28 +02:00
// const rect = this.getBoundingClientRect();
// const visibleRect = getVisibleRect(this, overflowElement, undefined, rect);
// const minTop = visibleRect ? visibleRect.rect.top - this.size.height : 0;
// const maxTop = Infinity;
// for(const elements of this.playersSynced.keys()) {
// const offsets = elements.map((element) => {
// const elementRect = element.getBoundingClientRect();
// const top = elementRect.top - rect.top;
// const left = elementRect.left - rect.left;
// return top >= minTop && (top + elementRect.height) <= maxTop ? {top, left} : undefined;
// }).filter(Boolean);
// if(offsets.length) {
// offsetsMap.set(elements, offsets);
// }
// }
2022-08-31 06:22:16 +02:00
return offsetsMap ;
}
public clearCanvas() {
2022-11-01 18:39:23 +01:00
if ( this . isCanvasClean ) {
return ;
}
2022-08-31 06:22:16 +02:00
const { context , canvas } = this ;
context . clearRect ( 0 , 0 , canvas . width , canvas . height ) ;
2022-11-01 18:39:23 +01:00
this . isCanvasClean = true ;
2022-08-31 06:22:16 +02:00
}
public render ( offsetsMap : ReturnType < CustomEmojiRendererElement [ ' getOffsets ' ] > ) {
2022-09-03 20:45:00 +02:00
const { context , canvas , isDimensionsSet } = this ;
if ( ! isDimensionsSet ) {
2022-11-01 18:39:23 +01:00
this . setDimensionsFromRect ( undefined , false ) ;
2022-09-03 20:45:00 +02:00
}
2022-11-01 18:39:23 +01:00
this . isCanvasClean = false ;
2022-08-31 06:22:16 +02:00
const { width , height , dpr } = canvas ;
2022-09-13 14:27:28 +02:00
for ( const [ elements , offsets ] of offsetsMap ) {
const player = this . playersSynced . get ( elements ) ;
2022-11-01 18:39:23 +01:00
const frame = syncedPlayersFrames . get ( player ) || ( player instanceof HTMLVideoElement ? player : undefined ) ;
2022-08-31 06:22:16 +02:00
if ( ! frame ) {
continue ;
}
const isImageData = frame instanceof ImageData ;
2022-11-01 18:39:23 +01:00
let frameWidth : number , frameHeight : number ;
if ( player instanceof HTMLVideoElement ) {
frameWidth = this . size . width * dpr ;
frameHeight = this . size . height * dpr ;
} else {
frameWidth = frame . width ;
frameHeight = frame . height ;
}
2022-11-07 15:25:27 +01:00
// ! check performance of scaling
const elementWidth = Math . round ( offsets [ 0 ] . width * dpr ) ;
if ( elementWidth !== frameWidth ) {
// if(this.size.width === 36) {
// console.warn('different width', elementWidth, frameWidth, this);
// }
frameWidth = elementWidth ;
frameHeight = elementWidth ;
}
2022-09-13 14:27:28 +02:00
const maxTop = height - frameHeight ;
const maxLeft = width - frameWidth ;
2022-11-01 18:39:23 +01:00
if ( ! this . clearedElements . has ( elements ) && ! this . isSelectable ) {
if ( this . isSelectable /* && false */ ) {
elements . forEach ( ( element ) = > {
element . lastChildWas ? ? = element . lastChild ;
replaceContent ( element , element . firstChild ) ;
} ) ;
} else {
elements . forEach ( ( element ) = > {
element . replaceChildren ( ) ;
} ) ;
}
2022-08-31 06:22:16 +02:00
2022-09-13 14:27:28 +02:00
this . clearedElements . add ( elements ) ;
2022-08-31 06:22:16 +02:00
}
offsets . forEach ( ( { top , left } ) = > {
top = Math . round ( top * dpr ) , left = Math . round ( left * dpr ) ;
2022-11-07 15:25:27 +01:00
if ( left < 0 || /* top > maxTop || */ left > maxLeft ) {
2022-08-31 06:22:16 +02:00
return ;
}
if ( isImageData ) {
2022-11-01 18:39:23 +01:00
context . putImageData ( frame , left , top ) ;
2022-08-31 06:22:16 +02:00
} else {
// context.clearRect(left, top, width, height);
2022-11-01 18:39:23 +01:00
context . drawImage ( frame , left , top , frameWidth , frameHeight ) ;
2022-08-31 06:22:16 +02:00
}
} ) ;
}
}
public checkForAnyFrame() {
2022-09-13 14:27:28 +02:00
for ( const player of this . playersSynced . values ( ) ) {
2022-11-01 18:39:23 +01:00
if ( syncedPlayersFrames . has ( player ) || player instanceof HTMLVideoElement ) {
2022-08-31 06:22:16 +02:00
return true ;
}
}
return false ;
}
public remove() {
2022-11-01 18:39:23 +01:00
super . remove ( ) ;
2022-09-13 14:27:28 +02:00
// this.canvas.remove();
2022-08-31 06:22:16 +02:00
}
2022-09-03 20:45:00 +02:00
// public setDimensions() {
// const {canvas} = this;
// sequentialDom.mutateElement(canvas, () => {
// const rect = canvas.getBoundingClientRect();
// this.setDimensionsFromRect(rect);
// });
// }
2022-08-31 06:22:16 +02:00
2022-11-01 18:39:23 +01:00
public setDimensionsFromRect ( rect : DOMRect = this . lastRect , forceRenderAfter = true ) {
2022-08-31 06:22:16 +02:00
const { canvas } = this ;
2022-09-03 20:45:00 +02:00
const { dpr } = canvas ;
if ( this . lastRect !== rect ) {
this . lastRect = rect ;
}
2022-11-01 18:39:23 +01:00
if ( ! rect || ! dpr || this . ignoreSettingDimensions ) {
return ;
}
const { width , height } = rect ;
// if(this.isSelectable) {
// height = this.parentElement.scrollHeight || height;
// this.style.width = width + 'px';
// this.style.height = height + 'px';
// }
const newWidth = Math . floor ( Math . round ( width * dpr ) ) ;
const newHeight = Math . floor ( Math . round ( height * dpr ) ) ;
if ( canvas . width === newWidth && canvas . height === newHeight ) {
return ;
}
canvas . width = newWidth ;
canvas . height = newHeight ;
this . isDimensionsSet = true ;
this . isCanvasClean = true ;
2022-11-07 15:25:27 +01:00
if ( this . forceRenderAfterSize || ( this . isSelectable && forceRenderAfter ) ) {
this . forceRenderAfterSize = undefined ;
2022-11-01 18:39:23 +01:00
this . forceRender ( ) ;
}
}
public forceRender() {
if ( ! this . isDimensionsSet ) {
2022-09-03 20:45:00 +02:00
return ;
}
2022-11-01 18:39:23 +01:00
if ( ! renderEmojis ( new Set ( [ this ] ) ) ) {
this . clearCanvas ( ) ;
}
}
public add (
addCustomEmojis : Parameters < typeof wrapRichText > [ 1 ] [ 'customEmojis' ] ,
lazyLoadQueue? : LazyLoadQueue | false ,
onlyThumb? : boolean ,
withThumb? : boolean
) {
const renderer = this ;
addCustomEmojis . forEach ( ( addElements , docId ) = > { // prevent adding old elements
let elements = this . customEmojis . get ( docId ) ;
if ( ! elements ) this . customEmojis . set ( docId , elements = new Set ( ) ) ;
else this . clearedElements . delete ( elements ) ;
for ( const el of addElements ) {
if ( elements . has ( el ) ) {
addElements . delete ( el ) ;
} else {
el . clean = false ;
el . renderer = renderer ;
el . elements = elements ;
el . middlewareHelper = this . middlewareHelper . get ( ) . create ( ) ;
elements . add ( el ) ;
if ( el . lastChildWas && ! el . lastChildWas . parentNode ) {
el . append ( el . lastChildWas ) ;
}
}
}
if ( ! addElements . size ) {
addCustomEmojis . delete ( docId ) ;
}
} ) ;
if ( ! addCustomEmojis . size ) {
return ;
}
const usingOwnQueue = ! ! ( ! lazyLoadQueue && lazyLoadQueue !== false && globalLazyLoadQueue ) ;
const docIds = Array . from ( addCustomEmojis . keys ( ) ) ;
const managers = rootScope . managers ;
const middleware = this . middlewareHelper . get ( ) ;
const size = this . size ;
const loadPromise = managers . appEmojiManager . getCachedCustomEmojiDocuments ( docIds ) . then ( ( docs ) = > {
if ( middleware && ! middleware ( ) ) return ;
const loadPromises : Promise < any > [ ] = [ ] ;
const wrap = ( doc : MyDocument , _loadPromises? : Promise < any > [ ] ) = > {
const docId = doc . id ;
const newElements = addCustomEmojis . get ( docId ) ;
const customEmojis = renderer . customEmojis . get ( docId ) ;
const isLottie = doc . sticker === 2 ;
const isStatic = doc . mime_type === 'video/webm' && ! IS_WEBM_SUPPORTED ;
const willHaveSyncedPlayer = ( isLottie || ( doc . sticker === 3 && this . isSelectable ) ) && ! onlyThumb && ! isStatic ;
const loadPromises : Promise < any > [ ] = [ ] ;
const newElementsArray = Array . from ( newElements ) ;
const promise = wrapSticker ( {
div : newElementsArray ,
doc ,
width : size.width ,
height : size.height ,
loop : true ,
play : CUSTOM_EMOJI_INSTANT_PLAY ,
managers ,
isCustomEmoji : true ,
group : 'none' ,
loadPromises ,
middleware ,
exportLoad : usingOwnQueue || lazyLoadQueue === false ? 2 : 1 , // 2 - export load always, 1 - do not export load if cached static
needFadeIn : false ,
loadStickerMiddleware : willHaveSyncedPlayer && middleware ? middleware . create ( ) . get ( ( ) = > {
// if(syncedPlayers.get(key) !== syncedPlayer) {
// return false;
// }
// let good = false;
// for(const middleware of syncedPlayer.middlewares) {
// if(middleware()) {
// good = true;
// break;
// }
// }
// return good;
return ! ! syncedPlayer . middlewares . size ;
} ) : undefined ,
static : isStatic ,
onlyThumb ,
withThumb : withThumb ? ? ( renderer . clearedElements . has ( customEmojis ) ? false : undefined ) ,
syncedVideo : this.isSelectable
} ) ;
if ( _loadPromises ) {
promise . then ( ( ) = > _loadPromises . push ( . . . loadPromises ) ) ;
}
const addition : {
onRender ? : ( _p : Awaited < Awaited < typeof promise > [ 'render' ] > ) = > Promise < void > ,
elements : typeof newElements
} = {
elements : newElements
} ;
if ( doc . sticker === 1 || onlyThumb || isStatic ) {
if ( this . isSelectable ) {
addition . onRender = ( ) = > Promise . all ( loadPromises ) . then ( ( ) = > {
if ( middleware && ! middleware ( ) ) return ;
newElementsArray . forEach ( ( element ) = > {
const { placeholder } = element ;
placeholder . src = ( element . firstElementChild as HTMLImageElement ) . src ;
} ) ;
} ) ;
}
return promise . then ( ( res ) = > ( { . . . res , . . . addition } ) ) ;
}
// eslint-disable-next-line prefer-const
addition . onRender = ( _p ) = > Promise . all ( loadPromises ) . then ( ( ) = > {
if ( ( middleware && ! middleware ( ) ) || ! doc . animated ) {
return ;
}
const players = Array . isArray ( _p ) ? _p as HTMLVideoElement [ ] : [ _p as RLottiePlayer ] ;
const player = Array . isArray ( players ) ? players [ 0 ] : players ;
assumeType < RLottiePlayer | HTMLVideoElement > ( player ) ;
newElementsArray . forEach ( ( element , idx ) = > {
const player = players [ idx ] || players [ 0 ] ;
element . player = player ;
if ( syncedPlayer ) {
element . syncedPlayer = syncedPlayer ;
if ( element . paused ) {
element . syncedPlayer . pausedElements . add ( element ) ;
} else if ( player . paused ) {
player . play ( ) ;
}
}
if ( element . isConnected ) {
2023-03-01 11:20:49 +01:00
animationIntersector . addAnimation ( {
animation : element ,
group : element.renderer.animationGroup ,
controlled : true
} ) ;
2022-11-01 18:39:23 +01:00
}
} ) ;
if ( player instanceof RLottiePlayer || ( player instanceof HTMLVideoElement && this . isSelectable ) ) {
syncedPlayer . player = player ;
renderer . playersSynced . set ( customEmojis , player ) ;
}
if ( player instanceof RLottiePlayer ) {
player . group = renderer . animationGroup ;
player . overrideRender ? ? = ( frame ) = > {
syncedPlayersFrames . set ( player , frame ) ;
// frames.set(containers, frame);
} ;
} else if ( player instanceof HTMLVideoElement ) {
// player.play();
// const cache = framesCache.getCache(key);
// let {width, height} = renderer.size;
// width *= dpr;
// height *= dpr;
// const onFrame = (frame: ImageBitmap | HTMLCanvasElement) => {
// topFrames.set(player, frame);
// player.requestVideoFrameCallback(callback);
// };
// let frameNo = -1, lastTime = 0;
// const callback: VideoFrameRequestCallback = (now, metadata) => {
// const time = player.currentTime;
// if(lastTime > time) {
// frameNo = -1;
// }
// const _frameNo = ++frameNo;
// lastTime = time;
// // const frameNo = Math.floor(player.currentTime * 1000 / CUSTOM_EMOJI_FRAME_INTERVAL);
// // const frameNo = metadata.presentedFrames;
// const imageBitmap = cache.framesNew.get(_frameNo);
// if(imageBitmap) {
// onFrame(imageBitmap);
// } else if(IS_IMAGE_BITMAP_SUPPORTED) {
// createImageBitmap(player, {resizeWidth: width, resizeHeight: height}).then((imageBitmap) => {
// cache.framesNew.set(_frameNo, imageBitmap);
// if(frameNo === _frameNo) onFrame(imageBitmap);
// });
// } else {
// const canvas = document.createElement('canvas');
// const context = canvas.getContext('2d');
// canvas.width = width;
// canvas.height = height;
// context.drawImage(player, 0, 0);
// cache.framesNew.set(_frameNo, canvas);
// onFrame(canvas);
// }
// };
// // player.requestVideoFrameCallback(callback);
// // setInterval(callback, CUSTOM_EMOJI_FRAME_INTERVAL);
}
if ( willHaveSyncedPlayer ) {
const dpr = getLottiePixelRatio ( this . size . width , this . size . height ) ;
renderer . canvas . dpr = dpr ;
setRenderInterval ( ) ;
}
} ) ;
let syncedPlayer : SyncedPlayer ;
const key = [ docId , size . width , size . height ] . join ( '-' ) ;
if ( willHaveSyncedPlayer ) {
syncedPlayer = syncedPlayers . get ( key ) ;
if ( ! syncedPlayer ) {
syncedPlayer = {
player : undefined ,
middlewares : new Set ( ) ,
pausedElements : new Set ( ) ,
key
} ;
syncedPlayers . set ( key , syncedPlayer ) ;
}
for ( const element of newElements ) {
const middleware = element . middlewareHelper . get ( ) ;
syncedPlayer . middlewares . add ( middleware ) ;
middleware . onClean ( ( ) = > {
syncedPlayer . middlewares . delete ( middleware ) ;
if ( ! syncedPlayer . middlewares . size ) {
if ( syncedPlayer . player ) {
const frame = syncedPlayersFrames . get ( syncedPlayer . player ) ;
if ( frame ) {
( frame as ImageBitmap ) . close ? . ( ) ;
syncedPlayersFrames . delete ( syncedPlayer . player ) ;
}
syncedPlayersFrames . delete ( syncedPlayer . player ) ;
if ( syncedPlayer . player instanceof RLottiePlayer ) {
syncedPlayer . player . overrideRender = noop ;
syncedPlayer . player . remove ( ) ;
} else if ( syncedPlayer . player instanceof HTMLVideoElement ) {
const cacheName = framesCache . generateName ( '' + element . docId , 0 , 0 , undefined , undefined ) ;
delete videosCache [ cacheName ] ;
}
syncedPlayer . player = undefined ;
}
if ( syncedPlayers . get ( syncedPlayer . key ) === syncedPlayer && syncedPlayers . delete ( syncedPlayer . key ) && ! syncedPlayers . size ) {
clearRenderInterval ( ) ;
}
}
} ) ;
}
}
return promise . then ( ( res ) = > ( { . . . res , . . . addition } ) ) ;
} ;
const missing : DocId [ ] = [ ] ;
const cachedPromises = docs . map ( ( doc , idx ) = > {
if ( ! doc ) {
missing . push ( docIds [ idx ] ) ;
return ;
}
return wrap ( doc , loadPromises ) ;
} ) . filter ( Boolean ) ;
const uncachedPromisesPromise = ! missing . length ?
Promise . resolve ( [ ] as typeof cachedPromises ) :
managers . appEmojiManager . getCustomEmojiDocuments ( missing ) . then ( ( docs ) = > {
if ( middleware && ! middleware ( ) ) return [ ] ;
return docs . filter ( Boolean ) . map ( ( doc ) = > wrap ( doc ) ) ;
} ) ;
const loadFromPromises = ( promises : typeof cachedPromises ) = > {
return Promise . all ( promises ) . then ( ( arr ) = > {
const promises = arr . map ( ( { load , onRender , elements } ) = > {
if ( ! load ) {
return ;
}
const l = ( ) = > load ( ) . then ( onRender ) ;
if ( usingOwnQueue ) {
elements . forEach ( ( element ) = > {
globalLazyLoadQueue . push ( {
div : element ,
load : ( ) = > {
elements . forEach ( ( element ) = > {
2022-11-27 15:50:02 +01:00
globalLazyLoadQueue . delete ( { div : element } ) ;
2022-11-01 18:39:23 +01:00
} ) ;
return l ( ) ;
}
} ) ;
} ) ;
} else {
return l ( ) ;
}
} ) ;
return Promise . all ( promises ) ;
} ) ;
} ;
const load = ( ) = > {
if ( middleware && ! middleware ( ) ) return ;
const cached = loadFromPromises ( cachedPromises ) ;
const uncached = uncachedPromisesPromise . then ( ( promises ) = > loadFromPromises ( promises ) ) ;
return Promise . all ( [ cached , uncached ] ) ;
} ;
if ( lazyLoadQueue ) {
lazyLoadQueue . push ( {
div : renderer.canvas ,
load
} ) ;
} else {
load ( ) ;
}
return Promise . all ( cachedPromises ) . then ( ( ) = > Promise . all ( loadPromises ) ) . then ( ( ) = > { } ) ;
} ) ;
// recordPromise(loadPromise, 'render emojis: ' + docIds.length);
return loadPromise ;
}
public static create ( options : CustomEmojiRendererElementOptions ) {
const renderer = new CustomEmojiRendererElement ( ) ;
renderer . animationGroup = options . animationGroup ;
renderer . size = options . customEmojiSize || mediaSizes . active . customEmoji ;
renderer . isSelectable = options . isSelectable ;
if ( options . wrappingDraft ) {
renderer . contentEditable = 'false' ;
renderer . style . height = 'inherit' ;
}
// const middleware = () => !!renderer.disconnectedCallback && (!options.middleware || options.middleware());
let middleware = options . middleware ;
if ( middleware ) {
renderer . middlewareHelper = middleware . create ( ) ;
middleware = renderer . middlewareHelper . get ( ) ;
middleware . onDestroy ( ( ) = > {
2023-01-06 20:27:29 +01:00
renderer . destroy ? . ( ) ;
2022-11-01 18:39:23 +01:00
} ) ;
} else {
2023-01-06 20:27:29 +01:00
// console.error('no middleware', this, options);
renderer . auto = true ;
2022-11-01 18:39:23 +01:00
renderer . middlewareHelper = getMiddleware ( ) ;
}
return renderer ;
2022-08-31 06:22:16 +02:00
}
}
2022-09-13 14:27:28 +02:00
type CustomEmojiRenderer = CustomEmojiRendererElement ;
type SyncedPlayer = {
2022-11-01 18:39:23 +01:00
player : RLottiePlayer | HTMLVideoElement ,
middlewares : Set < Middleware > ,
2022-09-13 14:27:28 +02:00
pausedElements : Set < CustomEmojiElement > ,
key : string
} ;
type CustomEmojiFrame = Parameters < RLottiePlayer [ ' overrideRender ' ] > [ 0 ] | HTMLVideoElement ;
const CUSTOM_EMOJI_INSTANT_PLAY = true ; // do not wait for animationIntersector
let emojiRenderInterval : number ;
2022-11-01 18:39:23 +01:00
const emojiRenderers : Set < CustomEmojiRenderer > = new Set ( ) ;
2022-09-13 14:27:28 +02:00
const syncedPlayers : Map < string , SyncedPlayer > = new Map ( ) ;
const syncedPlayersFrames : Map < RLottiePlayer | HTMLVideoElement , CustomEmojiFrame > = new Map ( ) ;
2022-11-01 18:39:23 +01:00
export const renderEmojis = ( renderers = emojiRenderers ) = > {
const r = Array . from ( renderers ) ;
const t = r . filter ( ( r ) = > r . isConnected && r . checkForAnyFrame ( ) && ! r . ignoreSettingDimensions ) ;
2022-08-31 06:22:16 +02:00
if ( ! t . length ) {
2022-11-01 18:39:23 +01:00
return false ;
2022-08-31 06:22:16 +02:00
}
2022-09-13 14:27:28 +02:00
const o = t . map ( ( renderer ) = > {
2022-11-01 18:39:23 +01:00
const paused = [ . . . renderer . playersSynced . values ( ) ] . reduce ( ( acc , v ) = > acc + + ! ! v . paused , 0 ) ;
if ( renderer . playersSynced . size === paused ) {
return ;
}
2022-09-13 14:27:28 +02:00
const offsets = renderer . getOffsets ( ) ;
2022-11-01 18:39:23 +01:00
if ( offsets . size ) {
return [ renderer , offsets ] as const ;
}
2022-09-13 14:27:28 +02:00
} ) . filter ( Boolean ) ;
2022-08-31 06:22:16 +02:00
2022-09-13 14:27:28 +02:00
for ( const [ renderer ] of o ) {
renderer . clearCanvas ( ) ;
2022-08-31 06:22:16 +02:00
}
2022-09-13 14:27:28 +02:00
for ( const [ renderer , offsets ] of o ) {
renderer . render ( offsets ) ;
2022-08-31 06:22:16 +02:00
}
2022-11-01 18:39:23 +01:00
return true ;
2022-08-31 06:22:16 +02:00
} ;
const CUSTOM_EMOJI_FPS = 60 ;
const CUSTOM_EMOJI_FRAME_INTERVAL = 1000 / CUSTOM_EMOJI_FPS ;
const setRenderInterval = ( ) = > {
2022-09-13 14:27:28 +02:00
if ( emojiRenderInterval ) {
2022-08-31 06:22:16 +02:00
return ;
}
2022-09-13 14:27:28 +02:00
emojiRenderInterval = window . setInterval ( renderEmojis , CUSTOM_EMOJI_FRAME_INTERVAL ) ;
renderEmojis ( ) ;
2022-08-31 06:22:16 +02:00
} ;
const clearRenderInterval = ( ) = > {
2022-09-13 14:27:28 +02:00
if ( ! emojiRenderInterval ) {
2022-08-31 06:22:16 +02:00
return ;
}
2022-09-13 14:27:28 +02:00
clearInterval ( emojiRenderInterval ) ;
emojiRenderInterval = undefined ;
2022-08-31 06:22:16 +02:00
} ;
2022-09-13 14:27:28 +02:00
( window as any ) . syncedPlayers = syncedPlayers ;
2022-11-01 18:39:23 +01:00
( window as any ) . emojiRenderers = emojiRenderers ;
2022-08-31 06:22:16 +02:00
customElements . define ( 'custom-emoji-element' , CustomEmojiElement ) ;
customElements . define ( 'custom-emoji-renderer-element' , CustomEmojiRendererElement ) ;
2022-04-25 16:54:30 +02:00
2022-11-01 18:39:23 +01:00
type CustomEmojiRendererElementOptions = Partial < {
loadPromises : Promise < any > [ ] ,
customEmojiRenderer : CustomEmojiRendererElement ,
isSelectable : boolean ,
wrappingDraft : boolean
2023-01-06 20:27:29 +01:00
} > & WrapSomethingOptions ;
2022-11-01 18:39:23 +01:00
2022-04-25 16:54:30 +02:00
/ * *
* * Expecting correctly sorted nested entities ( RichTextProcessor . sortEntities )
* /
export default function wrapRichText ( text : string , options : Partial < {
entities : MessageEntity [ ] ,
contextSite : string ,
highlightUsername : string ,
noLinks : boolean ,
noLinebreaks : boolean ,
noCommands : boolean ,
wrappingDraft : boolean ,
2022-08-04 08:49:54 +02:00
// mustWrapEmoji: boolean,
2022-04-25 16:54:30 +02:00
fromBot : boolean ,
noTextFormat : boolean ,
passEntities : Partial < {
[ _ in MessageEntity [ '_' ] ] : boolean
} > ,
2023-03-03 16:47:02 +01:00
maxMediaTimestamp : number ,
2022-04-25 16:54:30 +02:00
noEncoding : boolean ,
2022-11-01 18:39:23 +01:00
isSelectable : boolean ,
2022-04-25 16:54:30 +02:00
contextHashtag? : string ,
2022-09-13 14:27:28 +02:00
// ! recursive, do not provide
2022-04-25 16:54:30 +02:00
nasty ? : {
i : number ,
usedLength : number ,
text : string ,
lastEntity? : MessageEntity
} ,
2022-08-31 06:22:16 +02:00
voodoo? : boolean ,
2022-11-01 18:39:23 +01:00
customEmojis? : Map < DocId , CustomEmojiElements > ,
customWraps? : Set < HTMLElement > ,
} > & CustomEmojiRendererElementOptions = { } ) {
2022-04-25 16:54:30 +02:00
const fragment = document . createDocumentFragment ( ) ;
if ( ! text ) {
return fragment ;
}
const nasty = options . nasty ? ? = {
i : 0 ,
usedLength : 0 ,
text
} ;
2022-11-01 18:39:23 +01:00
const wrapSomething = ( wrapElement : HTMLElement , noFiller? : boolean ) = > {
const element = document . createElement ( 'span' ) ;
// element.append(BOM, a, BOM);
if ( options . wrappingDraft ) {
element . contentEditable = 'false' ;
}
// element.style.display = 'inline-block';
element . classList . add ( 'input-something' ) ;
element . append ( /* BOM, */ wrapElement ) ;
( lastElement || fragment ) . append ( element ) ;
wrapElement . classList . add ( 'input-selectable' ) ;
// if(wrapElement instanceof HTMLImageElement) {
// element.prepend(f());
// } else {
! noFiller && wrapElement . append ( createCustomFiller ( true ) ) ;
// }
customWraps . add ( element ) ;
return element ;
} ;
options . isSelectable || = options . wrappingDraft ;
const customEmojis = options . customEmojis ? ? = new Map ( ) as Map < DocId , CustomEmojiElements > ;
const customWraps = options . customWraps ? ? = new Set ( ) ;
2022-08-31 06:22:16 +02:00
2022-04-25 16:54:30 +02:00
const entities = options . entities ? ? = parseEntities ( nasty . text ) ;
const passEntities = options . passEntities ? ? = { } ;
const contextSite = options . contextSite ? ? = 'Telegram' ;
const contextExternal = contextSite !== 'Telegram' ;
const textLength = nasty . text . length ;
const length = entities . length ;
let lastElement : HTMLElement | DocumentFragment ;
for ( ; nasty . i < length ; ++ nasty . i ) {
let entity = entities [ nasty . i ] ;
// * check whether text was sliced
// TODO: consider about moving it to other function
if ( entity . offset >= textLength ) {
if ( entity . _ !== 'messageEntityCaret' ) { // * can set caret to the end
continue ;
}
} else if ( ( entity . offset + entity . length ) > textLength ) {
entity = copy ( entity ) ;
2022-09-16 19:44:10 +02:00
// entity.length = entity.offset + entity.length - textLength;
entity . length = textLength - entity . offset ;
2022-04-25 16:54:30 +02:00
}
if ( entity . length ) {
nasty . lastEntity = entity ;
}
let nextEntity = entities [ nasty . i + 1 ] ;
const startOffset = entity . offset ;
const endOffset = startOffset + entity . length ;
const endPartOffset = Math . min ( endOffset , nextEntity ? . offset ? ? 0xFFFF ) ;
const fullEntityText = nasty . text . slice ( startOffset , endOffset ) ;
const sliced = nasty . text . slice ( startOffset , endPartOffset ) ;
let partText = sliced ;
if ( nasty . usedLength < startOffset ) {
( lastElement || fragment ) . append ( nasty . text . slice ( nasty . usedLength , startOffset ) ) ;
}
if ( lastElement ) {
lastElement = fragment ;
}
nasty . usedLength = endPartOffset ;
2022-08-04 08:49:54 +02:00
let element : HTMLElement ,
2022-04-25 16:54:30 +02:00
property : 'textContent' | 'alt' = 'textContent' ,
usedText = false ;
switch ( entity . _ ) {
case 'messageEntityBold' : {
if ( ! options . noTextFormat ) {
if ( options . wrappingDraft ) {
element = document . createElement ( 'span' ) ;
2022-11-01 18:39:23 +01:00
// element.style.fontWeight = 'bold';
element . style . fontFamily = 'markup-bold' ;
2022-04-25 16:54:30 +02:00
} else {
element = document . createElement ( 'strong' ) ;
}
}
break ;
}
case 'messageEntityItalic' : {
if ( ! options . noTextFormat ) {
if ( options . wrappingDraft ) {
element = document . createElement ( 'span' ) ;
2022-11-01 18:39:23 +01:00
// element.style.fontStyle = 'italic';
element . style . fontFamily = 'markup-italic' ;
2022-04-25 16:54:30 +02:00
} else {
element = document . createElement ( 'em' ) ;
}
}
break ;
}
case 'messageEntityStrike' : {
if ( options . wrappingDraft ) {
element = document . createElement ( 'span' ) ;
2022-11-01 18:39:23 +01:00
// const styleName = IS_SAFARI ? 'text-decoration' : 'text-decoration-line';
// element.style.cssText = `${styleName}: line-through;`;
element . style . fontFamily = 'markup-strikethrough' ;
2022-12-02 16:20:27 +01:00
} else /* if(!options.noTextFormat) */ {
2022-04-25 16:54:30 +02:00
element = document . createElement ( 'del' ) ;
}
break ;
}
case 'messageEntityUnderline' : {
if ( options . wrappingDraft ) {
element = document . createElement ( 'span' ) ;
2022-11-01 18:39:23 +01:00
// const styleName = IS_SAFARI ? 'text-decoration' : 'text-decoration-line';
// element.style.cssText = `${styleName}: underline;`;
element . style . fontFamily = 'markup-underline' ;
2022-04-25 16:54:30 +02:00
} else if ( ! options . noTextFormat ) {
element = document . createElement ( 'u' ) ;
}
break ;
}
2022-08-04 08:49:54 +02:00
2022-04-25 16:54:30 +02:00
case 'messageEntityPre' :
case 'messageEntityCode' : {
if ( options . wrappingDraft ) {
element = document . createElement ( 'span' ) ;
2022-11-01 18:39:23 +01:00
// element.style.fontFamily = 'var(--font-monospace)';
element . style . fontFamily = 'markup-monospace' ;
2022-04-25 16:54:30 +02:00
} else if ( ! options . noTextFormat ) {
element = document . createElement ( 'code' ) ;
}
2022-08-04 08:49:54 +02:00
2022-04-25 16:54:30 +02:00
break ;
}
2022-08-04 08:49:54 +02:00
2022-04-25 16:54:30 +02:00
// case 'messageEntityPre': {
// if(options.wrappingDraft) {
// element = document.createElement('span');
// element.style.fontFamily = 'var(--font-monospace)';
// } else if(!options.noTextFormat) {
// element = document.createElement('pre');
// const inner = document.createElement('code');
// if(entity.language) {
// inner.className = 'language-' + entity.language;
// inner.textContent = entityText;
// usedText = true;
// }
// }
2022-08-04 08:49:54 +02:00
2022-04-25 16:54:30 +02:00
// break;
// }
case 'messageEntityHighlight' : {
element = document . createElement ( 'i' ) ;
element . className = 'text-highlight' ;
break ;
}
case 'messageEntityBotCommand' : {
// if(!(options.noLinks || options.noCommands || contextExternal)/* && !entity.unsafe */) {
if ( ! options . noLinks && passEntities [ entity . _ ] ) {
let command = fullEntityText . slice ( 1 ) ;
let bot : string | boolean ;
let atPos : number ;
if ( ( atPos = command . indexOf ( '@' ) ) !== - 1 ) {
bot = command . slice ( atPos + 1 ) ;
command = command . slice ( 0 , atPos ) ;
} else {
bot = options . fromBot ;
}
element = document . createElement ( 'a' ) ;
( element as HTMLAnchorElement ) . href = encodeEntities ( 'tg://bot_command?command=' + encodeURIComponent ( command ) + ( bot ? '&bot=' + encodeURIComponent ( bot ) : '' ) ) ;
if ( ! contextExternal ) {
element . setAttribute ( 'onclick' , 'execBotCommand(this)' ) ;
}
}
break ;
}
2022-08-31 06:22:16 +02:00
case 'messageEntityCustomEmoji' : {
if ( ! IS_CUSTOM_EMOJI_SUPPORTED ) {
break ;
}
2022-09-16 19:44:10 +02:00
while ( nextEntity ? . _ === 'messageEntityEmoji' && nextEntity . offset < endOffset ) {
2022-08-31 06:22:16 +02:00
++ nasty . i ;
nasty . lastEntity = nextEntity ;
nasty . usedLength += nextEntity . length ;
nextEntity = entities [ nasty . i + 1 ] ;
}
2022-11-01 18:39:23 +01:00
const customEmojiElement = element = CustomEmojiElement . create ( entity . document_id ) ;
const { docId } = customEmojiElement ;
let set = customEmojis . get ( docId ) ;
if ( ! set ) customEmojis . set ( docId , set = new Set ( ) ) ;
set . add ( customEmojiElement ) ;
customEmojiElement . dataset . stickerEmoji = fullEntityText ;
if ( options . wrappingDraft ) {
element = document . createElement ( 'img' ) ;
( element as HTMLImageElement ) . alt = fullEntityText ;
for ( const i in customEmojiElement . dataset ) {
element . dataset [ i ] = customEmojiElement . dataset [ i ] ;
}
( element as any ) . customEmojiElement = customEmojiElement ;
customEmojiElement . placeholder = element as HTMLImageElement ;
element . classList . add ( 'custom-emoji-placeholder' ) ;
( element as HTMLImageElement ) . src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAAtJREFUGFdjYAACAAAFAAGq1chRAAAAAElFTkSuQmCC' ;
property = 'alt' ;
break ;
}
if ( options . isSelectable ) {
// const s = document.createElement('span');
// s.append(fullEntityText);
// element.append(s);
// element.textContent = fullEntityText;
// element.textContent = 'a';
// element.contentEditable = 'false';
// const x = f();
// x.style.display = 'inline-block';
// x.contentEditable = 'false';
// (lastElement || fragment).append(BOM);
// (lastElement || fragment).append(x);
element = wrapSomething ( element , ! ! options . customEmojiRenderer ) ;
// const a = element;
// element = document.createElement('span');
// element.append(BOM, a, BOM);
// element.contentEditable = 'false';
}
2022-08-31 06:22:16 +02:00
property = 'alt' ;
break ;
}
2022-04-25 16:54:30 +02:00
case 'messageEntityEmoji' : {
let isSupported = IS_EMOJI_SUPPORTED ;
if ( isSupported ) {
for ( const version in EmojiVersions ) {
if ( version ) {
2022-06-27 23:14:52 +02:00
const emojiData = EmojiVersions [ version as EMOJI_VERSION ] ;
if ( emojiData . hasOwnProperty ( entity . unicode ) && ! EMOJI_VERSIONS_SUPPORTED [ version as EMOJI_VERSION ] ) {
2022-04-25 16:54:30 +02:00
isSupported = false ;
break ;
}
}
}
}
2022-08-04 08:49:54 +02:00
// if(!(options.wrappingDraft && isSupported)) { // * fix safari emoji
2022-04-25 16:54:30 +02:00
if ( ! isSupported ) { // no wrapping needed
// if(isSupported) { // ! contenteditable="false" нужен для поля ввода, иначе там будет меняться шрифт в Safari, или же рендерить смайлик напрямую, без контейнера
// insertPart(entity, '<span class="emoji">', '</span>');
// } else {
2022-08-04 08:49:54 +02:00
element = document . createElement ( 'img' ) ;
( element as HTMLImageElement ) . src = ` assets/img/emoji/ ${ entity . unicode } .png ` ;
property = 'alt' ;
element . className = 'emoji' ;
2022-11-01 18:39:23 +01:00
// if(options.isSelectable) {
// usedText = true;
// (element as HTMLImageElement).alt = partText;
// element = wrapSomething(element);
// }
// const a = element;
// a.contentEditable = 'false';
// element = document.createElement('span');
// element.append(a);
// element.contentEditable = 'false';
2022-04-25 16:54:30 +02:00
// }
2022-08-04 08:49:54 +02:00
// } else if(options.mustWrapEmoji) {
2022-04-25 16:54:30 +02:00
} else if ( ! options . wrappingDraft ) {
element = document . createElement ( 'span' ) ;
element . className = 'emoji' ;
} / * else if ( ! IS_SAFARI ) {
insertPart ( entity , '<span class="emoji" contenteditable="false">' , '</span>' ) ;
} * /
/ * i f ( ! i s S u p p o r t e d ) {
insertPart ( entity , ` <img src="assets/img/emoji/ ${ entity . unicode } .png" alt=" ` , ` " class="emoji"> ` ) ;
} * /
break ;
}
2022-08-04 08:49:54 +02:00
2022-04-25 16:54:30 +02:00
case 'messageEntityCaret' : {
element = document . createElement ( 'span' ) ;
element . className = 'composer-sel' ;
break ;
}
// case 'messageEntityLinebreak': {
// if(options.noLinebreaks) {
// insertPart(entity, ' ');
// } else {
// insertPart(entity, '<br/>');
// }
2022-08-04 08:49:54 +02:00
2022-04-25 16:54:30 +02:00
// break;
// }
case 'messageEntityUrl' :
case 'messageEntityTextUrl' : {
if ( ! ( options . noLinks && ! passEntities [ entity . _ ] ) ) {
// let inner: string;
let url : string = ( entity as MessageEntity . messageEntityTextUrl ) . url || fullEntityText ;
let masked = false ;
let onclick : string ;
const wrapped = wrapUrl ( url , true ) ;
url = wrapped . url ;
onclick = wrapped . onclick ;
if ( entity . _ === 'messageEntityTextUrl' ) {
2022-08-04 08:49:54 +02:00
if ( nextEntity ? . _ === 'messageEntityUrl' &&
nextEntity . length === entity . length &&
2022-04-25 16:54:30 +02:00
nextEntity . offset === entity . offset ) {
2022-08-31 06:22:16 +02:00
nasty . lastEntity = nextEntity ;
++ nasty . i ;
2022-04-25 16:54:30 +02:00
}
if ( url !== fullEntityText ) {
masked = true ;
}
} else {
2022-08-04 08:49:54 +02:00
// inner = encodeEntities(replaceUrlEncodings(entityText));
2022-04-25 16:54:30 +02:00
}
const currentContext = ! ! onclick ;
if ( ! onclick && masked && ! currentContext ) {
onclick = 'showMaskedAlert' ;
}
if ( options . wrappingDraft ) {
onclick = undefined ;
}
2022-08-04 08:49:54 +02:00
const href = ( currentContext || typeof electronHelpers === 'undefined' ) ?
url :
` javascript:electronHelpers.openExternal(' ${ url } '); ` ;
2022-04-25 16:54:30 +02:00
element = document . createElement ( 'a' ) ;
element . className = 'anchor-url' ;
( element as HTMLAnchorElement ) . href = href ;
if ( ! ( currentContext || typeof electronHelpers !== 'undefined' ) ) {
setBlankToAnchor ( element as HTMLAnchorElement ) ;
}
if ( onclick ) {
element . setAttribute ( 'onclick' , onclick + '(this)' ) ;
}
}
break ;
}
case 'messageEntityEmail' : {
if ( ! options . noLinks ) {
element = document . createElement ( 'a' ) ;
( element as HTMLAnchorElement ) . href = encodeEntities ( 'mailto:' + fullEntityText ) ;
setBlankToAnchor ( element as HTMLAnchorElement ) ;
}
break ;
}
2022-08-04 08:49:54 +02:00
2022-04-25 16:54:30 +02:00
case 'messageEntityHashtag' : {
const contextUrl = ! options . noLinks && SITE_HASHTAGS [ contextSite ] ;
if ( contextUrl ) {
const hashtag = fullEntityText . slice ( 1 ) ;
element = document . createElement ( 'a' ) ;
element . className = 'anchor-hashtag' ;
( element as HTMLAnchorElement ) . href = contextUrl . replace ( '{1}' , encodeURIComponent ( hashtag ) ) ;
if ( contextExternal ) {
setBlankToAnchor ( element as HTMLAnchorElement ) ;
} else {
element . setAttribute ( 'onclick' , 'searchByHashtag(this)' ) ;
}
}
break ;
}
case 'messageEntityMentionName' : {
if ( ! ( options . noLinks && ! passEntities [ entity . _ ] ) ) {
element = document . createElement ( 'a' ) ;
2022-06-17 18:01:43 +02:00
( element as HTMLAnchorElement ) . href = buildURLHash ( '' + entity . user_id ) ;
2022-04-25 16:54:30 +02:00
element . className = 'follow' ;
element . dataset . follow = '' + entity . user_id ;
}
break ;
}
case 'messageEntityMention' : {
// const contextUrl = !options.noLinks && siteMentions[contextSite];
if ( ! options . noLinks ) {
const username = fullEntityText . slice ( 1 ) ;
2023-01-06 20:27:29 +01:00
element = wrapTelegramUrlToAnchor ( 't.me/' + username ) ;
2022-04-25 16:54:30 +02:00
element . className = 'mention' ;
// insertPart(entity, `<a class="mention" href="${contextUrl.replace('{1}', encodeURIComponent(username))}"${contextExternal ? ' target="_blank" rel="noopener noreferrer"' : ''}>`, '</a>');
}
2022-08-04 08:49:54 +02:00
2022-04-25 16:54:30 +02:00
break ;
}
case 'messageEntitySpoiler' : {
if ( options . noTextFormat ) {
2022-06-17 18:01:43 +02:00
const encoded = encodeSpoiler ( nasty . text , entity ) ;
nasty . text = encoded . text ;
partText = encoded . entityText ;
2022-09-16 19:44:10 +02:00
if ( endPartOffset !== endOffset ) {
nasty . usedLength += endOffset - endPartOffset ;
}
2022-08-31 06:22:16 +02:00
let n : MessageEntity ;
for ( ; n = entities [ nasty . i + 1 ] , n && n . offset < endOffset ; ) {
// nasty.usedLength += n.length;
++ nasty . i ;
nasty . lastEntity = n ;
nextEntity = entities [ nasty . i + 1 ] ;
}
2022-04-25 16:54:30 +02:00
} else if ( options . wrappingDraft ) {
element = document . createElement ( 'span' ) ;
2022-11-01 18:39:23 +01:00
// element.style.fontFamily = 'spoiler';
element . style . fontFamily = 'markup-spoiler' ;
2022-04-25 16:54:30 +02:00
} else {
const container = document . createElement ( 'span' ) ;
container . className = 'spoiler' ;
element = document . createElement ( 'span' ) ;
element . className = 'spoiler-text' ;
element . textContent = partText ;
usedText = true ;
container . append ( element ) ;
fragment . append ( container ) ;
2022-07-23 00:41:11 +02:00
container [ ` on ${ CLICK_EVENT_NAME } ` ] = ( window as any ) . onSpoilerClick ;
2022-04-25 16:54:30 +02:00
}
2022-08-04 08:49:54 +02:00
2022-04-25 16:54:30 +02:00
break ;
}
2023-03-03 16:47:02 +01:00
case 'messageEntityTimestamp' : {
if ( ! options . maxMediaTimestamp || entity . time > options . maxMediaTimestamp ) {
break ;
}
element = document . createElement ( 'a' ) ;
element . classList . add ( 'timestamp' ) ;
element . dataset . timestamp = '' + entity . time ;
( element as HTMLAnchorElement ) . href = '#' ;
element . setAttribute ( 'onclick' , 'setMediaTimestamp(this)' ) ;
2023-03-07 22:25:07 +01:00
if ( options . maxMediaTimestamp === Infinity ) {
element . classList . add ( 'is-disabled' ) ;
}
2023-03-03 16:47:02 +01:00
break ;
}
2022-04-25 16:54:30 +02:00
}
2022-11-01 18:39:23 +01:00
if ( ! usedText && partText ) {
2022-04-25 16:54:30 +02:00
if ( element ) {
// @ts-ignore
element [ property ] = partText ;
} else {
( element || fragment ) . append ( partText ) ;
}
}
2022-11-01 18:39:23 +01:00
if ( element && ! element . parentNode ) {
2022-04-25 16:54:30 +02:00
( lastElement || fragment ) . append ( element ) ;
}
2022-07-21 02:12:57 +02:00
while ( nextEntity && nextEntity . offset < endOffset ) {
2022-04-25 16:54:30 +02:00
++ nasty . i ;
( element || fragment ) . append ( wrapRichText ( nasty . text , {
. . . options ,
voodoo : true
} ) ) ;
nextEntity = entities [ nasty . i + 1 ] ;
}
2022-11-01 18:39:23 +01:00
// if(!element?.parentNode) {
2022-04-25 16:54:30 +02:00
// (lastElement || fragment).append(element ?? partText);
// }
2022-06-28 03:41:29 +02:00
if ( nasty . usedLength <= endOffset ) {
if ( nasty . usedLength < endOffset ) {
( element || fragment ) . append ( nasty . text . slice ( nasty . usedLength , endOffset ) ) ;
nasty . usedLength = endOffset ;
}
2022-08-04 08:49:54 +02:00
2022-06-28 03:41:29 +02:00
lastElement = fragment ;
nasty . lastEntity = undefined ;
} else if ( entity . length > partText . length && element ) {
2022-04-25 16:54:30 +02:00
lastElement = element ;
} else {
lastElement = fragment ;
}
if ( options . voodoo ) {
return fragment ;
}
}
if ( nasty . lastEntity ) {
nasty . usedLength = nasty . lastEntity . offset + nasty . lastEntity . length ;
}
if ( nasty . usedLength < textLength ) {
( lastElement || fragment ) . append ( nasty . text . slice ( nasty . usedLength ) ) ;
}
2022-11-01 18:39:23 +01:00
if ( ( ! options . wrappingDraft || options . customEmojiRenderer ) && customEmojis . size ) {
let renderer = options . customEmojiRenderer ;
if ( ! renderer ) {
renderer = CustomEmojiRendererElement . create ( options ) ;
fragment . prepend ( renderer ) ;
2022-09-13 14:27:28 +02:00
}
2022-11-01 18:39:23 +01:00
const loadPromise = renderer . add ( customEmojis , options . lazyLoadQueue , options . wrappingDraft ) ;
options . loadPromises ? . push ( loadPromise ) ;
// recordPromise(loadPromise, 'render emojis: ' + docIds.length);
}
2022-09-13 14:27:28 +02:00
2022-11-01 18:39:23 +01:00
if ( customWraps . size ) {
insertCustomFillers ( customWraps ) ;
}
2022-08-31 06:22:16 +02:00
2022-11-01 18:39:23 +01:00
return fragment ;
}
2022-08-31 06:22:16 +02:00
2022-11-01 18:39:23 +01:00
export const createCustomFiller = ( notFiller? : boolean ) = > {
const x = document . createElement ( 'span' ) ;
x . classList . add ( notFiller ? 'input-filler2' : 'input-filler' ) ;
x . textContent = BOM ;
return x ;
} ;
2022-08-31 06:22:16 +02:00
2022-11-01 18:39:23 +01:00
export function isCustomFillerNeededBySiblingNode ( node : ChildNode ) {
if (
// !node?.textContent ||
// node.textContent.endsWith('\n') ||
node ? . textContent !== BOM ||
( node as HTMLElement ) ? . getAttribute ? . ( 'contenteditable' ) === 'false'
) {
// if(!node || (node as HTMLElement).firstElementChild || node.textContent.endsWith('\n')) {
if ( ! node || node . textContent !== BOM || ( node as HTMLElement ) . firstElementChild ) {
return 2 ;
} else if ( node . nodeType === node . ELEMENT_NODE ) {
return 1 ;
} / * else if ( node . nodeType === node . TEXT_NODE && ! node . nodeValue ) {
( node as CharacterData ) . insertData ( 0 , BOM ) ;
} * /
}
2022-08-31 06:22:16 +02:00
2022-11-01 18:39:23 +01:00
return 0 ;
}
2022-08-31 06:22:16 +02:00
2022-11-01 18:39:23 +01:00
export function insertCustomFillers ( elements : Iterable < HTMLElement > ) {
const check = ( element : HTMLElement , node : ChildNode , method : 'before' | 'after' ) = > {
const needed = isCustomFillerNeededBySiblingNode ( node ) ;
if ( needed === 2 ) {
element [ method ] ( createCustomFiller ( ) ) ;
} else if ( needed === 1 ) {
node . appendChild ( document . createTextNode ( BOM ) ) ;
}
} ;
2022-08-31 06:22:16 +02:00
2022-11-01 18:39:23 +01:00
for ( const element of elements ) {
const { previousSibling , nextSibling } = element ;
check ( element , previousSibling , 'before' ) ;
check ( element , nextSibling , 'after' ) ;
2022-08-31 06:22:16 +02:00
}
2022-04-25 16:54:30 +02:00
}
2022-06-28 03:41:29 +02:00
( window as any ) . wrapRichText = wrapRichText ;