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.
316 lines
8.7 KiB
316 lines
8.7 KiB
2 years ago
|
/*
|
||
|
* 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 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 lineHeight: number;
|
||
|
private lineBorderRadius: number;
|
||
|
private lineMarginVertical: number;
|
||
|
private statusWidth: number;
|
||
|
private generatedValues: {
|
||
|
firstLineWidth: number,
|
||
|
secondLineWidth: number,
|
||
|
statusWidth: number
|
||
|
}[];
|
||
|
|
||
|
private getRectFrom: Element;
|
||
|
private onRemove: () => void;
|
||
|
private blockScrollable: Scrollable;
|
||
|
|
||
|
constructor() {
|
||
|
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 = 54;
|
||
|
this.marginVertical = 9;
|
||
|
this.lineHeight = 10;
|
||
|
this.lineBorderRadius = 6;
|
||
|
this.lineMarginVertical = 8;
|
||
|
this.statusWidth = 24;
|
||
|
}
|
||
|
|
||
|
public attach({container, rect, getRectFrom, onRemove, blockScrollable}: {
|
||
|
container: HTMLElement,
|
||
|
rect?: {width: number, height: number},
|
||
|
getRectFrom?: HTMLElement,
|
||
|
onRemove?: DialogsPlaceholder['onRemove'],
|
||
|
blockScrollable?: DialogsPlaceholder['blockScrollable']
|
||
|
}) {
|
||
|
const {canvas} = this;
|
||
|
|
||
|
this.onRemove = onRemove;
|
||
|
this.getRectFrom = 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(!rootScope.settings.animationsEnabled) {
|
||
|
this.remove();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public remove() {
|
||
|
this.stopAnimation();
|
||
|
|
||
|
if(this.canvas.parentElement) {
|
||
|
this.canvas.remove();
|
||
|
|
||
|
if(this.onRemove) {
|
||
|
this.onRemove();
|
||
|
this.onRemove = undefined;
|
||
|
}
|
||
|
|
||
|
if(this.blockScrollable) {
|
||
|
this.blockScrollable.container.style.overflowY = '';
|
||
|
this.blockScrollable = undefined;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private updateCanvasSize(rect: {width: number, height: number} = this.getRectFrom.getBoundingClientRect()) {
|
||
|
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(!rootScope.settings.animationsEnabled) {
|
||
|
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(rootScope.settings.animationsEnabled) {
|
||
|
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.avatarSize + this.marginVertical * 2) * 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;
|
||
|
drawCircleFromStart(ctx, marginLeft, y + marginVertical, avatarSize / 2, true);
|
||
|
|
||
|
marginLeft += avatarSize + 10;
|
||
|
roundRect(ctx, marginLeft, y + marginVertical + lineMarginVertical, firstLineWidth, lineHeight, lineBorderRadius, true);
|
||
|
roundRect(ctx, marginLeft, y + marginVertical + avatarSize - lineHeight - lineMarginVertical, secondLineWidth, lineHeight, lineBorderRadius, true);
|
||
|
|
||
|
roundRect(ctx, canvas.width / dpr - 24 - statusWidth, y + marginVertical + lineMarginVertical, statusWidth, lineHeight, lineBorderRadius, true);
|
||
|
}
|
||
|
}
|