Browse Source

New backgrounds: gradients & patterns

master
Eduard Kuzmenko 3 years ago
parent
commit
cd066bf4a9
  1. 7
      src/components/chat/bubbles.ts
  2. 174
      src/components/chat/chat.ts
  3. 393
      src/components/chat/gradientRenderer.ts
  4. 121
      src/components/chat/patternRenderer.ts
  5. 177
      src/components/sidebarLeft/tabs/background.ts
  6. 15
      src/components/sidebarLeft/tabs/backgroundColor.ts
  7. 41
      src/helpers/averageColor.ts
  8. 1
      src/helpers/blob/blobSafeMimeType.ts
  9. 6
      src/helpers/color.ts
  10. 5
      src/helpers/dom/renderImageFromUrl.ts
  11. 58
      src/lib/appManagers/appImManager.ts
  12. 3
      src/lib/appManagers/appNotificationsManager.ts
  13. 49
      src/lib/appManagers/appStateManager.ts
  14. 11
      src/lib/mtproto/apiFileManager.ts
  15. 56
      src/pages/pageIm.ts
  16. 41
      src/scss/partials/_chat.scss
  17. 6
      src/scss/partials/_chatlist.scss
  18. 16
      src/scss/partials/_leftSidebar.scss
  19. 1
      src/scss/partials/_scrollable.scss

7
src/components/chat/bubbles.ts

@ -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) => {

174
src/components/chat/chat.ts

@ -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';
@ -78,6 +79,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,
public appChatsManager: AppChatsManager, public appChatsManager: AppChatsManager,
@ -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');
}
}
let gradientRenderer: ChatBackgroundGradientRenderer;
const color = theme.background.color;
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';
// }
} }
if(theme.background.type === 'color') { if(patternRenderer) {
item.style.backgroundColor = theme.background.color; const setOpacityTo = isDarkPattern ? gradientCanvas : patternCanvas;
item.style.backgroundImage = 'none'; setOpacityTo.style.setProperty('--opacity-max', '' + Math.abs(intensity));
} }
return new Promise<void>((resolve) => { 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 ? () => {
if(previousGradientRenderer) {
previousGradientRenderer.cleanup();
}
prev.remove(); prev.remove();
} : null); } : 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

@ -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

@ -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;
}
}

177
src/components/sidebarLeft/tabs/background.ts

@ -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);
// * wait for animation end
setTimeout(() => {
const active = grid.querySelector('.active') as HTMLElement; const active = grid.querySelector('.active') as HTMLElement;
if(!active) return; if(!active) return;
// * wait for animation end const wallpaper = this.wallpapersByElement.get(active);
setTimeout(() => { if((wallpaper as WallPaper.wallPaper).pFlags.pattern || wallpaper._ === 'wallPaperNoFile') {
this.setBackgroundDocument(active.dataset.slug, appDocsManager.getDoc(active.dataset.docId)); 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,42 +209,101 @@ 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) {
size = appPhotosManager.choosePhotoSize(doc, 200, 200);
wrapped = wrapPhoto({
photo: doc,
message: null, message: null,
container: media, container: media,
withoutPreloader: true, withoutPreloader: true,
size: appPhotosManager.choosePhotoSize(wallpaper.document, 200, 200) size: size,
noFadeIn: wallpaper.pFlags.pattern
}); });
container.dataset.docId = '' + wallpaper.document.id;
container.dataset.slug = wallpaper.slug;
if(this.theme.background.type === 'image' && this.theme.background.slug === wallpaper.slug) {
container.classList.add('active');
}
(wrapped.loadPromises.thumb || wrapped.loadPromises.full).then(() => { (wrapped.loadPromises.thumb || wrapped.loadPromises.full).then(() => {
sequentialDom.mutate(() => { sequentialDom.mutate(() => {
container.append(media); container.append(media);
}); });
}); });
if(wallpaper.pFlags.pattern) {
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);
}
if(wallpaper.settings && wallpaper.settings.background_color !== undefined) {
const {canvas} = ChatBackgroundGradientRenderer.create(colors);
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);
return container; return 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>();
let download: Promise<void> | DownloadBlob;
if(doc) {
download = appDocsManager.downloadDoc(doc, appImManager.chat.bubbles ? appImManager.chat.bubbles.lazyLoadQueue.queueId : 0);
deferred.addNotifyListener = download.addNotifyListener; deferred.addNotifyListener = download.addNotifyListener;
deferred.cancel = download.cancel; 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);
if(slug) {
this.saveToCache(slug, url); this.saveToCache(slug, url);
appImManager.applyCurrentTheme(slug, url).then(deferred.resolve); }
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;
} }

