Merge branch 'backgrounds'
This commit is contained in:
commit
ccfd64b229
@ -689,6 +689,13 @@ export default class ChatBubbles {
|
|||||||
} else {
|
} else {
|
||||||
this.renderNewMessagesByIds([mid], true);
|
this.renderNewMessagesByIds([mid], true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(rootScope.settings.animationsEnabled) {
|
||||||
|
const gradientRenderer = this.chat.gradientRenderer;
|
||||||
|
if(gradientRenderer) {
|
||||||
|
gradientRenderer.toNextPosition();
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.listenerSetter.add(rootScope)('history_multiappend', (msgIdsByPeer) => {
|
this.listenerSetter.add(rootScope)('history_multiappend', (msgIdsByPeer) => {
|
||||||
|
@ -24,7 +24,6 @@ import type { ServerTimeManager } from "../../lib/mtproto/serverTimeManager";
|
|||||||
import type { AppMessagesIdsManager } from "../../lib/appManagers/appMessagesIdsManager";
|
import type { AppMessagesIdsManager } from "../../lib/appManagers/appMessagesIdsManager";
|
||||||
import type { AppGroupCallsManager } from "../../lib/appManagers/appGroupCallsManager";
|
import type { AppGroupCallsManager } from "../../lib/appManagers/appGroupCallsManager";
|
||||||
import type { AppReactionsManager } from "../../lib/appManagers/appReactionsManager";
|
import type { AppReactionsManager } from "../../lib/appManagers/appReactionsManager";
|
||||||
import type { State } from "../../lib/appManagers/appStateManager";
|
|
||||||
import type stateStorage from '../../lib/stateStorage';
|
import type stateStorage from '../../lib/stateStorage';
|
||||||
import EventListenerBase from "../../helpers/eventListenerBase";
|
import EventListenerBase from "../../helpers/eventListenerBase";
|
||||||
import { logger, LogTypes } from "../../lib/logger";
|
import { logger, LogTypes } from "../../lib/logger";
|
||||||
@ -37,13 +36,15 @@ import ChatSelection from "./selection";
|
|||||||
import ChatTopbar from "./topbar";
|
import ChatTopbar from "./topbar";
|
||||||
import { BOT_START_PARAM, NULL_PEER_ID, REPLIES_PEER_ID } from "../../lib/mtproto/mtproto_config";
|
import { BOT_START_PARAM, NULL_PEER_ID, REPLIES_PEER_ID } from "../../lib/mtproto/mtproto_config";
|
||||||
import SetTransition from "../singleTransition";
|
import SetTransition from "../singleTransition";
|
||||||
import { fastRaf } from "../../helpers/schedulers";
|
|
||||||
import AppPrivateSearchTab from "../sidebarRight/tabs/search";
|
import AppPrivateSearchTab from "../sidebarRight/tabs/search";
|
||||||
import renderImageFromUrl from "../../helpers/dom/renderImageFromUrl";
|
import renderImageFromUrl, { renderImageFromUrlPromise } from "../../helpers/dom/renderImageFromUrl";
|
||||||
import mediaSizes from "../../helpers/mediaSizes";
|
import mediaSizes from "../../helpers/mediaSizes";
|
||||||
import ChatSearch from "./search";
|
import ChatSearch from "./search";
|
||||||
import { IS_TOUCH_SUPPORTED } from "../../environment/touchSupport";
|
import { IS_TOUCH_SUPPORTED } from "../../environment/touchSupport";
|
||||||
import getAutoDownloadSettingsByPeerId, { ChatAutoDownloadSettings } from "../../helpers/autoDownload";
|
import getAutoDownloadSettingsByPeerId, { ChatAutoDownloadSettings } from "../../helpers/autoDownload";
|
||||||
|
import ChatBackgroundGradientRenderer from "./gradientRenderer";
|
||||||
|
import ChatBackgroundPatternRenderer from "./patternRenderer";
|
||||||
|
import { pause } from "../../helpers/schedulers/pause";
|
||||||
|
|
||||||
export type ChatType = 'chat' | 'pinned' | 'replies' | 'discussion' | 'scheduled';
|
export type ChatType = 'chat' | 'pinned' | 'replies' | 'discussion' | 'scheduled';
|
||||||
|
|
||||||
@ -77,6 +78,13 @@ export default class Chat extends EventListenerBase<{
|
|||||||
|
|
||||||
public isRestricted: boolean;
|
public isRestricted: boolean;
|
||||||
public autoDownload: ChatAutoDownloadSettings;
|
public autoDownload: ChatAutoDownloadSettings;
|
||||||
|
|
||||||
|
public gradientRenderer: ChatBackgroundGradientRenderer;
|
||||||
|
public patternRenderer: ChatBackgroundPatternRenderer;
|
||||||
|
public gradientCanvas: HTMLCanvasElement;
|
||||||
|
public patternCanvas: HTMLCanvasElement;
|
||||||
|
public backgroundTempId: number;
|
||||||
|
public setBackgroundPromise: Promise<void>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public appImManager: AppImManager,
|
public appImManager: AppImManager,
|
||||||
@ -120,32 +128,107 @@ export default class Chat extends EventListenerBase<{
|
|||||||
|
|
||||||
this.container.append(this.backgroundEl);
|
this.container.append(this.backgroundEl);
|
||||||
this.appImManager.chatsContainer.append(this.container);
|
this.appImManager.chatsContainer.append(this.container);
|
||||||
|
|
||||||
|
this.backgroundTempId = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setBackground(url: string): Promise<void> {
|
public setBackground(url: string, skipAnimation?: boolean): Promise<void> {
|
||||||
const theme = rootScope.getTheme();
|
const theme = rootScope.getTheme();
|
||||||
|
|
||||||
let item: HTMLElement;
|
let item: HTMLElement;
|
||||||
if(theme.background.type === 'color' && document.documentElement.style.cursor === 'grabbing') {
|
const isColorBackground = !!theme.background.color && !theme.background.slug && !theme.background.intensity;
|
||||||
const _item = this.backgroundEl.lastElementChild as HTMLElement;
|
if(
|
||||||
if(_item && _item.dataset.type === theme.background.type) {
|
isColorBackground &&
|
||||||
item = _item;
|
document.documentElement.style.cursor === 'grabbing' &&
|
||||||
}
|
this.gradientRenderer &&
|
||||||
|
!this.patternRenderer
|
||||||
|
) {
|
||||||
|
this.gradientCanvas.dataset.colors = theme.background.color;
|
||||||
|
this.gradientRenderer.init(this.gradientCanvas);
|
||||||
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tempId = ++this.backgroundTempId;
|
||||||
|
|
||||||
|
const previousGradientRenderer = this.gradientRenderer;
|
||||||
|
const previousPatternRenderer = this.patternRenderer;
|
||||||
|
const previousGradientCanvas = this.gradientCanvas;
|
||||||
|
const previousPatternCanvas = this.patternCanvas;
|
||||||
|
|
||||||
|
this.gradientRenderer =
|
||||||
|
this.patternRenderer =
|
||||||
|
this.gradientCanvas =
|
||||||
|
this.patternCanvas =
|
||||||
|
undefined;
|
||||||
|
|
||||||
|
const intensity = theme.background.intensity && theme.background.intensity / 100;
|
||||||
|
const isDarkPattern = !!intensity && intensity < 0;
|
||||||
|
|
||||||
|
let patternRenderer: ChatBackgroundPatternRenderer;
|
||||||
|
let patternCanvas = item?.firstElementChild as HTMLCanvasElement;
|
||||||
|
let gradientCanvas: HTMLCanvasElement;
|
||||||
if(!item) {
|
if(!item) {
|
||||||
item = document.createElement('div');
|
item = document.createElement('div');
|
||||||
item.classList.add('chat-background-item');
|
item.classList.add('chat-background-item');
|
||||||
item.dataset.type = theme.background.type;
|
|
||||||
|
if(url) {
|
||||||
|
if(intensity) {
|
||||||
|
item.classList.add('is-pattern');
|
||||||
|
|
||||||
|
const rect = this.appImManager.chatsContainer.getBoundingClientRect();
|
||||||
|
patternRenderer = this.patternRenderer = ChatBackgroundPatternRenderer.getInstance({
|
||||||
|
url,
|
||||||
|
width: rect.width,
|
||||||
|
height: rect.height
|
||||||
|
});
|
||||||
|
|
||||||
|
patternCanvas = this.patternCanvas = patternRenderer.createCanvas();
|
||||||
|
patternCanvas.classList.add('chat-background-item-canvas', 'chat-background-item-pattern-canvas');
|
||||||
|
} else if(theme.background.slug) {
|
||||||
|
item.classList.add('is-image');
|
||||||
|
}
|
||||||
|
} else if(theme.background.color) {
|
||||||
|
item.classList.add('is-color');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(theme.background.type === 'color') {
|
let gradientRenderer: ChatBackgroundGradientRenderer;
|
||||||
item.style.backgroundColor = theme.background.color;
|
const color = theme.background.color;
|
||||||
item.style.backgroundImage = 'none';
|
if(color) {
|
||||||
|
// if(color.includes(',')) {
|
||||||
|
const {canvas, gradientRenderer: _gradientRenderer} = ChatBackgroundGradientRenderer.create(color);
|
||||||
|
gradientRenderer = this.gradientRenderer = _gradientRenderer;
|
||||||
|
gradientCanvas = this.gradientCanvas = canvas;
|
||||||
|
gradientCanvas.classList.add('chat-background-item-canvas', 'chat-background-item-color-canvas');
|
||||||
|
|
||||||
|
if(rootScope.settings.animationsEnabled) {
|
||||||
|
gradientRenderer.scrollAnimate(true);
|
||||||
|
}
|
||||||
|
// } else {
|
||||||
|
// item.style.backgroundColor = color;
|
||||||
|
// item.style.backgroundImage = 'none';
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise<void>((resolve) => {
|
if(patternRenderer) {
|
||||||
|
const setOpacityTo = isDarkPattern ? gradientCanvas : patternCanvas;
|
||||||
|
setOpacityTo.style.setProperty('--opacity-max', '' + Math.abs(intensity));
|
||||||
|
}
|
||||||
|
|
||||||
|
const promise = new Promise<void>((resolve) => {
|
||||||
const cb = () => {
|
const cb = () => {
|
||||||
|
if(this.backgroundTempId !== tempId) {
|
||||||
|
if(patternRenderer) {
|
||||||
|
patternRenderer.cleanup(patternCanvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(gradientRenderer) {
|
||||||
|
gradientRenderer.cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const prev = this.backgroundEl.lastElementChild as HTMLElement;
|
const prev = this.backgroundEl.lastElementChild as HTMLElement;
|
||||||
|
|
||||||
if(prev === item) {
|
if(prev === item) {
|
||||||
@ -153,27 +236,57 @@ export default class Chat extends EventListenerBase<{
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const append = [gradientCanvas, isDarkPattern ? undefined : patternCanvas].filter(Boolean);
|
||||||
|
if(append.length) {
|
||||||
|
item.append(...append);
|
||||||
|
}
|
||||||
|
|
||||||
this.backgroundEl.append(item);
|
this.backgroundEl.append(item);
|
||||||
|
|
||||||
// * одного недостаточно, при обновлении страницы все равно фон появляется неплавно
|
SetTransition(item, 'is-visible', true, !skipAnimation ? 200 : 0, prev ? () => {
|
||||||
// ! с requestAnimationFrame лучше, но все равно иногда моргает, так что использую два фаста.
|
if(previousPatternRenderer) {
|
||||||
fastRaf(() => {
|
previousPatternRenderer.cleanup(previousPatternCanvas);
|
||||||
fastRaf(() => {
|
}
|
||||||
SetTransition(item, 'is-visible', true, 200, prev ? () => {
|
|
||||||
prev.remove();
|
if(previousGradientRenderer) {
|
||||||
} : null);
|
previousGradientRenderer.cleanup();
|
||||||
});
|
}
|
||||||
});
|
|
||||||
|
prev.remove();
|
||||||
|
} : null, 2);
|
||||||
|
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
if(url) {
|
if(patternRenderer) {
|
||||||
|
const renderPatternPromise = patternRenderer.renderToCanvas(patternCanvas);
|
||||||
|
renderPatternPromise.then(() => {
|
||||||
|
let promise: Promise<any>;
|
||||||
|
if(isDarkPattern) {
|
||||||
|
promise = patternRenderer.exportCanvasPatternToImage(patternCanvas).then(url => {
|
||||||
|
if(this.backgroundTempId !== tempId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gradientCanvas.style.webkitMaskImage = `url(${url})`;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
promise = Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
promise.then(cb);
|
||||||
|
});
|
||||||
|
} else if(url) {
|
||||||
renderImageFromUrl(item, url, cb);
|
renderImageFromUrl(item, url, cb);
|
||||||
} else {
|
} else {
|
||||||
cb();
|
cb();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return this.setBackgroundPromise = Promise.race([
|
||||||
|
pause(500),
|
||||||
|
promise
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public setType(type: ChatType) {
|
public setType(type: ChatType) {
|
||||||
@ -245,6 +358,19 @@ export default class Chat extends EventListenerBase<{
|
|||||||
this.bubbles.cleanup();
|
this.bubbles.cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private cleanupBackground() {
|
||||||
|
++this.backgroundTempId;
|
||||||
|
if(this.patternRenderer) {
|
||||||
|
this.patternRenderer.cleanup(this.patternCanvas);
|
||||||
|
this.patternRenderer = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.gradientRenderer) {
|
||||||
|
this.gradientRenderer.cleanup();
|
||||||
|
this.gradientRenderer = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public destroy() {
|
public destroy() {
|
||||||
//const perf = performance.now();
|
//const perf = performance.now();
|
||||||
|
|
||||||
@ -253,6 +379,8 @@ export default class Chat extends EventListenerBase<{
|
|||||||
this.input.destroy();
|
this.input.destroy();
|
||||||
this.contextMenu && this.contextMenu.destroy();
|
this.contextMenu && this.contextMenu.destroy();
|
||||||
|
|
||||||
|
this.cleanupBackground();
|
||||||
|
|
||||||
delete this.topbar;
|
delete this.topbar;
|
||||||
delete this.bubbles;
|
delete this.bubbles;
|
||||||
delete this.input;
|
delete this.input;
|
||||||
|
393
src/components/chat/gradientRenderer.ts
Normal file
393
src/components/chat/gradientRenderer.ts
Normal file
@ -0,0 +1,393 @@
|
|||||||
|
import { animate } from "../../helpers/animation";
|
||||||
|
import { hexToRgb } from "../../helpers/color";
|
||||||
|
|
||||||
|
const WIDTH = 50;
|
||||||
|
const HEIGHT = WIDTH;
|
||||||
|
|
||||||
|
export default class ChatBackgroundGradientRenderer {
|
||||||
|
private readonly _width = WIDTH;
|
||||||
|
private readonly _height = HEIGHT;
|
||||||
|
private _phase: number;
|
||||||
|
private _tail: number;
|
||||||
|
private readonly _tails = 90;
|
||||||
|
private readonly _scrollTails = 50;
|
||||||
|
private _frames: ImageData[];
|
||||||
|
private _colors: {r: number, g: number, b: number}[];
|
||||||
|
/* private readonly _curve = [
|
||||||
|
0, 25, 50, 75, 100, 150, 200, 250, 300, 350, 400, 500, 600, 700, 800, 900,
|
||||||
|
1000, 1100, 1200, 1300, 1400, 1500, 1600, 1700, 1800, 1830, 1860, 1890, 1920,
|
||||||
|
1950, 1980, 2010, 2040, 2070, 2100, 2130, 2160, 2190, 2220, 2250, 2280, 2310,
|
||||||
|
2340, 2370, 2400, 2430, 2460, 2490, 2520, 2550, 2580, 2610, 2630, 2640, 2650,
|
||||||
|
2660, 2670, 2680, 2690, 2700
|
||||||
|
]; */
|
||||||
|
private readonly _curve = [
|
||||||
|
0 , 0.25 , 0.50 , 0.75 , 1 , 1.5 , 2 , 2.5 , 3 , 3.5 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 ,
|
||||||
|
13 , 14 , 15 , 16 , 17 , 18 , 18.3 , 18.6 , 18.9 , 19.2 , 19.5 , 19.8 , 20.1 , 20.4 , 20.7 ,
|
||||||
|
21.0 , 21.3 , 21.6 , 21.9 , 22.2 , 22.5 , 22.8 , 23.1 , 23.4 , 23.7 , 24.0 , 24.3 , 24.6 ,
|
||||||
|
24.9 , 25.2 , 25.5 , 25.8 , 26.1 , 26.3 , 26.4 , 26.5 , 26.6 , 26.7 , 26.8 , 26.9 , 27 ,
|
||||||
|
];
|
||||||
|
private readonly _incrementalCurve: number[];
|
||||||
|
private readonly _positions = [
|
||||||
|
{ x: 0.80, y: 0.10 },
|
||||||
|
{ x: 0.60, y: 0.20 },
|
||||||
|
{ x: 0.35, y: 0.25 },
|
||||||
|
{ x: 0.25, y: 0.60 },
|
||||||
|
{ x: 0.20, y: 0.90 },
|
||||||
|
{ x: 0.40, y: 0.80 },
|
||||||
|
{ x: 0.65, y: 0.75 },
|
||||||
|
{ x: 0.75, y: 0.40 }
|
||||||
|
];
|
||||||
|
private readonly _phases = this._positions.length;
|
||||||
|
private _onWheelRAF: number;
|
||||||
|
private _scrollDelta: number;
|
||||||
|
|
||||||
|
// private _ts = 0;
|
||||||
|
// private _fps = 15;
|
||||||
|
// private _frametime = 1000 / this._fps;
|
||||||
|
// private _raf: number;
|
||||||
|
|
||||||
|
private _canvas: HTMLCanvasElement;
|
||||||
|
private _ctx: CanvasRenderingContext2D;
|
||||||
|
private _hc: HTMLCanvasElement;
|
||||||
|
private _hctx: CanvasRenderingContext2D;
|
||||||
|
|
||||||
|
private _addedScrollListener: boolean;
|
||||||
|
private _animatingToNextPosition: boolean;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
const diff = this._tails / this._curve[this._curve.length - 1];
|
||||||
|
|
||||||
|
for(let i = 0, length = this._curve.length; i < length; ++i) {
|
||||||
|
this._curve[i] = this._curve[i] * diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._incrementalCurve = this._curve.map((v, i, arr) => {
|
||||||
|
return v - (arr[i - 1] ?? 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private hexToRgb(hex: string) {
|
||||||
|
const result = hexToRgb(hex);
|
||||||
|
return {r: result[0], g: result[1], b: result[2]};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPositions(shift: number) {
|
||||||
|
const positions = this._positions.slice();
|
||||||
|
while(shift > 0) {
|
||||||
|
positions.push(positions.shift());
|
||||||
|
--shift;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: typeof positions = [];
|
||||||
|
for(let i = 0; i < positions.length; i += 2) {
|
||||||
|
result.push(positions[i]);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getNextPositions(phase: number, curveMax: number, curve: number[]) {
|
||||||
|
const pos = this.getPositions(phase);
|
||||||
|
if(!curve[0] && curve.length === 1) {
|
||||||
|
return [pos];
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextPos = this.getPositions(++phase % this._phases);
|
||||||
|
const distances = nextPos.map((nextPos, idx) => {
|
||||||
|
return {
|
||||||
|
x: (nextPos.x - pos[idx].x) / curveMax,
|
||||||
|
y: (nextPos.y - pos[idx].y) / curveMax,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const positions = curve.map((value) => {
|
||||||
|
return distances.map((distance, idx) => {
|
||||||
|
return {
|
||||||
|
x: pos[idx].x + distance.x * value,
|
||||||
|
y: pos[idx].y + distance.y * value
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return positions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private curPosition(phase: number, tail: number) {
|
||||||
|
const positions = this.getNextPositions(phase, this._tails, [tail]);
|
||||||
|
return positions[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private changeTail(diff: number) {
|
||||||
|
this._tail += diff;
|
||||||
|
|
||||||
|
while(this._tail >= this._tails) {
|
||||||
|
this._tail -= this._tails;
|
||||||
|
if(++this._phase >= this._phases) {
|
||||||
|
this._phase -= this._phases;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while(this._tail < 0) {
|
||||||
|
this._tail += this._tails;
|
||||||
|
if(--this._phase < 0) {
|
||||||
|
this._phase += this._phases;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onWheel = (e: {deltaY: number}) => {
|
||||||
|
if(this._animatingToNextPosition) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._scrollDelta += e.deltaY;
|
||||||
|
if(this._onWheelRAF === undefined) {
|
||||||
|
this._onWheelRAF = requestAnimationFrame(this.drawOnWheel);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private drawOnWheel = () => {
|
||||||
|
let diff = this._scrollDelta / this._scrollTails;
|
||||||
|
this._scrollDelta %= this._scrollTails;
|
||||||
|
diff = diff > 0 ? Math.floor(diff) : Math.ceil(diff);
|
||||||
|
if(diff) {
|
||||||
|
this.changeTail(diff);
|
||||||
|
const curPos = this.curPosition(this._phase, this._tail);
|
||||||
|
this.drawGradient(curPos);
|
||||||
|
}
|
||||||
|
this._onWheelRAF = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
private drawNextPositionAnimated = () => {
|
||||||
|
const frames = this._frames;
|
||||||
|
const id = frames.shift();
|
||||||
|
if(id) {
|
||||||
|
this.drawImageData(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const leftLength = frames.length;
|
||||||
|
if(!leftLength) {
|
||||||
|
this._animatingToNextPosition = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !!leftLength;
|
||||||
|
};
|
||||||
|
|
||||||
|
private getGradientImageData(positions: {x: number, y: number}[]) {
|
||||||
|
const id = this._hctx.createImageData(this._width, this._height);
|
||||||
|
const pixels = id.data;
|
||||||
|
|
||||||
|
let offset = 0;
|
||||||
|
for(let y = 0; y < this._height; ++y) {
|
||||||
|
const directPixelY = y / this._height;
|
||||||
|
const centerDistanceY = directPixelY - 0.5;
|
||||||
|
const centerDistanceY2 = centerDistanceY * centerDistanceY;
|
||||||
|
|
||||||
|
for(let x = 0; x < this._width; ++x) {
|
||||||
|
const directPixelX = x / this._width;
|
||||||
|
|
||||||
|
const centerDistanceX = directPixelX - 0.5;
|
||||||
|
const centerDistance = Math.sqrt(centerDistanceX * centerDistanceX + centerDistanceY2);
|
||||||
|
|
||||||
|
const swirlFactor = 0.35 * centerDistance;
|
||||||
|
const theta = swirlFactor * swirlFactor * 0.8 * 8.0;
|
||||||
|
const sinTheta = Math.sin(theta);
|
||||||
|
const cosTheta = Math.cos(theta);
|
||||||
|
|
||||||
|
const pixelX = Math.max(0.0, Math.min(1.0, 0.5 + centerDistanceX * cosTheta - centerDistanceY * sinTheta));
|
||||||
|
const pixelY = Math.max(0.0, Math.min(1.0, 0.5 + centerDistanceX * sinTheta + centerDistanceY * cosTheta));
|
||||||
|
|
||||||
|
let distanceSum = 0.0;
|
||||||
|
|
||||||
|
let r = 0.0;
|
||||||
|
let g = 0.0;
|
||||||
|
let b = 0.0;
|
||||||
|
|
||||||
|
for(let i = 0; i < this._colors.length; i++) {
|
||||||
|
const colorX = positions[i].x;
|
||||||
|
const colorY = positions[i].y;
|
||||||
|
|
||||||
|
const distanceX = pixelX - colorX;
|
||||||
|
const distanceY = pixelY - colorY;
|
||||||
|
|
||||||
|
let distance = Math.max(0.0, 0.9 - Math.sqrt(distanceX * distanceX + distanceY * distanceY));
|
||||||
|
distance = distance * distance * distance * distance;
|
||||||
|
distanceSum += distance;
|
||||||
|
|
||||||
|
r += distance * this._colors[i].r / 255;
|
||||||
|
g += distance * this._colors[i].g / 255;
|
||||||
|
b += distance * this._colors[i].b / 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
pixels[offset++] = r / distanceSum * 255.0;
|
||||||
|
pixels[offset++] = g / distanceSum * 255.0;
|
||||||
|
pixels[offset++] = b / distanceSum * 255.0;
|
||||||
|
pixels[offset++] = 0xFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawImageData(id: ImageData) {
|
||||||
|
this._hctx.putImageData(id, 0, 0);
|
||||||
|
this._ctx.drawImage(this._hc, 0, 0, this._width, this._height);
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawGradient(positions: {x: number, y: number}[]) {
|
||||||
|
this.drawImageData(this.getGradientImageData(positions));
|
||||||
|
}
|
||||||
|
|
||||||
|
// private doAnimate = () => {
|
||||||
|
// const now = +Date.now();
|
||||||
|
// if(!document.hasFocus() || (now - this._ts) < this._frametime) {
|
||||||
|
// this._raf = requestAnimationFrame(this.doAnimate);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// this._ts = now;
|
||||||
|
// this.changeTail(1);
|
||||||
|
// const cur_pos = this.curPosition(this._phase, this._tail);
|
||||||
|
// this.drawGradient(cur_pos);
|
||||||
|
// this._raf = requestAnimationFrame(this.doAnimate);
|
||||||
|
// };
|
||||||
|
|
||||||
|
// public animate(start?: boolean) {
|
||||||
|
// if(!start) {
|
||||||
|
// cancelAnimationFrame(this._raf);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// this.doAnimate();
|
||||||
|
// }
|
||||||
|
|
||||||
|
public init(el: HTMLCanvasElement) {
|
||||||
|
this._frames = [];
|
||||||
|
this._phase = 0;
|
||||||
|
this._tail = 0;
|
||||||
|
this._scrollDelta = 0;
|
||||||
|
if(this._onWheelRAF !== undefined) {
|
||||||
|
cancelAnimationFrame(this._onWheelRAF);
|
||||||
|
this._onWheelRAF = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const colors = el.getAttribute('data-colors').split(',').reverse();
|
||||||
|
this._colors = colors.map(color => {
|
||||||
|
return this.hexToRgb(color);
|
||||||
|
});
|
||||||
|
|
||||||
|
if(!this._hc) {
|
||||||
|
this._hc = document.createElement('canvas');
|
||||||
|
this._hc.width = this._width;
|
||||||
|
this._hc.height = this._height;
|
||||||
|
this._hctx = this._hc.getContext('2d');
|
||||||
|
}
|
||||||
|
|
||||||
|
this._canvas = el;
|
||||||
|
this._ctx = this._canvas.getContext('2d');
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
public update() {
|
||||||
|
if(this._colors.length < 2) {
|
||||||
|
const color = this._colors[0];
|
||||||
|
this._ctx.fillStyle = `rgb(${color.r}, ${color.g}, ${color.b})`;
|
||||||
|
this._ctx.fillRect(0, 0, this._width, this._height);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pos = this.curPosition(this._phase, this._tail);
|
||||||
|
this.drawGradient(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
public toNextPosition() {
|
||||||
|
if(this._colors.length < 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tail = this._tail;
|
||||||
|
const tails = this._tails;
|
||||||
|
|
||||||
|
let nextPhaseOnIdx: number;
|
||||||
|
|
||||||
|
const curve: number[] = [];
|
||||||
|
for(let i = 0, length = this._incrementalCurve.length; i < length; ++i) {
|
||||||
|
const inc = this._incrementalCurve[i];
|
||||||
|
let value = (curve[i - 1] ?? tail) + inc;
|
||||||
|
|
||||||
|
if(+value.toFixed(2) > tails && nextPhaseOnIdx === undefined) {
|
||||||
|
nextPhaseOnIdx = i;
|
||||||
|
value %= tails;
|
||||||
|
}
|
||||||
|
|
||||||
|
curve.push(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentPhaseCurve = curve.slice(0, nextPhaseOnIdx);
|
||||||
|
const nextPhaseCurve = nextPhaseOnIdx !== undefined ? curve.slice(nextPhaseOnIdx) : [];
|
||||||
|
|
||||||
|
[currentPhaseCurve, nextPhaseCurve].forEach((curve, idx, curves) => {
|
||||||
|
const last = curve[curve.length - 1];
|
||||||
|
if(last !== undefined && last > tails) {
|
||||||
|
curve[curve.length - 1] = +last.toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._tail = last ?? 0;
|
||||||
|
|
||||||
|
if(!curve.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const positions = this.getNextPositions(this._phase, tails, curve);
|
||||||
|
if(idx !== (curves.length - 1)) {
|
||||||
|
if(++this._phase >= this._phases) {
|
||||||
|
this._phase -= this._phases;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ids = positions.map((pos) => {
|
||||||
|
return this.getGradientImageData(pos);
|
||||||
|
});
|
||||||
|
|
||||||
|
this._frames.push(...ids);
|
||||||
|
});
|
||||||
|
|
||||||
|
this._animatingToNextPosition = true;
|
||||||
|
animate(this.drawNextPositionAnimated);
|
||||||
|
}
|
||||||
|
|
||||||
|
public scrollAnimate(start?: boolean) {
|
||||||
|
if(this._colors.length < 2 && start) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(start && !this._addedScrollListener) {
|
||||||
|
document.addEventListener('wheel', this.onWheel);
|
||||||
|
this._addedScrollListener = true;
|
||||||
|
} else if(!start && this._addedScrollListener) {
|
||||||
|
document.removeEventListener('wheel', this.onWheel);
|
||||||
|
this._addedScrollListener = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public cleanup() {
|
||||||
|
this.scrollAnimate(false);
|
||||||
|
// this.animate(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static createCanvas(colors?: string) {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = WIDTH;
|
||||||
|
canvas.height = HEIGHT;
|
||||||
|
if(colors !== undefined) {
|
||||||
|
canvas.dataset.colors = colors;
|
||||||
|
}
|
||||||
|
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static create(colors?: string) {
|
||||||
|
const canvas = this.createCanvas(colors);
|
||||||
|
const gradientRenderer = new ChatBackgroundGradientRenderer();
|
||||||
|
gradientRenderer.init(canvas);
|
||||||
|
|
||||||
|
return {gradientRenderer, canvas};
|
||||||
|
}
|
||||||
|
}
|
121
src/components/chat/patternRenderer.ts
Normal file
121
src/components/chat/patternRenderer.ts
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
/*
|
||||||
|
* https://github.com/morethanwords/tweb
|
||||||
|
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
||||||
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { IS_SAFARI } from "../../environment/userAgent";
|
||||||
|
import { indexOfAndSplice } from "../../helpers/array";
|
||||||
|
import { renderImageFromUrlPromise } from "../../helpers/dom/renderImageFromUrl";
|
||||||
|
import { deepEqual } from "../../helpers/object";
|
||||||
|
|
||||||
|
type ChatBackgroundPatternRendererInitOptions = {
|
||||||
|
url: string,
|
||||||
|
width: number,
|
||||||
|
height: number
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class ChatBackgroundPatternRenderer {
|
||||||
|
private static INSTANCES: ChatBackgroundPatternRenderer[] = [];
|
||||||
|
|
||||||
|
private pattern: CanvasPattern;
|
||||||
|
private objectUrl: string;
|
||||||
|
private options: ChatBackgroundPatternRendererInitOptions;
|
||||||
|
private canvases: Set<HTMLCanvasElement>;
|
||||||
|
private createCanvasPatternPromise: Promise<void>;
|
||||||
|
private exportCanvasPatternToImagePromise: Promise<string>;
|
||||||
|
// private img: HTMLImageElement;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.canvases = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getInstance(options: ChatBackgroundPatternRendererInitOptions) {
|
||||||
|
let instance = this.INSTANCES.find((instance) => {
|
||||||
|
return deepEqual(instance.options, options);
|
||||||
|
});
|
||||||
|
|
||||||
|
if(!instance) {
|
||||||
|
instance = new ChatBackgroundPatternRenderer();
|
||||||
|
instance.init(options);
|
||||||
|
this.INSTANCES.push(instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(options: ChatBackgroundPatternRendererInitOptions) {
|
||||||
|
this.options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public renderToCanvas(canvas: HTMLCanvasElement) {
|
||||||
|
return this.createCanvasPattern(canvas).then(() => {
|
||||||
|
return this.fillCanvas(canvas);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private createCanvasPattern(canvas: HTMLCanvasElement) {
|
||||||
|
if(this.createCanvasPatternPromise) return this.createCanvasPatternPromise;
|
||||||
|
return this.createCanvasPatternPromise = new Promise((resolve) => {
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.crossOrigin = 'anonymous';
|
||||||
|
renderImageFromUrlPromise(img, this.options.url, false).then(() => {
|
||||||
|
let createPatternFrom: HTMLImageElement | HTMLCanvasElement;
|
||||||
|
if(IS_SAFARI) {
|
||||||
|
const canvas = createPatternFrom = document.createElement('canvas');
|
||||||
|
canvas.width = img.naturalWidth;
|
||||||
|
canvas.height = img.naturalHeight;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||||
|
} else {
|
||||||
|
createPatternFrom = img;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this.img = img;
|
||||||
|
this.pattern = canvas.getContext('2d').createPattern(createPatternFrom, 'repeat-x');
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public exportCanvasPatternToImage(canvas: HTMLCanvasElement) {
|
||||||
|
if(this.exportCanvasPatternToImagePromise) return this.exportCanvasPatternToImagePromise;
|
||||||
|
return this.exportCanvasPatternToImagePromise = new Promise<string>((resolve) => {
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
|
const newUrl = this.objectUrl = URL.createObjectURL(blob);
|
||||||
|
resolve(newUrl);
|
||||||
|
}, 'image/png');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public cleanup(canvas: HTMLCanvasElement) {
|
||||||
|
this.canvases.delete(canvas);
|
||||||
|
|
||||||
|
if(!this.canvases.size) {
|
||||||
|
indexOfAndSplice(ChatBackgroundPatternRenderer.INSTANCES, this);
|
||||||
|
|
||||||
|
if(this.objectUrl) {
|
||||||
|
URL.revokeObjectURL(this.objectUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public fillCanvas(canvas: HTMLCanvasElement) {
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
context.fillStyle = this.pattern;
|
||||||
|
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
// context.drawImage(this.img, 0, 0, canvas.width, canvas.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setCanvasDimensions(canvas: HTMLCanvasElement) {
|
||||||
|
canvas.width = this.options.width * window.devicePixelRatio;
|
||||||
|
canvas.height = this.options.height * window.devicePixelRatio * 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
public createCanvas() {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
this.canvases.add(canvas);
|
||||||
|
this.setCanvasDimensions(canvas);
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
}
|
@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { generateSection } from "..";
|
import { generateSection } from "..";
|
||||||
import { averageColor } from "../../../helpers/averageColor";
|
import { averageColor, averageColorFromCanvas } from "../../../helpers/averageColor";
|
||||||
import blur from "../../../helpers/blur";
|
import blur from "../../../helpers/blur";
|
||||||
import { deferredPromise } from "../../../helpers/cancellablePromise";
|
import { deferredPromise } from "../../../helpers/cancellablePromise";
|
||||||
import { attachClickEvent } from "../../../helpers/dom/clickEvent";
|
import { attachClickEvent } from "../../../helpers/dom/clickEvent";
|
||||||
@ -14,9 +14,10 @@ import { requestFile } from "../../../helpers/files";
|
|||||||
import highlightningColor from "../../../helpers/highlightningColor";
|
import highlightningColor from "../../../helpers/highlightningColor";
|
||||||
import { copy } from "../../../helpers/object";
|
import { copy } from "../../../helpers/object";
|
||||||
import sequentialDom from "../../../helpers/sequentialDom";
|
import sequentialDom from "../../../helpers/sequentialDom";
|
||||||
|
import ChatBackgroundGradientRenderer from "../../chat/gradientRenderer";
|
||||||
import { AccountWallPapers, PhotoSize, WallPaper } from "../../../layer";
|
import { AccountWallPapers, PhotoSize, WallPaper } from "../../../layer";
|
||||||
import appDocsManager, { MyDocument } from "../../../lib/appManagers/appDocsManager";
|
import appDocsManager, { MyDocument } from "../../../lib/appManagers/appDocsManager";
|
||||||
import appDownloadManager from "../../../lib/appManagers/appDownloadManager";
|
import appDownloadManager, { DownloadBlob } from "../../../lib/appManagers/appDownloadManager";
|
||||||
import appImManager from "../../../lib/appManagers/appImManager";
|
import appImManager from "../../../lib/appManagers/appImManager";
|
||||||
import appPhotosManager from "../../../lib/appManagers/appPhotosManager";
|
import appPhotosManager from "../../../lib/appManagers/appPhotosManager";
|
||||||
import appStateManager, { Theme, STATE_INIT } from "../../../lib/appManagers/appStateManager";
|
import appStateManager, { Theme, STATE_INIT } from "../../../lib/appManagers/appStateManager";
|
||||||
@ -38,6 +39,9 @@ export default class AppBackgroundTab extends SliderSuperTab {
|
|||||||
private clicked: Set<DocId> = new Set();
|
private clicked: Set<DocId> = new Set();
|
||||||
private blurCheckboxField: CheckboxField;
|
private blurCheckboxField: CheckboxField;
|
||||||
|
|
||||||
|
private wallpapersByElement: Map<HTMLElement, WallPaper> = new Map();
|
||||||
|
private elementsByKey: Map<string, HTMLElement> = new Map();
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.header.classList.add('with-border');
|
this.header.classList.add('with-border');
|
||||||
this.container.classList.add('background-container', 'background-image-container');
|
this.container.classList.add('background-container', 'background-image-container');
|
||||||
@ -71,12 +75,17 @@ export default class AppBackgroundTab extends SliderSuperTab {
|
|||||||
this.theme.background.blur = blurCheckboxField.input.checked;
|
this.theme.background.blur = blurCheckboxField.input.checked;
|
||||||
appStateManager.pushToState('settings', rootScope.settings);
|
appStateManager.pushToState('settings', rootScope.settings);
|
||||||
|
|
||||||
const active = grid.querySelector('.active') as HTMLElement;
|
|
||||||
if(!active) return;
|
|
||||||
|
|
||||||
// * wait for animation end
|
// * wait for animation end
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.setBackgroundDocument(active.dataset.slug, appDocsManager.getDoc(active.dataset.docId));
|
const active = grid.querySelector('.active') as HTMLElement;
|
||||||
|
if(!active) return;
|
||||||
|
|
||||||
|
const wallpaper = this.wallpapersByElement.get(active);
|
||||||
|
if((wallpaper as WallPaper.wallPaper).pFlags.pattern || wallpaper._ === 'wallPaperNoFile') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setBackgroundDocument(wallpaper);
|
||||||
}, 100);
|
}, 100);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -164,15 +173,13 @@ export default class AppBackgroundTab extends SliderSuperTab {
|
|||||||
wallpaper = _wallpaper as WallPaper.wallPaper;
|
wallpaper = _wallpaper as WallPaper.wallPaper;
|
||||||
wallpaper.document = appDocsManager.saveDoc(wallpaper.document);
|
wallpaper.document = appDocsManager.saveDoc(wallpaper.document);
|
||||||
|
|
||||||
container.dataset.docId = '' + wallpaper.document.id;
|
this.setBackgroundDocument(wallpaper).then(deferred.resolve, deferred.reject);
|
||||||
container.dataset.slug = wallpaper.slug;
|
|
||||||
|
|
||||||
this.setBackgroundDocument(wallpaper.slug, wallpaper.document).then(deferred.resolve, deferred.reject);
|
|
||||||
}, deferred.reject);
|
}, deferred.reject);
|
||||||
}, deferred.reject);
|
}, deferred.reject);
|
||||||
|
|
||||||
|
const key = this.getWallpaperKey(wallpaper);
|
||||||
deferred.then(() => {
|
deferred.then(() => {
|
||||||
this.clicked.delete(wallpaper.document.id);
|
this.clicked.delete(key);
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
container.remove();
|
container.remove();
|
||||||
//console.error('i saw the body drop', err);
|
//console.error('i saw the body drop', err);
|
||||||
@ -185,7 +192,7 @@ export default class AppBackgroundTab extends SliderSuperTab {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const container = this.addWallPaper(wallpaper, false);
|
const container = this.addWallPaper(wallpaper, false);
|
||||||
this.clicked.add(wallpaper.document.id);
|
this.clicked.add(key);
|
||||||
|
|
||||||
preloader.attach(container, false, deferred);
|
preloader.attach(container, false, deferred);
|
||||||
});
|
});
|
||||||
@ -202,41 +209,100 @@ export default class AppBackgroundTab extends SliderSuperTab {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private addWallPaper(wallpaper: WallPaper.wallPaper, append = true) {
|
private getColorsFromWallpaper(wallpaper: WallPaper) {
|
||||||
if(wallpaper.pFlags.pattern ||
|
return wallpaper.settings ? [
|
||||||
!wallpaper.document ||
|
wallpaper.settings.background_color,
|
||||||
(wallpaper.document as MyDocument).mime_type.indexOf('application/') === 0) {
|
wallpaper.settings.second_background_color,
|
||||||
|
wallpaper.settings.third_background_color,
|
||||||
|
wallpaper.settings.fourth_background_color
|
||||||
|
].filter(Boolean).map(color => '#' + color.toString(16)).join(',') : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private getWallpaperKey(wallpaper: WallPaper) {
|
||||||
|
return '' + wallpaper.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getWallpaperKeyFromTheme(theme: Theme) {
|
||||||
|
return '' + theme.background.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private addWallPaper(wallpaper: WallPaper, append = true) {
|
||||||
|
const colors = this.getColorsFromWallpaper(wallpaper);
|
||||||
|
const hasFile = wallpaper._ === 'wallPaper';
|
||||||
|
if((hasFile && wallpaper.pFlags.pattern && !colors)/* ||
|
||||||
|
(wallpaper.document as MyDocument).mime_type.indexOf('application/') === 0 */) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
wallpaper.document = appDocsManager.saveDoc(wallpaper.document);
|
const isDark = !!wallpaper.pFlags.dark;
|
||||||
|
|
||||||
|
const doc: MyDocument = hasFile ? (wallpaper.document = appDocsManager.saveDoc(wallpaper.document)) : undefined;
|
||||||
|
|
||||||
const container = document.createElement('div');
|
const container = document.createElement('div');
|
||||||
container.classList.add('grid-item');
|
container.classList.add('grid-item');
|
||||||
|
|
||||||
|
container.dataset.id = '' + wallpaper.id;
|
||||||
|
|
||||||
|
const key = this.getWallpaperKey(wallpaper);
|
||||||
|
this.wallpapersByElement.set(container, wallpaper);
|
||||||
|
this.elementsByKey.set(key, container);
|
||||||
|
|
||||||
const media = document.createElement('div');
|
const media = document.createElement('div');
|
||||||
media.classList.add('grid-item-media');
|
media.classList.add('grid-item-media');
|
||||||
|
|
||||||
const wrapped = wrapPhoto({
|
let wrapped: ReturnType<typeof wrapPhoto>, size: PhotoSize;
|
||||||
photo: wallpaper.document,
|
if(hasFile) {
|
||||||
message: null,
|
size = appPhotosManager.choosePhotoSize(doc, 200, 200);
|
||||||
container: media,
|
wrapped = wrapPhoto({
|
||||||
withoutPreloader: true,
|
photo: doc,
|
||||||
size: appPhotosManager.choosePhotoSize(wallpaper.document, 200, 200)
|
message: null,
|
||||||
});
|
container: media,
|
||||||
|
withoutPreloader: true,
|
||||||
|
size: size,
|
||||||
|
noFadeIn: wallpaper.pFlags.pattern
|
||||||
|
});
|
||||||
|
|
||||||
container.dataset.docId = '' + wallpaper.document.id;
|
(wrapped.loadPromises.thumb || wrapped.loadPromises.full).then(() => {
|
||||||
container.dataset.slug = wallpaper.slug;
|
sequentialDom.mutate(() => {
|
||||||
|
container.append(media);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
if(this.theme.background.type === 'image' && this.theme.background.slug === wallpaper.slug) {
|
if(wallpaper.pFlags.pattern) {
|
||||||
container.classList.add('active');
|
media.classList.add('is-pattern');
|
||||||
|
|
||||||
|
if(isDark) {
|
||||||
|
wrapped.images.full.style.display = 'none';
|
||||||
|
if(wrapped.images.thumb) {
|
||||||
|
wrapped.images.thumb.style.display = 'none';
|
||||||
|
}
|
||||||
|
} else if(wallpaper.settings?.intensity) {
|
||||||
|
wrapped.images.full.style.opacity = '' + Math.abs(wallpaper.settings.intensity) / 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
container.append(media);
|
||||||
}
|
}
|
||||||
|
|
||||||
(wrapped.loadPromises.thumb || wrapped.loadPromises.full).then(() => {
|
if(wallpaper.settings && wallpaper.settings.background_color !== undefined) {
|
||||||
sequentialDom.mutate(() => {
|
const {canvas} = ChatBackgroundGradientRenderer.create(colors);
|
||||||
container.append(media);
|
canvas.classList.add('background-colors-canvas');
|
||||||
});
|
|
||||||
});
|
if(isDark && hasFile) {
|
||||||
|
const cacheContext = appDownloadManager.getCacheContext(doc, size.type);
|
||||||
|
wrapped.loadPromises.full.then(() => {
|
||||||
|
canvas.style.webkitMaskImage = `url(${cacheContext.url})`;
|
||||||
|
canvas.style.opacity = '' + Math.abs(wallpaper.settings.intensity) / 100;
|
||||||
|
media.append(canvas);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
media.append(canvas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.getWallpaperKeyFromTheme(this.theme) === key) {
|
||||||
|
container.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
this.grid[append ? 'append' : 'prepend'](container);
|
this.grid[append ? 'append' : 'prepend'](container);
|
||||||
|
|
||||||
@ -247,19 +313,24 @@ export default class AppBackgroundTab extends SliderSuperTab {
|
|||||||
const target = findUpClassName(e.target, 'grid-item') as HTMLElement;
|
const target = findUpClassName(e.target, 'grid-item') as HTMLElement;
|
||||||
if(!target) return;
|
if(!target) return;
|
||||||
|
|
||||||
const {docId, slug} = target.dataset;
|
const wallpaper = this.wallpapersByElement.get(target);
|
||||||
if(this.clicked.has(docId)) return;
|
if(wallpaper._ === 'wallPaperNoFile') {
|
||||||
this.clicked.add(docId);
|
this.setBackgroundDocument(wallpaper);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = this.getWallpaperKey(wallpaper);
|
||||||
|
if(this.clicked.has(key)) return;
|
||||||
|
this.clicked.add(key);
|
||||||
|
|
||||||
|
const doc = wallpaper.document as MyDocument;
|
||||||
const preloader = new ProgressivePreloader({
|
const preloader = new ProgressivePreloader({
|
||||||
cancelable: true,
|
cancelable: true,
|
||||||
tryAgainOnFail: false
|
tryAgainOnFail: false
|
||||||
});
|
});
|
||||||
|
|
||||||
const doc = appDocsManager.getDoc(docId);
|
|
||||||
|
|
||||||
const load = () => {
|
const load = () => {
|
||||||
const promise = this.setBackgroundDocument(slug, doc);
|
const promise = this.setBackgroundDocument(wallpaper);
|
||||||
const cacheContext = appDownloadManager.getCacheContext(doc);
|
const cacheContext = appDownloadManager.getCacheContext(doc);
|
||||||
if(!cacheContext.url || this.theme.background.blur) {
|
if(!cacheContext.url || this.theme.background.blur) {
|
||||||
preloader.attach(target, true, promise);
|
preloader.attach(target, true, promise);
|
||||||
@ -288,15 +359,20 @@ export default class AppBackgroundTab extends SliderSuperTab {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private setBackgroundDocument = (slug: string, doc: MyDocument) => {
|
private setBackgroundDocument = (wallpaper: WallPaper) => {
|
||||||
let _tempId = ++this.tempId;
|
let _tempId = ++this.tempId;
|
||||||
const middleware = () => _tempId === this.tempId;
|
const middleware = () => _tempId === this.tempId;
|
||||||
|
|
||||||
const download = appDocsManager.downloadDoc(doc, appImManager.chat.bubbles ? appImManager.chat.bubbles.lazyLoadQueue.queueId : 0);
|
const doc = (wallpaper as WallPaper.wallPaper).document as MyDocument;
|
||||||
|
|
||||||
const deferred = deferredPromise<void>();
|
const deferred = deferredPromise<void>();
|
||||||
deferred.addNotifyListener = download.addNotifyListener;
|
let download: Promise<void> | DownloadBlob;
|
||||||
deferred.cancel = download.cancel;
|
if(doc) {
|
||||||
|
download = appDocsManager.downloadDoc(doc, appImManager.chat.bubbles ? appImManager.chat.bubbles.lazyLoadQueue.queueId : 0);
|
||||||
|
deferred.addNotifyListener = download.addNotifyListener;
|
||||||
|
deferred.cancel = download.cancel;
|
||||||
|
} else {
|
||||||
|
download = Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
download.then(() => {
|
download.then(() => {
|
||||||
if(!middleware()) {
|
if(!middleware()) {
|
||||||
@ -305,27 +381,47 @@ export default class AppBackgroundTab extends SliderSuperTab {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const background = this.theme.background;
|
const background = this.theme.background;
|
||||||
const onReady = (url: string) => {
|
const onReady = (url?: string) => {
|
||||||
//const perf = performance.now();
|
//const perf = performance.now();
|
||||||
averageColor(url).then(pixel => {
|
let getPixelPromise: Promise<Uint8ClampedArray>;
|
||||||
|
if(url && !this.theme.background.color) {
|
||||||
|
getPixelPromise = averageColor(url);
|
||||||
|
} else {
|
||||||
|
const {canvas} = ChatBackgroundGradientRenderer.create(this.getColorsFromWallpaper(wallpaper));
|
||||||
|
getPixelPromise = Promise.resolve(averageColorFromCanvas(canvas));
|
||||||
|
}
|
||||||
|
|
||||||
|
getPixelPromise.then((pixel) => {
|
||||||
if(!middleware()) {
|
if(!middleware()) {
|
||||||
deferred.resolve();
|
deferred.resolve();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hsla = highlightningColor(Array.from(pixel) as any);
|
const hsla = highlightningColor(Array.from(pixel) as any);
|
||||||
|
// const hsla = 'rgba(0, 0, 0, 0.3)';
|
||||||
//console.log(doc, hsla, performance.now() - perf);
|
//console.log(doc, hsla, performance.now() - perf);
|
||||||
|
|
||||||
|
const slug = (wallpaper as WallPaper.wallPaper).slug ?? '';
|
||||||
|
background.id = wallpaper.id;
|
||||||
|
background.intensity = wallpaper.settings?.intensity ?? 0;
|
||||||
|
background.color = this.getColorsFromWallpaper(wallpaper);
|
||||||
background.slug = slug;
|
background.slug = slug;
|
||||||
background.type = 'image';
|
|
||||||
background.highlightningColor = hsla;
|
background.highlightningColor = hsla;
|
||||||
appStateManager.pushToState('settings', rootScope.settings);
|
appStateManager.pushToState('settings', rootScope.settings);
|
||||||
|
|
||||||
this.saveToCache(slug, url);
|
if(slug) {
|
||||||
appImManager.applyCurrentTheme(slug, url).then(deferred.resolve);
|
this.saveToCache(slug, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
appImManager.applyCurrentTheme(slug, url, true).then(deferred.resolve);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if(!doc) {
|
||||||
|
onReady();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const cacheContext = appDownloadManager.getCacheContext(doc);
|
const cacheContext = appDownloadManager.getCacheContext(doc);
|
||||||
if(background.blur) {
|
if(background.blur) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -349,8 +445,7 @@ export default class AppBackgroundTab extends SliderSuperTab {
|
|||||||
|
|
||||||
private setActive = () => {
|
private setActive = () => {
|
||||||
const active = this.grid.querySelector('.active');
|
const active = this.grid.querySelector('.active');
|
||||||
const background = this.theme.background;
|
const target = this.elementsByKey.get(this.getWallpaperKeyFromTheme(this.theme));
|
||||||
const target = background.type === 'image' ? this.grid.querySelector(`.grid-item[data-slug="${background.slug}"]`) : null;
|
|
||||||
if(active === target) {
|
if(active === target) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -93,7 +93,7 @@ export default class AppBackgroundColorTab extends SliderSuperTab {
|
|||||||
private setActive() {
|
private setActive() {
|
||||||
const active = this.grid.querySelector('.active');
|
const active = this.grid.querySelector('.active');
|
||||||
const background = this.theme.background;
|
const background = this.theme.background;
|
||||||
const target = background.type === 'color' ? this.grid.querySelector(`.grid-item[data-color="${background.color}"]`) : null;
|
const target = background.color ? this.grid.querySelector(`.grid-item[data-color="${background.color}"]`) : null;
|
||||||
if(active === target) {
|
if(active === target) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -115,8 +115,10 @@ export default class AppBackgroundColorTab extends SliderSuperTab {
|
|||||||
const background = this.theme.background;
|
const background = this.theme.background;
|
||||||
const hsla = highlightningColor(rgba);
|
const hsla = highlightningColor(rgba);
|
||||||
|
|
||||||
|
background.id = '2';
|
||||||
|
background.intensity = 0;
|
||||||
|
background.slug = '';
|
||||||
background.color = hex.toLowerCase();
|
background.color = hex.toLowerCase();
|
||||||
background.type = 'color';
|
|
||||||
background.highlightningColor = hsla;
|
background.highlightningColor = hsla;
|
||||||
appStateManager.pushToState('settings', rootScope.settings);
|
appStateManager.pushToState('settings', rootScope.settings);
|
||||||
|
|
||||||
@ -133,14 +135,17 @@ export default class AppBackgroundColorTab extends SliderSuperTab {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const background = this.theme.background;
|
const background = this.theme.background;
|
||||||
|
|
||||||
|
const color = (background.color || '').split(',')[0];
|
||||||
|
const isColored = !!color && !background.slug;
|
||||||
|
|
||||||
// * set active if type is color
|
// * set active if type is color
|
||||||
if(background.type === 'color') {
|
if(isColored) {
|
||||||
this.colorPicker.onChange = this.onColorChange;
|
this.colorPicker.onChange = this.onColorChange;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.colorPicker.setColor(background.color || '#cccccc');
|
this.colorPicker.setColor(color || '#cccccc');
|
||||||
|
|
||||||
if(background.type !== 'color') {
|
if(!isColored) {
|
||||||
this.colorPicker.onChange = this.onColorChange;
|
this.colorPicker.onChange = this.onColorChange;
|
||||||
}
|
}
|
||||||
}, 0);
|
}, 0);
|
||||||
|
@ -6,7 +6,28 @@
|
|||||||
|
|
||||||
import renderImageFromUrl from "./dom/renderImageFromUrl";
|
import renderImageFromUrl from "./dom/renderImageFromUrl";
|
||||||
|
|
||||||
export const averageColor = (imageUrl: string): Promise<Uint8ClampedArray> => {
|
export function averageColorFromCanvas(canvas: HTMLCanvasElement) {
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
|
||||||
|
const pixel = new Array(4).fill(0);
|
||||||
|
const pixels = context.getImageData(0, 0, canvas.width, canvas.height).data;
|
||||||
|
for(let i = 0; i < pixels.length; i += 4) {
|
||||||
|
pixel[0] += pixels[i];
|
||||||
|
pixel[1] += pixels[i + 1];
|
||||||
|
pixel[2] += pixels[i + 2];
|
||||||
|
pixel[3] += pixels[i + 3];
|
||||||
|
}
|
||||||
|
|
||||||
|
const pixelsLength = pixels.length / 4;
|
||||||
|
const outPixel = new Uint8ClampedArray(4);
|
||||||
|
outPixel[0] = pixel[0] / pixelsLength;
|
||||||
|
outPixel[1] = pixel[1] / pixelsLength;
|
||||||
|
outPixel[2] = pixel[2] / pixelsLength;
|
||||||
|
outPixel[3] = pixel[3] / pixelsLength;
|
||||||
|
return outPixel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function averageColor(imageUrl: string) {
|
||||||
const img = document.createElement('img');
|
const img = document.createElement('img');
|
||||||
return new Promise<Uint8ClampedArray>((resolve) => {
|
return new Promise<Uint8ClampedArray>((resolve) => {
|
||||||
renderImageFromUrl(img, imageUrl, () => {
|
renderImageFromUrl(img, imageUrl, () => {
|
||||||
@ -25,23 +46,7 @@ export const averageColor = (imageUrl: string): Promise<Uint8ClampedArray> => {
|
|||||||
|
|
||||||
const context = canvas.getContext('2d');
|
const context = canvas.getContext('2d');
|
||||||
context.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight, 0, 0, canvas.width, canvas.height);
|
context.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight, 0, 0, canvas.width, canvas.height);
|
||||||
|
resolve(averageColorFromCanvas(canvas));
|
||||||
const pixel = new Array(4).fill(0);
|
|
||||||
const pixels = context.getImageData(0, 0, canvas.width, canvas.height).data;
|
|
||||||
for(let i = 0; i < pixels.length; i += 4) {
|
|
||||||
pixel[0] += pixels[i];
|
|
||||||
pixel[1] += pixels[i + 1];
|
|
||||||
pixel[2] += pixels[i + 2];
|
|
||||||
pixel[3] += pixels[i + 3];
|
|
||||||
}
|
|
||||||
|
|
||||||
const pixelsLength = pixels.length / 4;
|
|
||||||
const outPixel = new Uint8ClampedArray(4);
|
|
||||||
outPixel[0] = pixel[0] / pixelsLength;
|
|
||||||
outPixel[1] = pixel[1] / pixelsLength;
|
|
||||||
outPixel[2] = pixel[2] / pixelsLength;
|
|
||||||
outPixel[3] = pixel[3] / pixelsLength;
|
|
||||||
resolve(outPixel);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -15,6 +15,7 @@ export default function blobSafeMimeType(mimeType: string) {
|
|||||||
'image/jpeg',
|
'image/jpeg',
|
||||||
'image/png',
|
'image/png',
|
||||||
'image/gif',
|
'image/gif',
|
||||||
|
'image/svg+xml',
|
||||||
'image/webp',
|
'image/webp',
|
||||||
'image/bmp',
|
'image/bmp',
|
||||||
'video/mp4',
|
'video/mp4',
|
||||||
|
@ -104,7 +104,11 @@ export function hslaStringToRgba(hsla: string) {
|
|||||||
|
|
||||||
export function hexaToRgba(hexa: string) {
|
export function hexaToRgba(hexa: string) {
|
||||||
const arr: ColorRgba = [] as any;
|
const arr: ColorRgba = [] as any;
|
||||||
const offset = 1;
|
const offset = hexa[0] === '#' ? 1 : 0;
|
||||||
|
if(hexa.length === (5 + offset)) {
|
||||||
|
hexa = (offset ? '#' : '') + '0' + hexa.slice(offset);
|
||||||
|
}
|
||||||
|
|
||||||
if(hexa.length === (3 + offset)) {
|
if(hexa.length === (3 + offset)) {
|
||||||
for(let i = offset; i < hexa.length; ++i) {
|
for(let i = offset; i < hexa.length; ++i) {
|
||||||
arr.push(parseInt(hexa[i] + hexa[i], 16));
|
arr.push(parseInt(hexa[i] + hexa[i], 16));
|
||||||
|
@ -52,7 +52,10 @@ export default function renderImageFromUrl(
|
|||||||
}, {once: true});
|
}, {once: true});
|
||||||
|
|
||||||
if(callback) {
|
if(callback) {
|
||||||
loader.addEventListener('error', callback);
|
loader.addEventListener('error', (err) => {
|
||||||
|
console.error('Render image from url failed:', err, url, loader);
|
||||||
|
callback();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@ import appDraftsManager from './appDraftsManager';
|
|||||||
import serverTimeManager from '../mtproto/serverTimeManager';
|
import serverTimeManager from '../mtproto/serverTimeManager';
|
||||||
import stateStorage from '../stateStorage';
|
import stateStorage from '../stateStorage';
|
||||||
import appDownloadManager from './appDownloadManager';
|
import appDownloadManager from './appDownloadManager';
|
||||||
import { AppStateManager } from './appStateManager';
|
import { AppStateManager, STATE_INIT } from './appStateManager';
|
||||||
import { MOUNT_CLASS_TO } from '../../config/debug';
|
import { MOUNT_CLASS_TO } from '../../config/debug';
|
||||||
import appNavigationController from '../../components/appNavigationController';
|
import appNavigationController from '../../components/appNavigationController';
|
||||||
import appNotificationsManager from './appNotificationsManager';
|
import appNotificationsManager from './appNotificationsManager';
|
||||||
@ -128,10 +128,12 @@ export class AppImManager {
|
|||||||
private chatsSelectTabDebounced: () => void;
|
private chatsSelectTabDebounced: () => void;
|
||||||
|
|
||||||
public markupTooltip: MarkupTooltip;
|
public markupTooltip: MarkupTooltip;
|
||||||
private backgroundPromises: {[slug: string]: Promise<string>} = {};
|
private backgroundPromises: {[slug: string]: Promise<string>};
|
||||||
|
|
||||||
private topbarCall: TopbarCall;
|
private topbarCall: TopbarCall;
|
||||||
emojiAnimationContainer: HTMLDivElement;
|
public emojiAnimationContainer: HTMLDivElement;
|
||||||
|
|
||||||
|
private lastBackgroundUrl: string;
|
||||||
|
|
||||||
get myId() {
|
get myId() {
|
||||||
return rootScope.myId;
|
return rootScope.myId;
|
||||||
@ -147,6 +149,14 @@ export class AppImManager {
|
|||||||
|
|
||||||
this.log = logger('IM', LogTypes.Log | LogTypes.Warn | LogTypes.Debug | LogTypes.Error);
|
this.log = logger('IM', LogTypes.Log | LogTypes.Warn | LogTypes.Debug | LogTypes.Error);
|
||||||
|
|
||||||
|
this.backgroundPromises = {};
|
||||||
|
STATE_INIT.settings.themes.forEach(theme => {
|
||||||
|
if(theme.background.slug) {
|
||||||
|
const url = /* window.location.origin + window.location.pathname + */'assets/img/' + theme.background.slug + '.svg';
|
||||||
|
this.backgroundPromises[theme.background.slug] = Promise.resolve(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.selectTab(0);
|
this.selectTab(0);
|
||||||
|
|
||||||
window.addEventListener('blur', () => {
|
window.addEventListener('blur', () => {
|
||||||
@ -205,7 +215,9 @@ export class AppImManager {
|
|||||||
animationIntersector.checkAnimations(false);
|
animationIntersector.checkAnimations(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// setTimeout(() => {
|
||||||
this.applyCurrentTheme();
|
this.applyCurrentTheme();
|
||||||
|
// }, 0);
|
||||||
|
|
||||||
// * fix simultaneous opened both sidebars, can happen when floating sidebar is opened with left sidebar
|
// * fix simultaneous opened both sidebars, can happen when floating sidebar is opened with left sidebar
|
||||||
mediaSizes.addEventListener('changeScreen', (from, to) => {
|
mediaSizes.addEventListener('changeScreen', (from, to) => {
|
||||||
@ -217,6 +229,13 @@ export class AppImManager {
|
|||||||
this.appendEmojiAnimationContainer(to);
|
this.appendEmojiAnimationContainer(to);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const resizeBackgroundDebounced = debounce(() => {
|
||||||
|
this.setBackground(this.lastBackgroundUrl, false);
|
||||||
|
}, 200, false, true);
|
||||||
|
mediaSizes.addEventListener('resize', () => {
|
||||||
|
resizeBackgroundDebounced();
|
||||||
|
});
|
||||||
|
|
||||||
rootScope.addEventListener('history_focus', (e) => {
|
rootScope.addEventListener('history_focus', (e) => {
|
||||||
let {peerId, threadId, mid, startParam} = e;
|
let {peerId, threadId, mid, startParam} = e;
|
||||||
if(threadId) threadId = appMessagesIdsManager.generateMessageId(threadId);
|
if(threadId) threadId = appMessagesIdsManager.generateMessageId(threadId);
|
||||||
@ -309,6 +328,11 @@ export class AppImManager {
|
|||||||
popup.show();
|
popup.show();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// remove scroll listener when setting chat to tray
|
||||||
|
rootScope.addEventListener('chat_changing', ({to}) => {
|
||||||
|
this.toggleChatGradientAnimation(to);
|
||||||
|
});
|
||||||
|
|
||||||
stateStorage.get('chatPositions').then((c) => {
|
stateStorage.get('chatPositions').then((c) => {
|
||||||
stateStorage.setToCache('chatPositions', c || {});
|
stateStorage.setToCache('chatPositions', c || {});
|
||||||
});
|
});
|
||||||
@ -551,6 +575,14 @@ export class AppImManager {
|
|||||||
this.attachKeydownListener();
|
this.attachKeydownListener();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private toggleChatGradientAnimation(activatingChat: Chat) {
|
||||||
|
this.chats.forEach(chat => {
|
||||||
|
if(chat.gradientRenderer) {
|
||||||
|
chat.gradientRenderer.scrollAnimate(rootScope.settings.animationsEnabled && chat === activatingChat);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private appendEmojiAnimationContainer(screen: ScreenSize) {
|
private appendEmojiAnimationContainer(screen: ScreenSize) {
|
||||||
const appendTo = screen === ScreenSize.mobile ? this.columnEl : document.body;
|
const appendTo = screen === ScreenSize.mobile ? this.columnEl : document.body;
|
||||||
if(this.emojiAnimationContainer.parentElement !== appendTo) {
|
if(this.emojiAnimationContainer.parentElement !== appendTo) {
|
||||||
@ -1005,19 +1037,19 @@ export class AppImManager {
|
|||||||
public setCurrentBackground(broadcastEvent = false) {
|
public setCurrentBackground(broadcastEvent = false) {
|
||||||
const theme = rootScope.getTheme();
|
const theme = rootScope.getTheme();
|
||||||
|
|
||||||
if(theme.background.type === 'image' || (theme.background.type === 'default' && theme.background.slug)) {
|
if(theme.background.slug) {
|
||||||
const defaultTheme = AppStateManager.STATE_INIT.settings.themes.find(t => t.name === theme.name);
|
const defaultTheme = AppStateManager.STATE_INIT.settings.themes.find(t => t.name === theme.name);
|
||||||
const isDefaultBackground = theme.background.blur === defaultTheme.background.blur &&
|
// const isDefaultBackground = theme.background.blur === defaultTheme.background.blur &&
|
||||||
theme.background.slug === defaultTheme.background.slug;
|
// theme.background.slug === defaultTheme.background.slug;
|
||||||
|
|
||||||
if(!isDefaultBackground) {
|
// if(!isDefaultBackground) {
|
||||||
return this.getBackground(theme.background.slug).then((url) => {
|
return this.getBackground(theme.background.slug).then((url) => {
|
||||||
return this.setBackground(url, broadcastEvent);
|
return this.setBackground(url, broadcastEvent);
|
||||||
}, () => { // * if NO_ENTRY_FOUND
|
}, () => { // * if NO_ENTRY_FOUND
|
||||||
theme.background = copy(defaultTheme.background); // * reset background
|
theme.background = copy(defaultTheme.background); // * reset background
|
||||||
return this.setBackground('', true);
|
return this.setBackground('', true);
|
||||||
});
|
});
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.setBackground('', broadcastEvent);
|
return this.setBackground('', broadcastEvent);
|
||||||
@ -1031,6 +1063,7 @@ export class AppImManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public setBackground(url: string, broadcastEvent = true): Promise<void> {
|
public setBackground(url: string, broadcastEvent = true): Promise<void> {
|
||||||
|
this.lastBackgroundUrl = url;
|
||||||
const promises = this.chats.map(chat => chat.setBackground(url));
|
const promises = this.chats.map(chat => chat.setBackground(url));
|
||||||
return promises[promises.length - 1].then(() => {
|
return promises[promises.length - 1].then(() => {
|
||||||
if(broadcastEvent) {
|
if(broadcastEvent) {
|
||||||
@ -1133,6 +1166,8 @@ export class AppImManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
I18n.setTimeFormat(rootScope.settings.timeFormat);
|
I18n.setTimeFormat(rootScope.settings.timeFormat);
|
||||||
|
|
||||||
|
this.toggleChatGradientAnimation(this.chat);
|
||||||
};
|
};
|
||||||
|
|
||||||
// * не могу использовать тут TransitionSlider, так как мне нужен отрисованный блок рядом
|
// * не могу использовать тут TransitionSlider, так как мне нужен отрисованный блок рядом
|
||||||
@ -1433,7 +1468,7 @@ export class AppImManager {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if(this.chats.length) {
|
if(this.chats.length) {
|
||||||
chat.backgroundEl.append(this.chat.backgroundEl.lastElementChild.cloneNode(true));
|
chat.setBackground(this.lastBackgroundUrl, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.chats.push(chat);
|
this.chats.push(chat);
|
||||||
@ -1556,7 +1591,10 @@ export class AppImManager {
|
|||||||
// * wait for cached render
|
// * wait for cached render
|
||||||
const promise = result?.cached ? result.promise : Promise.resolve();
|
const promise = result?.cached ? result.promise : Promise.resolve();
|
||||||
if(peerId) {
|
if(peerId) {
|
||||||
promise.then(() => {
|
Promise.all([
|
||||||
|
promise,
|
||||||
|
chat.setBackgroundPromise
|
||||||
|
]).then(() => {
|
||||||
//window.requestAnimationFrame(() => {
|
//window.requestAnimationFrame(() => {
|
||||||
setTimeout(() => { // * setTimeout is better here
|
setTimeout(() => { // * setTimeout is better here
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -457,7 +457,8 @@ export class AppNotificationsManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.checkMuteUntilTimeout = window.setTimeout(this.checkMuteUntil, (closestMuteUntil - timestamp) * 1000);
|
const timeout = Math.min(1800e3, (closestMuteUntil - timestamp) * 1000);
|
||||||
|
this.checkMuteUntilTimeout = window.setTimeout(this.checkMuteUntil, timeout);
|
||||||
};
|
};
|
||||||
|
|
||||||
public savePeerSettings({key, peerId, settings}: {
|
public savePeerSettings({key, peerId, settings}: {
|
||||||
|
@ -33,11 +33,13 @@ const STATE_VERSION = App.version;
|
|||||||
const BUILD = App.build;
|
const BUILD = App.build;
|
||||||
|
|
||||||
export type Background = {
|
export type Background = {
|
||||||
type: 'color' | 'image' | 'default',
|
type?: 'color' | 'image' | 'default', // ! DEPRECATED
|
||||||
blur: boolean,
|
blur: boolean,
|
||||||
highlightningColor?: string,
|
highlightningColor?: string,
|
||||||
color?: string,
|
color?: string,
|
||||||
slug?: string,
|
slug?: string, // image slug
|
||||||
|
intensity?: number, // pattern intensity
|
||||||
|
id: string | number, // wallpaper id
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Theme = {
|
export type Theme = {
|
||||||
@ -186,18 +188,23 @@ export const STATE_INIT: State = {
|
|||||||
themes: [{
|
themes: [{
|
||||||
name: 'day',
|
name: 'day',
|
||||||
background: {
|
background: {
|
||||||
type: 'image',
|
|
||||||
blur: false,
|
blur: false,
|
||||||
slug: 'ByxGo2lrMFAIAAAAmkJxZabh8eM', // * new blurred camomile,
|
slug: 'pattern',
|
||||||
highlightningColor: 'hsla(85.5319, 36.9171%, 40.402%, 0.4)'
|
color: '#dbddbb,#6ba587,#d5d88d,#88b884',
|
||||||
|
highlightningColor: 'hsla(86.4, 43.846153%, 45.117647%, .4)',
|
||||||
|
intensity: 50,
|
||||||
|
id: '1'
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
name: 'night',
|
name: 'night',
|
||||||
background: {
|
background: {
|
||||||
type: 'color',
|
|
||||||
blur: false,
|
blur: false,
|
||||||
color: '#0f0f0f',
|
slug: 'pattern',
|
||||||
highlightningColor: 'hsla(0, 0%, 3.82353%, 0.4)'
|
// color: '#dbddbb,#6ba587,#d5d88d,#88b884',
|
||||||
|
color: '#fec496,#dd6cb9,#962fbf,#4f5bd5',
|
||||||
|
highlightningColor: 'hsla(299.142857, 44.166666%, 37.470588%, .4)',
|
||||||
|
intensity: -50,
|
||||||
|
id: '-1'
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
theme: 'system',
|
theme: 'system',
|
||||||
@ -506,6 +513,32 @@ export class AppStateManager extends EventListenerBase<{
|
|||||||
result.length = 0;
|
result.length = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// * migrate backgrounds (March 13, 2022; to version 1.3.0)
|
||||||
|
if(compareVersion(state.version, '1.3.0') === -1) {
|
||||||
|
let migrated = false;
|
||||||
|
state.settings.themes.forEach((theme, idx, arr) => {
|
||||||
|
if((
|
||||||
|
theme.name === 'day' &&
|
||||||
|
theme.background.slug === 'ByxGo2lrMFAIAAAAmkJxZabh8eM' &&
|
||||||
|
theme.background.type === 'image'
|
||||||
|
) || (
|
||||||
|
theme.name === 'night' &&
|
||||||
|
theme.background.color === '#0f0f0f' &&
|
||||||
|
theme.background.type === 'color'
|
||||||
|
)) {
|
||||||
|
const newTheme = STATE_INIT.settings.themes.find(newTheme => newTheme.name === theme.name);
|
||||||
|
if(newTheme) {
|
||||||
|
arr[idx] = copy(newTheme);
|
||||||
|
migrated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if(migrated) {
|
||||||
|
this.pushToState('settings', state.settings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if(compareVersion(state.version, STATE_VERSION) !== 0) {
|
if(compareVersion(state.version, STATE_VERSION) !== 0) {
|
||||||
this.newVersion = STATE_VERSION;
|
this.newVersion = STATE_VERSION;
|
||||||
|
@ -272,6 +272,12 @@ export class ApiFileManager {
|
|||||||
return cryptoWorker.invokeCrypto('gzipUncompress', bytes.slice().buffer, true) as Promise<string>;
|
return cryptoWorker.invokeCrypto('gzipUncompress', bytes.slice().buffer, true) as Promise<string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private uncompressTGV = (bytes: Uint8Array, fileName: string) => {
|
||||||
|
//this.log('uncompressTGS', bytes, bytes.slice().buffer);
|
||||||
|
// slice нужен потому что в uint8array - 5053 length, в arraybuffer - 5084
|
||||||
|
return cryptoWorker.invokeCrypto('gzipUncompress', bytes.slice().buffer, true) as Promise<string>;
|
||||||
|
};
|
||||||
|
|
||||||
private convertWebp = (bytes: Uint8Array, fileName: string) => {
|
private convertWebp = (bytes: Uint8Array, fileName: string) => {
|
||||||
const convertPromise = deferredPromise<Uint8Array>();
|
const convertPromise = deferredPromise<Uint8Array>();
|
||||||
|
|
||||||
@ -324,7 +330,10 @@ export class ApiFileManager {
|
|||||||
|
|
||||||
let process: ApiFileManager['uncompressTGS'] | ApiFileManager['convertWebp'];
|
let process: ApiFileManager['uncompressTGS'] | ApiFileManager['convertWebp'];
|
||||||
|
|
||||||
if(options.mimeType === 'image/webp' && !isWebpSupported()) {
|
if(options.mimeType === 'application/x-tgwallpattern') {
|
||||||
|
process = this.uncompressTGV;
|
||||||
|
options.mimeType = 'image/svg+xml';
|
||||||
|
} else if(options.mimeType === 'image/webp' && !isWebpSupported()) {
|
||||||
process = this.convertWebp;
|
process = this.convertWebp;
|
||||||
options.mimeType = 'image/png';
|
options.mimeType = 'image/png';
|
||||||
} else if(options.mimeType === 'application/x-tgsticker') {
|
} else if(options.mimeType === 'application/x-tgsticker') {
|
||||||
|
@ -8,6 +8,7 @@ import blurActiveElement from "../helpers/dom/blurActiveElement";
|
|||||||
import loadFonts from "../helpers/dom/loadFonts";
|
import loadFonts from "../helpers/dom/loadFonts";
|
||||||
import appStateManager from "../lib/appManagers/appStateManager";
|
import appStateManager from "../lib/appManagers/appStateManager";
|
||||||
import I18n from "../lib/langPack";
|
import I18n from "../lib/langPack";
|
||||||
|
import rootScope from "../lib/rootScope";
|
||||||
import Page from "./page";
|
import Page from "./page";
|
||||||
|
|
||||||
let onFirstMount = () => {
|
let onFirstMount = () => {
|
||||||
@ -16,9 +17,7 @@ let onFirstMount = () => {
|
|||||||
// ! TOO SLOW
|
// ! TOO SLOW
|
||||||
/* appStateManager.saveState(); */
|
/* appStateManager.saveState(); */
|
||||||
|
|
||||||
import('../lib/rootScope').then(m => {
|
rootScope.dispatchEvent('im_mount');
|
||||||
m.default.dispatchEvent('im_mount');
|
|
||||||
});
|
|
||||||
|
|
||||||
if(!I18n.requestedServerLanguage) {
|
if(!I18n.requestedServerLanguage) {
|
||||||
I18n.getCacheLangPack().then(langPack => {
|
I18n.getCacheLangPack().then(langPack => {
|
||||||
@ -28,64 +27,33 @@ let onFirstMount = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
blurActiveElement();
|
page.pageEl.style.display = '';
|
||||||
return loadFonts().then(() => {
|
|
||||||
return new Promise<void>((resolve) => {
|
|
||||||
window.requestAnimationFrame(() => {
|
|
||||||
// setTimeout(() => {
|
|
||||||
const promise = import('../lib/appManagers/appDialogsManager');
|
|
||||||
promise.finally(async() => {
|
|
||||||
document.getElementById('auth-pages').remove();
|
|
||||||
//alert('pageIm!');
|
|
||||||
resolve();
|
|
||||||
|
|
||||||
//AudioContext && global.navigator && global.navigator.mediaDevices && global.navigator.mediaDevices.getUserMedia && global.WebAssembly;
|
|
||||||
|
|
||||||
/* // @ts-ignore
|
|
||||||
var AudioContext = globalThis.AudioContext || globalThis.webkitAudioContext;
|
|
||||||
alert('AudioContext:' + typeof(AudioContext));
|
|
||||||
// @ts-ignore
|
|
||||||
alert('global.navigator:' + typeof(navigator));
|
|
||||||
alert('navigator.mediaDevices:' + typeof(navigator.mediaDevices));
|
|
||||||
alert('navigator.mediaDevices.getUserMedia:' + typeof(navigator.mediaDevices?.getUserMedia));
|
|
||||||
alert('global.WebAssembly:' + typeof(WebAssembly)); */
|
|
||||||
|
|
||||||
//(Array.from(document.getElementsByClassName('rp')) as HTMLElement[]).forEach(el => ripple(el));
|
|
||||||
});
|
|
||||||
// }, 5e3);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
//let promise = /* Promise.resolve() */.then(() => {//import('../lib/services').then(services => {
|
//alert('pageIm!');
|
||||||
/* fetch('assets/img/camomile.jpg')
|
|
||||||
.then(res => res.blob())
|
|
||||||
.then(blob => {
|
|
||||||
let img = new Image();
|
|
||||||
let url = URL.createObjectURL(blob);
|
|
||||||
img.src = url;
|
|
||||||
img.onload = () => {
|
|
||||||
let id = 'chat-background-canvas';
|
|
||||||
var canvas = document.getElementById(id) as HTMLCanvasElement;
|
|
||||||
//URL.revokeObjectURL(url);
|
|
||||||
|
|
||||||
let elements = ['.chat-container'].map(selector => {
|
//AudioContext && global.navigator && global.navigator.mediaDevices && global.navigator.mediaDevices.getUserMedia && global.WebAssembly;
|
||||||
return document.querySelector(selector) as HTMLDivElement;
|
|
||||||
});
|
/* // @ts-ignore
|
||||||
|
var AudioContext = globalThis.AudioContext || globalThis.webkitAudioContext;
|
||||||
stackBlurImage(img, id, 15, 0);
|
alert('AudioContext:' + typeof(AudioContext));
|
||||||
|
// @ts-ignore
|
||||||
canvas.toBlob(blob => {
|
alert('global.navigator:' + typeof(navigator));
|
||||||
//let dataUrl = canvas.toDataURL('image/jpeg', 1);
|
alert('navigator.mediaDevices:' + typeof(navigator.mediaDevices));
|
||||||
let dataUrl = URL.createObjectURL(blob);
|
alert('navigator.mediaDevices.getUserMedia:' + typeof(navigator.mediaDevices?.getUserMedia));
|
||||||
|
alert('global.WebAssembly:' + typeof(WebAssembly)); */
|
||||||
elements.forEach(el => {
|
|
||||||
el.style.backgroundImage = 'url(' + dataUrl + ')';
|
//(Array.from(document.getElementsByClassName('rp')) as HTMLElement[]).forEach(el => ripple(el));
|
||||||
});
|
|
||||||
}, 'image/jpeg', 1);
|
blurActiveElement();
|
||||||
};
|
|
||||||
}); */
|
return Promise.all([
|
||||||
//});
|
loadFonts()/* .then(() => new Promise((resolve) => window.requestAnimationFrame(resolve))) */,
|
||||||
|
import('../lib/appManagers/appDialogsManager')
|
||||||
|
]).then(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
document.getElementById('auth-pages').remove();
|
||||||
|
}, 1e3);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const page = new Page('page-chats', false, onFirstMount);
|
const page = new Page('page-chats', false, onFirstMount);
|
||||||
|
@ -669,10 +669,26 @@ $background-transition-total-time: #{$input-transition-time - $background-transi
|
|||||||
}
|
}
|
||||||
|
|
||||||
&-item {
|
&-item {
|
||||||
background-image: url('assets/img/bg.jpeg');
|
&.is-image {
|
||||||
background-size: cover;
|
background-image: url('assets/img/bg.jpeg');
|
||||||
background-position: center center;
|
background-position: center center;
|
||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-pattern {
|
||||||
|
margin: 0 !important;
|
||||||
|
background-image: none !important;
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: repeat-x;
|
||||||
|
background-color: #000 !important;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
// mix-blend-mode: overlay;
|
||||||
|
height: 150%;
|
||||||
|
top: -25%;
|
||||||
|
}
|
||||||
|
|
||||||
@include animation-level(2) {
|
@include animation-level(2) {
|
||||||
transition: opacity var(--transition-standard-out);
|
transition: opacity var(--transition-standard-out);
|
||||||
@ -698,6 +714,29 @@ $background-transition-total-time: #{$input-transition-time - $background-transi
|
|||||||
transition: transform var(--transition-standard-in), opacity var(--transition-standard-in) !important;
|
transition: transform var(--transition-standard-in), opacity var(--transition-standard-in) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-canvas {
|
||||||
|
--opacity-max: 1;
|
||||||
|
opacity: var(--opacity-max);
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-pattern-canvas {
|
||||||
|
mix-blend-mode: overlay;
|
||||||
|
// height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-color-canvas {
|
||||||
|
height: 100%;
|
||||||
|
// transform: scale(1.5);
|
||||||
|
// transform: scaleY(1.5);
|
||||||
|
|
||||||
|
// mask-repeat: round;
|
||||||
|
|
||||||
|
mask-size: cover;
|
||||||
|
mask-position: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -358,8 +358,8 @@ ul.chatlist {
|
|||||||
height: 1.25rem;
|
height: 1.25rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
border-radius: .1875rem;
|
border-radius: .125rem;
|
||||||
margin-top: -0.1875rem;
|
margin-top: -0.125rem;
|
||||||
margin-right: 0.375rem;
|
margin-right: 0.375rem;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
@ -372,7 +372,7 @@ ul.chatlist {
|
|||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
font-size: .625rem;
|
font-size: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-photo {
|
.media-photo {
|
||||||
|
@ -1197,6 +1197,14 @@
|
|||||||
&-media {
|
&-media {
|
||||||
transition: transform .2s ease-in-out;
|
transition: transform .2s ease-in-out;
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
|
|
||||||
|
&.is-pattern {
|
||||||
|
background-color: #000;
|
||||||
|
|
||||||
|
.media-photo {
|
||||||
|
mix-blend-mode: overlay;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1210,6 +1218,14 @@
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.background-colors-canvas {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
-webkit-mask-position: center;
|
||||||
|
-webkit-mask-size: contain;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.background-image-container {
|
.background-image-container {
|
||||||
|
@ -41,6 +41,7 @@ html:not(.is-safari):not(.is-ios) {
|
|||||||
max-height: 12.5rem;
|
max-height: 12.5rem;
|
||||||
border-radius: $border-radius-medium;
|
border-radius: $border-radius-medium;
|
||||||
background-color: var(--scrollbar-color);
|
background-color: var(--scrollbar-color);
|
||||||
|
backdrop-filter: blur(100);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user