Telegram Web K with changes to work inside I2P
https://web.telegram.i2p/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
325 lines
9.2 KiB
325 lines
9.2 KiB
/* |
|
* https://github.com/morethanwords/tweb |
|
* Copyright (C) 2019-2021 Eduard Kuzmenko |
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE |
|
*/ |
|
|
|
import Scrollable from '../components/scrollable'; |
|
import rootScope from '../lib/rootScope'; |
|
import {animate} from './animation'; |
|
import {drawCircleFromStart} from './canvas/drawCircle'; |
|
import roundRect from './canvas/roundRect'; |
|
import Shimmer from './canvas/shimmer'; |
|
import customProperties from './dom/customProperties'; |
|
import easeInOutSine from './easing/easeInOutSine'; |
|
import liteMode from './liteMode'; |
|
import mediaSizes from './mediaSizes'; |
|
|
|
export default class DialogsPlaceholder { |
|
private canvas: HTMLCanvasElement; |
|
private ctx: CanvasRenderingContext2D; |
|
private shimmer: Shimmer; |
|
private tempId: number; |
|
private detachTime: number; |
|
|
|
private length: number; |
|
private dialogHeight: number; |
|
private availableLength: number; |
|
|
|
private avatarSize: number; |
|
private marginVertical: number; |
|
private totalHeight: number; |
|
private lineHeight: number; |
|
private lineBorderRadius: number; |
|
private lineMarginVertical: number; |
|
private statusWidth: number; |
|
private generatedValues: { |
|
firstLineWidth: number, |
|
secondLineWidth: number, |
|
statusWidth: number |
|
}[]; |
|
|
|
private getRectFrom: () => DOMRectEditable; |
|
private onRemove: () => void; |
|
private blockScrollable: Scrollable; |
|
|
|
constructor(sizes: Partial<{ |
|
avatarSize: number, |
|
marginVertical: number, |
|
totalHeight: number |
|
}> = {}) { |
|
this.shimmer = new Shimmer(); |
|
this.tempId = 0; |
|
this.canvas = document.createElement('canvas'); |
|
this.canvas.classList.add('dialogs-placeholder-canvas'); |
|
this.ctx = this.canvas.getContext('2d'); |
|
|
|
this.generatedValues = []; |
|
this.avatarSize = sizes.avatarSize ?? 54; |
|
this.marginVertical = sizes.marginVertical ?? 9; |
|
this.totalHeight = sizes.totalHeight ?? (this.avatarSize + this.marginVertical * 2); |
|
this.lineHeight = 10; |
|
this.lineBorderRadius = 6; |
|
this.lineMarginVertical = 8; |
|
this.statusWidth = 24; |
|
} |
|
|
|
public attach({container, rect, getRectFrom, onRemove, blockScrollable}: { |
|
container: HTMLElement, |
|
rect?: ReturnType<DialogsPlaceholder['getRectFrom']>, |
|
getRectFrom?: HTMLElement | DialogsPlaceholder['getRectFrom'], |
|
onRemove?: DialogsPlaceholder['onRemove'], |
|
blockScrollable?: DialogsPlaceholder['blockScrollable'] |
|
}) { |
|
const {canvas} = this; |
|
|
|
this.onRemove = onRemove; |
|
this.getRectFrom = typeof(getRectFrom) === 'function' ? getRectFrom : (getRectFrom || container).getBoundingClientRect.bind(getRectFrom || container); |
|
if(this.blockScrollable = blockScrollable) { |
|
blockScrollable.container.style.overflowY = 'hidden'; |
|
} |
|
|
|
this.updateCanvasSize(rect); |
|
this.startAnimation(); |
|
container.append(canvas); |
|
} |
|
|
|
public detach(availableLength: number) { |
|
if(this.detachTime) { |
|
return; |
|
} |
|
|
|
this.availableLength = availableLength; |
|
this.detachTime = Date.now(); |
|
|
|
if(!liteMode.isAvailable('animations')) { |
|
this.remove(); |
|
} |
|
} |
|
|
|
public remove() { |
|
this.stopAnimation(); |
|
|
|
if(this.canvas.parentElement) { |
|
this.canvas.remove(); |
|
|
|
if(this.blockScrollable) { |
|
this.blockScrollable.container.style.overflowY = ''; |
|
this.blockScrollable = undefined; |
|
} |
|
} |
|
|
|
this.onRemove?.(); |
|
this.onRemove = undefined; |
|
} |
|
|
|
private updateCanvasSize(rect = this.getRectFrom()) { |
|
const {canvas} = this; |
|
const dpr = canvas.dpr = window.devicePixelRatio; |
|
canvas.width = rect.width * dpr; |
|
canvas.height = rect.height * dpr; |
|
canvas.style.width = rect.width + 'px'; |
|
canvas.style.height = rect.height + 'px'; |
|
} |
|
|
|
private renderDetachAnimationFrame() { |
|
const { |
|
canvas, |
|
ctx, |
|
detachTime, |
|
length, |
|
availableLength |
|
} = this; |
|
|
|
if(!detachTime) { |
|
return; |
|
} else if(!liteMode.isAvailable('animations')) { |
|
this.remove(); |
|
return; |
|
} |
|
|
|
const {width} = canvas; |
|
|
|
ctx.globalCompositeOperation = 'destination-out'; |
|
|
|
// ctx.fillStyle = 'rgba(0, 0, 0, 0)'; |
|
// ctx.fillRect(0, 0, width, height); |
|
|
|
// const DURATION = 500; |
|
// const DELAY = DURATION; |
|
const DURATION = 150; |
|
const DELAY = 15; |
|
const elapsedTime = Date.now() - detachTime; |
|
let completed = true; |
|
for(let i = 0; i < length; ++i) { |
|
const delay = availableLength < length && i >= availableLength ? DELAY * (availableLength - 1) : DELAY * i; |
|
const elapsedRowTime = elapsedTime - delay; |
|
if(elapsedRowTime <= 0) { |
|
completed = false; |
|
continue; |
|
} |
|
|
|
const progress = easeInOutSine(elapsedRowTime, 0, 1, DURATION); |
|
|
|
ctx.beginPath(); |
|
ctx.rect(0, this.dialogHeight * i, width, this.dialogHeight); |
|
ctx.fillStyle = `rgba(0, 0, 0, ${progress})`; |
|
ctx.fill(); |
|
|
|
if(progress < 1) { |
|
completed = false; |
|
} |
|
} |
|
|
|
// const totalRadius = Math.sqrt(width ** 2 + height ** 2); |
|
// const gradient = ctx.createRadialGradient( |
|
// 0, 0, 0, |
|
// 0, 0, totalRadius); |
|
// gradient.addColorStop(0, 'rgba(0, 0, 0, 1)'); |
|
// gradient.addColorStop(progress, 'rgba(0, 0, 0, 0)'); |
|
// gradient.addColorStop(1, 'rgba(0, 0, 0, 0)'); |
|
|
|
// const gradient = ctx.createLinearGradient(0, 0, 0, height); |
|
// gradient.addColorStop(0, 'rgba(0, 0, 0, 1)'); |
|
// gradient.addColorStop(progress, 'rgba(0, 0, 0, 0)'); |
|
// gradient.addColorStop(1, 'rgba(0, 0, 0, 0)'); |
|
|
|
// ctx.fillStyle = gradient; |
|
// ctx.fillRect(0, 0, width, height); |
|
|
|
ctx.globalCompositeOperation = 'source-over'; |
|
|
|
if(completed) { |
|
this.remove(); |
|
} |
|
} |
|
|
|
private renderFrame() { |
|
this.shimmer.on(); |
|
this.renderDetachAnimationFrame(); |
|
} |
|
|
|
private startAnimation() { |
|
const {canvas, shimmer} = this; |
|
const tempId = ++this.tempId; |
|
const pattern = this.createPattern(); |
|
|
|
shimmer.settings({ |
|
canvas, |
|
fillStyle: pattern |
|
}); |
|
|
|
const middleware = () => { |
|
return this.tempId === tempId; |
|
}; |
|
|
|
this.renderFrame(); |
|
animate(() => { |
|
if(!middleware()) { |
|
return false; |
|
} |
|
|
|
// ! should've removed the loop if animations are disabled |
|
if(liteMode.isAvailable('animations')) { |
|
this.renderFrame(); |
|
} |
|
|
|
// ! tempId can be changed during renderFrame |
|
return middleware(); |
|
}); |
|
|
|
rootScope.addEventListener('theme_change', this.onThemeChange); |
|
mediaSizes.addEventListener('resize', this.onResize); |
|
} |
|
|
|
private stopAnimation() { |
|
++this.tempId; |
|
rootScope.removeEventListener('theme_change', this.onThemeChange); |
|
mediaSizes.removeEventListener('resize', this.onResize); |
|
} |
|
|
|
private onThemeChange = () => { |
|
this.stopAnimation(); |
|
this.startAnimation(); |
|
}; |
|
|
|
private onResize = () => { |
|
const {canvas} = this; |
|
const {width, height, dpr} = canvas; |
|
this.updateCanvasSize(); |
|
if(canvas.width === width && canvas.height === height && canvas.dpr === dpr) { |
|
return; |
|
} |
|
|
|
this.stopAnimation(); |
|
this.startAnimation(); |
|
}; |
|
|
|
private createPattern() { |
|
const {canvas, ctx} = this; |
|
|
|
const patternCanvas = document.createElement('canvas'); |
|
const patternContext = patternCanvas.getContext('2d'); |
|
const dpr = canvas.dpr; |
|
patternCanvas.dpr = dpr; |
|
patternCanvas.width = canvas.width; |
|
patternCanvas.height = canvas.height; |
|
|
|
patternContext.fillStyle = customProperties.getProperty('surface-color'); |
|
patternContext.fillRect(0, 0, patternCanvas.width, patternCanvas.height); |
|
|
|
patternContext.fillStyle = '#000'; |
|
patternContext.globalCompositeOperation = 'destination-out'; |
|
|
|
const dialogHeight = this.dialogHeight = this.totalHeight * dpr; |
|
const length = this.length = Math.ceil(canvas.height / dialogHeight); |
|
for(let i = 0; i < length; ++i) { |
|
this.drawChat(patternContext, i, i * dialogHeight); |
|
} |
|
|
|
return ctx.createPattern(patternCanvas, 'no-repeat'); |
|
} |
|
|
|
private drawChat(ctx: CanvasRenderingContext2D, i: number, y: number) { |
|
let generatedValues = this.generatedValues[i]; |
|
if(!generatedValues) { |
|
generatedValues = this.generatedValues[i] = { |
|
firstLineWidth: 40 + Math.random() * 100, // 120 |
|
secondLineWidth: 120 + Math.random() * 130, // 240 |
|
statusWidth: 24 + Math.random() * 16 |
|
}; |
|
} |
|
|
|
const { |
|
firstLineWidth, |
|
secondLineWidth, |
|
statusWidth |
|
} = generatedValues; |
|
|
|
const {canvas} = ctx; |
|
const {dpr} = canvas; |
|
y /= dpr; |
|
|
|
const { |
|
avatarSize, |
|
marginVertical, |
|
lineHeight, |
|
lineBorderRadius, |
|
lineMarginVertical |
|
} = this; |
|
|
|
let marginLeft = 17; |
|
|
|
if(avatarSize) { |
|
drawCircleFromStart(ctx, marginLeft, y + marginVertical, avatarSize / 2, true); |
|
marginLeft += avatarSize + 10; |
|
} |
|
|
|
// 9 + 54 - 10 - 8 = 45 ........ 72 - 9 - 10 - 8 |
|
roundRect(ctx, marginLeft, y + marginVertical + lineMarginVertical, firstLineWidth, lineHeight, lineBorderRadius, true); |
|
// roundRect(ctx, marginLeft, y + marginVertical + avatarSize - lineHeight - lineMarginVertical, secondLineWidth, lineHeight, lineBorderRadius, true); |
|
roundRect(ctx, marginLeft, y + this.totalHeight - marginVertical - lineHeight - lineMarginVertical, secondLineWidth, lineHeight, lineBorderRadius, true); |
|
|
|
roundRect(ctx, canvas.width / dpr - 24 - statusWidth, y + marginVertical + lineMarginVertical, statusWidth, lineHeight, lineBorderRadius, true); |
|
} |
|
}
|
|
|