15
src/components/sidebarLeft/tabs/backgroundColor.ts

@ -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);

41
src/helpers/averageColor.ts

@ -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);
}); });
}); });
}; };

1
src/helpers/blob/blobSafeMimeType.ts

@ -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',

6
src/helpers/color.ts

@ -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));

5
src/helpers/dom/renderImageFromUrl.ts

@ -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();
});
} }
} }
} }

58
src/lib/appManagers/appImManager.ts

@ -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(() => {

3
src/lib/appManagers/appNotificationsManager.ts

@ -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}: {

49
src/lib/appManagers/appStateManager.ts

@ -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 = {
@ -185,18 +187,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',
@ -505,6 +512,32 @@ export class AppStateManager extends EventListenerBase<{
} }
} }
// * 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;
} }

11
src/lib/mtproto/apiFileManager.ts

@ -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') {

56
src/pages/pageIm.ts

@ -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,16 +27,9 @@ 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!'); //alert('pageIm!');
resolve();
//AudioContext && global.navigator && global.navigator.mediaDevices && global.navigator.mediaDevices.getUserMedia && global.WebAssembly; //AudioContext && global.navigator && global.navigator.mediaDevices && global.navigator.mediaDevices.getUserMedia && global.WebAssembly;
@ -51,41 +43,17 @@ let onFirstMount = () => {
alert('global.WebAssembly:' + typeof(WebAssembly)); */ alert('global.WebAssembly:' + typeof(WebAssembly)); */
//(Array.from(document.getElementsByClassName('rp')) as HTMLElement[]).forEach(el => ripple(el)); //(Array.from(document.getElementsByClassName('rp')) as HTMLElement[]).forEach(el => ripple(el));
});
// }, 5e3);
});
})
});
//let promise = /* Promise.resolve() */.then(() => {//import('../lib/services').then(services => { blurActiveElement();
/* 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 => {
return document.querySelector(selector) as HTMLDivElement;
});
stackBlurImage(img, id, 15, 0);
canvas.toBlob(blob => {
//let dataUrl = canvas.toDataURL('image/jpeg', 1);
let dataUrl = URL.createObjectURL(blob);
elements.forEach(el => { return Promise.all([
el.style.backgroundImage = 'url(' + dataUrl + ')'; loadFonts()/* .then(() => new Promise((resolve) => window.requestAnimationFrame(resolve))) */,
import('../lib/appManagers/appDialogsManager')
]).then(() => {
setTimeout(() => {
document.getElementById('auth-pages').remove();
}, 1e3);
}); });
}, 'image/jpeg', 1);
};
}); */
//});
}; };
const page = new Page('page-chats', false, onFirstMount); const page = new Page('page-chats', false, onFirstMount);

41
src/scss/partials/_chat.scss

@ -669,10 +669,26 @@ $background-transition-total-time: #{$input-transition-time - $background-transi
} }
&-item { &-item {
&.is-image {
background-image: url('assets/img/bg.jpeg'); background-image: url('assets/img/bg.jpeg');
background-size: cover;
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;
}
} }
} }

6
src/scss/partials/_chatlist.scss

@ -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 {

16
src/scss/partials/_leftSidebar.scss

@ -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 {

1
src/scss/partials/_scrollable.scss

@ -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…
Cancel
Save