PWA: share, push fixes, install button

Convert webp image to jpeg
Fix playing animations with open sidebar
New attach design
Fix breaking attach popup with invalid video
Fix managing folders
This commit is contained in:
Eduard Kuzmenko 2023-01-25 18:21:38 +04:00
parent 330ccf5e0e
commit 7c988c4135
64 changed files with 1765 additions and 620 deletions

6
.env
View File

@ -1,5 +1,5 @@
API_ID=1025907 API_ID=1025907
API_HASH=452b0359b988148995f22ff0f4229750 API_HASH=452b0359b988148995f22ff0f4229750
VERSION=1.7.0 VERSION=1.7.1
VERSION_FULL=1.7.0 (289) VERSION_FULL=1.7.1 (291)
BUILD=289 BUILD=291

View File

@ -0,0 +1,45 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M3278 6995 c-1 -1 -41 -5 -88 -9 -87 -7 -105 -9 -225 -27 -116 -18
-123 -19 -192 -35 -184 -42 -248 -59 -369 -99 -459 -153 -870 -390 -1229 -709
-358 -318 -674 -746 -857 -1159 -27 -62 -53 -121 -58 -132 -46 -95 -150 -427
-176 -560 -2 -11 -10 -54 -19 -95 -29 -147 -36 -200 -52 -370 -15 -165 -8
-592 12 -711 2 -13 6 -46 9 -74 9 -78 23 -156 51 -285 23 -107 88 -333 114
-395 5 -11 25 -65 46 -120 50 -132 181 -398 250 -508 30 -49 55 -90 55 -93 0
-3 4 -10 10 -17 5 -7 46 -64 91 -126 72 -103 179 -239 234 -297 243 -261 429
-421 682 -590 77 -52 145 -94 149 -94 4 0 15 -6 23 -14 29 -25 304 -161 431
-213 269 -110 577 -194 835 -228 22 -2 56 -7 75 -10 186 -26 698 -27 838 0 16
3 104 15 147 20 324 41 870 235 1167 415 37 22 70 40 73 40 3 0 11 5 18 10 7
6 62 43 122 83 253 168 492 376 678 592 33 39 63 72 67 75 17 13 181 236 245
335 221 340 406 777 481 1140 16 79 54 301 59 350 4 33 9 71 11 85 9 46 11
524 4 605 -12 128 -21 202 -25 227 -3 13 -8 43 -11 68 -9 67 -70 335 -94 412
-11 37 -21 70 -21 73 0 2 -16 49 -35 102 -222 616 -576 1124 -1074 1541 -66
56 -304 230 -351 257 -235 137 -500 275 -527 275 -4 0 -26 8 -48 19 -98 47
-408 141 -579 176 -44 9 -89 19 -100 21 -11 2 -38 6 -60 9 -22 3 -51 7 -65 10
-13 2 -52 7 -86 11 -33 3 -74 8 -90 10 -32 5 -540 14 -546 9z m1831 -2053 c39
-25 59 -53 72 -96 12 -44 14 -156 5 -226 -10 -74 -15 -112 -21 -170 -4 -30 -8
-66 -10 -80 -2 -14 -7 -45 -10 -70 -6 -54 -8 -66 -44 -306 -11 -72 -23 -156
-26 -185 -4 -30 -10 -72 -15 -94 -5 -22 -12 -62 -15 -90 -4 -27 -15 -104 -26
-170 -11 -66 -22 -131 -24 -145 -2 -14 -16 -95 -30 -180 -14 -85 -28 -167 -30
-182 -3 -16 -7 -43 -10 -60 -3 -18 -21 -121 -40 -228 -19 -107 -37 -211 -40
-230 -9 -57 -17 -104 -22 -125 -2 -11 -15 -81 -29 -155 -31 -169 -39 -201 -72
-275 -53 -123 -118 -176 -221 -179 -106 -4 -202 35 -371 152 -97 67 -359 244
-785 530 -132 89 -265 184 -296 211 -90 80 -112 153 -73 237 24 52 264 291
625 624 180 166 509 487 582 568 84 93 16 177 -87 107 -20 -14 -39 -25 -41
-25 -2 0 -23 -13 -47 -28 -24 -16 -72 -47 -108 -70 -36 -22 -70 -45 -75 -49
-6 -4 -253 -170 -550 -368 -297 -197 -542 -362 -545 -365 -3 -3 -50 -35 -106
-72 -218 -143 -331 -179 -499 -156 -74 10 -284 62 -400 98 -407 129 -466 158
-480 238 -15 87 53 137 370 275 664 288 651 282 720 312 17 8 47 21 67 29 885
382 1426 611 1743 738 110 43 214 85 230 93 151 66 471 174 550 185 70 10 153
2 184 -18z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

View File

@ -50,6 +50,25 @@
"type": "image/png" "type": "image/png"
} }
], ],
"screenshots" : [{
"src": "assets/img/screenshot.jpg",
"sizes": "1280x910",
"type": "image/jpeg"
}],
"share_target": {
"action": "./share/",
"method": "POST",
"enctype": "multipart/form-data",
"params": {
"title": "title",
"text": "text",
"url": "url",
"files": [{
"name": "files",
"accept": "*/*"
}]
}
},
"theme_color": "#ffffff", "theme_color": "#ffffff",
"background_color": "#ffffff", "background_color": "#ffffff",
"display": "standalone", "display": "standalone",

View File

@ -20,6 +20,25 @@
"type": "image/png" "type": "image/png"
} }
], ],
"screenshots" : [{
"src": "assets/img/screenshot.jpg",
"sizes": "1280x910",
"type": "image/jpeg"
}],
"share_target": {
"action": "./share/",
"method": "POST",
"enctype": "multipart/form-data",
"params": {
"title": "title",
"text": "text",
"url": "url",
"files": [{
"name": "files",
"accept": "*/*"
}]
}
},
"theme_color": "#ffffff", "theme_color": "#ffffff",
"background_color": "#ffffff", "background_color": "#ffffff",
"display": "standalone", "display": "standalone",

View File

@ -1 +1 @@
1.7.0 (289) 1.7.1 (291)

View File

@ -15,6 +15,7 @@ import forEachReverse from '../helpers/array/forEachReverse';
import idleController from '../helpers/idleController'; import idleController from '../helpers/idleController';
import appMediaPlaybackController from './appMediaPlaybackController'; import appMediaPlaybackController from './appMediaPlaybackController';
import {fastRaf} from '../helpers/schedulers'; import {fastRaf} from '../helpers/schedulers';
import {Middleware} from '../helpers/middleware';
export type AnimationItemGroup = '' | 'none' | 'chat' | 'lock' | export type AnimationItemGroup = '' | 'none' | 'chat' | 'lock' |
'STICKERS-POPUP' | 'emoticons-dropdown' | 'STICKERS-SEARCH' | 'GIFS-SEARCH' | 'STICKERS-POPUP' | 'emoticons-dropdown' | 'STICKERS-SEARCH' | 'GIFS-SEARCH' |
@ -24,7 +25,7 @@ export interface AnimationItem {
el: HTMLElement, el: HTMLElement,
group: AnimationItemGroup, group: AnimationItemGroup,
animation: AnimationItemWrapper, animation: AnimationItemWrapper,
controlled?: boolean controlled?: boolean | Middleware
}; };
export interface AnimationItemWrapper { export interface AnimationItemWrapper {
@ -179,7 +180,7 @@ export class AnimationIntersector {
animation: AnimationItem['animation'], animation: AnimationItem['animation'],
group: AnimationItemGroup = '', group: AnimationItemGroup = '',
observeElement?: HTMLElement, observeElement?: HTMLElement,
controlled?: boolean controlled?: AnimationItem['controlled']
) { ) {
if(group === 'none' || this.byPlayer.has(animation)) { if(group === 'none' || this.byPlayer.has(animation)) {
return; return;
@ -204,6 +205,12 @@ export class AnimationIntersector {
controlled controlled
}; };
if(controlled && typeof(controlled) !== 'boolean') {
controlled.onClean(() => {
this.removeAnimationByPlayer(animation);
});
}
if(animation instanceof RLottiePlayer) { if(animation instanceof RLottiePlayer) {
if(!rootScope.settings.stickers.loop && animation.loop) { if(!rootScope.settings.stickers.loop && animation.loop) {
animation.loop = rootScope.settings.stickers.loop; animation.loop = rootScope.settings.stickers.loop;

View File

@ -132,6 +132,7 @@ import confirmationPopup from '../confirmationPopup';
import wrapPeerTitle from '../wrappers/peerTitle'; import wrapPeerTitle from '../wrappers/peerTitle';
import {PopupPeerCheckboxOptions} from '../popups/peer'; import {PopupPeerCheckboxOptions} from '../popups/peer';
import toggleDisability from '../../helpers/dom/toggleDisability'; import toggleDisability from '../../helpers/dom/toggleDisability';
import {copyTextToClipboard} from '../../helpers/clipboard';
export const USER_REACTIONS_INLINE = false; export const USER_REACTIONS_INLINE = false;
const USE_MEDIA_TAILS = false; const USE_MEDIA_TAILS = false;
@ -1671,9 +1672,18 @@ export default class ChatBubbles {
const contactDiv: HTMLElement = findUpClassName(target, 'contact'); const contactDiv: HTMLElement = findUpClassName(target, 'contact');
if(contactDiv) { if(contactDiv) {
this.chat.appImManager.setInnerPeer({ const peerId = contactDiv.dataset.peerId.toPeerId();
peerId: contactDiv.dataset.peerId.toPeerId() if(peerId) {
}); this.chat.appImManager.setInnerPeer({
peerId
});
} else {
const phone = contactDiv.querySelector<HTMLElement>('.contact-number');
copyTextToClipboard(phone.innerText.replace(/\s/g, ''));
toastNew({langPackKey: 'PhoneCopied'});
cancelEvent(e);
}
return; return;
} }
@ -3940,11 +3950,9 @@ export default class ChatBubbles {
} }
return new Promise<PeerId>((resolve, reject) => { return new Promise<PeerId>((resolve, reject) => {
const popup = new PopupForward({ const popup = new PopupForward(undefined, (peerId) => {
[this.peerId]: []
}, (peerId) => {
resolve(peerId); resolve(peerId);
}, true); });
popup.addEventListener('close', () => { popup.addEventListener('close', () => {
reject(); reject();
@ -4568,11 +4576,12 @@ export default class ChatBubbles {
contactDetails.className = 'contact-details'; contactDetails.className = 'contact-details';
const contactNameDiv = document.createElement('div'); const contactNameDiv = document.createElement('div');
contactNameDiv.className = 'contact-name'; contactNameDiv.className = 'contact-name';
const fullName = [
contact.first_name,
contact.last_name
].filter(Boolean).join(' ');
contactNameDiv.append( contactNameDiv.append(
wrapEmojiText([ fullName.trim() ? wrapEmojiText(fullName) : i18n('AttachContact')
contact.first_name,
contact.last_name
].filter(Boolean).join(' '))
); );
const contactNumberDiv = document.createElement('div'); const contactNumberDiv = document.createElement('div');
@ -4585,7 +4594,8 @@ export default class ChatBubbles {
const avatarElem = new AvatarElement(); const avatarElem = new AvatarElement();
avatarElem.updateWithOptions({ avatarElem.updateWithOptions({
lazyLoadQueue: this.lazyLoadQueue, lazyLoadQueue: this.lazyLoadQueue,
peerId: contact.user_id.toPeerId() peerId: contact.user_id.toPeerId(),
peerTitle: fullName.trim() ? undefined : I18n.format('AttachContact', true)[0]
}); });
avatarElem.classList.add('contact-avatar', 'avatar-54'); avatarElem.classList.add('contact-avatar', 'avatar-54');

View File

@ -20,7 +20,7 @@ import {NULL_PEER_ID, REPLIES_PEER_ID} from '../../lib/mtproto/mtproto_config';
import SetTransition from '../singleTransition'; import SetTransition from '../singleTransition';
import AppPrivateSearchTab from '../sidebarRight/tabs/search'; import AppPrivateSearchTab from '../sidebarRight/tabs/search';
import renderImageFromUrl from '../../helpers/dom/renderImageFromUrl'; import renderImageFromUrl from '../../helpers/dom/renderImageFromUrl';
import mediaSizes from '../../helpers/mediaSizes'; import mediaSizes, {ScreenSize} 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';
@ -34,8 +34,9 @@ import AppSharedMediaTab from '../sidebarRight/tabs/sharedMedia';
import noop from '../../helpers/noop'; import noop from '../../helpers/noop';
import middlewarePromise from '../../helpers/middlewarePromise'; import middlewarePromise from '../../helpers/middlewarePromise';
import indexOfAndSplice from '../../helpers/array/indexOfAndSplice'; import indexOfAndSplice from '../../helpers/array/indexOfAndSplice';
import {Message} from '../../layer'; import {Message, WallPaper} from '../../layer';
import animationIntersector, {AnimationItemGroup} from '../animationIntersector'; import animationIntersector, {AnimationItemGroup} from '../animationIntersector';
import {getColorsFromWallPaper} from '../sidebarLeft/tabs/background';
export type ChatType = 'chat' | 'pinned' | 'discussion' | 'scheduled'; export type ChatType = 'chat' | 'pinned' | 'discussion' | 'scheduled';
@ -122,16 +123,19 @@ export default class Chat extends EventListenerBase<{
public setBackground(url: string, skipAnimation?: boolean): Promise<void> { public setBackground(url: string, skipAnimation?: boolean): Promise<void> {
const theme = themeController.getTheme(); const theme = themeController.getTheme();
const themeSettings = theme.settings;
const wallPaper = themeSettings.wallpaper;
const colors = getColorsFromWallPaper(wallPaper);
let item: HTMLElement; let item: HTMLElement;
const isColorBackground = !!theme.background.color && !theme.background.slug && !theme.background.intensity; const isColorBackground = !!colors && !(wallPaper as WallPaper.wallPaper).slug && !wallPaper.settings.intensity;
if( if(
isColorBackground && isColorBackground &&
document.documentElement.style.cursor === 'grabbing' && document.documentElement.style.cursor === 'grabbing' &&
this.gradientRenderer && this.gradientRenderer &&
!this.patternRenderer !this.patternRenderer
) { ) {
this.gradientCanvas.dataset.colors = theme.background.color; this.gradientCanvas.dataset.colors = colors;
this.gradientRenderer.init(this.gradientCanvas); this.gradientRenderer.init(this.gradientCanvas);
return Promise.resolve(); return Promise.resolve();
} }
@ -150,7 +154,7 @@ export default class Chat extends EventListenerBase<{
// this.renderDarkPattern = // this.renderDarkPattern =
undefined; undefined;
const intensity = theme.background.intensity && theme.background.intensity / 100; const intensity = wallPaper.settings?.intensity && wallPaper.settings.intensity / 100;
const isDarkPattern = !!intensity && intensity < 0; const isDarkPattern = !!intensity && intensity < 0;
let patternRenderer: ChatBackgroundPatternRenderer; let patternRenderer: ChatBackgroundPatternRenderer;
@ -190,19 +194,18 @@ export default class Chat extends EventListenerBase<{
// }); // });
// }; // };
// } // }
} else if(theme.background.slug) { } else {
item.classList.add('is-image'); item.classList.add('is-image');
} }
} else if(theme.background.color) { } else {
item.classList.add('is-color'); item.classList.add('is-color');
} }
} }
let gradientRenderer: ChatBackgroundGradientRenderer; let gradientRenderer: ChatBackgroundGradientRenderer;
const color = theme.background.color; if(colors) {
if(color) {
// if(color.includes(',')) { // if(color.includes(',')) {
const {canvas, gradientRenderer: _gradientRenderer} = ChatBackgroundGradientRenderer.create(color); const {canvas, gradientRenderer: _gradientRenderer} = ChatBackgroundGradientRenderer.create(colors);
gradientRenderer = this.gradientRenderer = _gradientRenderer; gradientRenderer = this.gradientRenderer = _gradientRenderer;
gradientCanvas = this.gradientCanvas = canvas; gradientCanvas = this.gradientCanvas = canvas;
gradientCanvas.classList.add('chat-background-item-canvas', 'chat-background-item-color-canvas'); gradientCanvas.classList.add('chat-background-item-canvas', 'chat-background-item-color-canvas');
@ -365,7 +368,7 @@ export default class Chat extends EventListenerBase<{
}); });
this.bubbles.listenerSetter.add(this.appImManager)('tab_changing', (tabId) => { this.bubbles.listenerSetter.add(this.appImManager)('tab_changing', (tabId) => {
freezeObservers(this.appImManager.chat !== this || tabId !== APP_TABS.CHAT); freezeObservers(this.appImManager.chat !== this || (tabId !== APP_TABS.CHAT && mediaSizes.activeScreen === ScreenSize.mobile));
}); });
} }

View File

@ -1608,7 +1608,6 @@ export default class ChatInput {
this.messageInputField.input.classList.replace('input-field-input', 'input-message-input'); this.messageInputField.input.classList.replace('input-field-input', 'input-message-input');
this.messageInputField.inputFake.classList.replace('input-field-input', 'input-message-input'); this.messageInputField.inputFake.classList.replace('input-field-input', 'input-message-input');
this.messageInput = this.messageInputField.input; this.messageInput = this.messageInputField.input;
this.messageInput.classList.add('no-scrollbar');
this.attachMessageInputListeners(); this.attachMessageInputListeners();
if(IS_STICKY_INPUT_BUGGED) { if(IS_STICKY_INPUT_BUGGED) {

View File

@ -165,14 +165,10 @@ export default class DotRenderer implements AnimationItemWrapper {
animationGroup: AnimationItemGroup, animationGroup: AnimationItemGroup,
multiply?: number multiply?: number
}) { }) {
middleware.onClean(() => {
animationIntersector.removeAnimationByPlayer(dotRenderer);
});
const dotRenderer = new DotRenderer(width, height, multiply); const dotRenderer = new DotRenderer(width, height, multiply);
dotRenderer.renderFirstFrame(); dotRenderer.renderFirstFrame();
animationIntersector.addAnimation(dotRenderer, animationGroup, dotRenderer.canvas, true); animationIntersector.addAnimation(dotRenderer, animationGroup, dotRenderer.canvas, middleware);
return dotRenderer; return dotRenderer;
} }

View File

@ -14,6 +14,7 @@ const USELESS_REG_EXP = new RegExp(`(<span>${BOM}</span>)|(<br\/?>)`, 'g');
export default class InputFieldAnimated extends InputField { export default class InputFieldAnimated extends InputField {
public inputFake: HTMLElement; public inputFake: HTMLElement;
public onChangeHeight: (height: number) => void;
// public onLengthChange: (length: number, isOverflow: boolean) => void; // public onLengthChange: (length: number, isOverflow: boolean) => void;
// protected wasInputFakeClientHeight: number; // protected wasInputFakeClientHeight: number;
@ -31,7 +32,7 @@ export default class InputFieldAnimated extends InputField {
_i18n(this.inputFake, options.placeholder, undefined, 'placeholder'); _i18n(this.inputFake, options.placeholder, undefined, 'placeholder');
} }
this.input.classList.add('scrollable', 'scrollable-y'); this.input.classList.add('scrollable', 'scrollable-y', 'no-scrollbar');
// this.wasInputFakeClientHeight = 0; // this.wasInputFakeClientHeight = 0;
// this.showScrollDebounced = debounce(() => this.input.classList.remove('no-scrollbar'), 150, false, true); // this.showScrollDebounced = debounce(() => this.input.classList.remove('no-scrollbar'), 150, false, true);
this.inputFake = document.createElement('div'); this.inputFake = document.createElement('div');
@ -62,6 +63,7 @@ export default class InputFieldAnimated extends InputField {
this.input.style.transitionDuration = `${transitionDuration}ms`; this.input.style.transitionDuration = `${transitionDuration}ms`;
if(setHeight) { if(setHeight) {
this.onChangeHeight?.(newHeight);
this.input.style.height = newHeight ? newHeight + 'px' : ''; this.input.style.height = newHeight ? newHeight + 'px' : '';
} }

View File

@ -11,13 +11,12 @@ import PopupPickUser from './pickUser';
export default class PopupForward extends PopupPickUser { export default class PopupForward extends PopupPickUser {
constructor( constructor(
peerIdMids: {[fromPeerId: PeerId]: number[]}, peerIdMids?: {[fromPeerId: PeerId]: number[]},
onSelect?: (peerId: PeerId) => Promise<void> | void, onSelect?: (peerId: PeerId) => Promise<void> | void
overrideOnSelect = false
) { ) {
super({ super({
peerTypes: ['dialogs', 'contacts'], peerTypes: ['dialogs', 'contacts'],
onSelect: overrideOnSelect ? onSelect : async(peerId) => { onSelect: !peerIdMids && onSelect ? onSelect : async(peerId) => {
if(onSelect) { if(onSelect) {
const res = onSelect(peerId); const res = onSelect(peerId);
if(res instanceof Promise) { if(res instanceof Promise) {

View File

@ -40,6 +40,9 @@ import defineNotNumerableProperties from '../../helpers/object/defineNotNumerabl
import {Photo, PhotoSize} from '../../layer'; import {Photo, PhotoSize} from '../../layer';
import {getPreviewBytesFromURL} from '../../helpers/bytes/getPreviewURLFromBytes'; import {getPreviewBytesFromURL} from '../../helpers/bytes/getPreviewURLFromBytes';
import {renderImageFromUrlPromise} from '../../helpers/dom/renderImageFromUrl'; import {renderImageFromUrlPromise} from '../../helpers/dom/renderImageFromUrl';
import ButtonMenuToggle from '../buttonMenuToggle';
import partition from '../../helpers/array/partition';
import InputFieldAnimated from '../inputFieldAnimated';
type SendFileParams = SendFileDetails & { type SendFileParams = SendFileDetails & {
file?: File, file?: File,
@ -53,15 +56,14 @@ type SendFileParams = SendFileDetails & {
let currentPopup: PopupNewMedia; let currentPopup: PopupNewMedia;
const MAX_WIDTH = 400 - 16;
export function getCurrentNewMediaPopup() { export function getCurrentNewMediaPopup() {
return currentPopup; return currentPopup;
} }
export default class PopupNewMedia extends PopupElement { export default class PopupNewMedia extends PopupElement {
private input: HTMLElement;
private mediaContainer: HTMLElement; private mediaContainer: HTMLElement;
private groupCheckboxField: CheckboxField;
private mediaCheckboxField: CheckboxField;
private wasInputValue: string; private wasInputValue: string;
private willAttach: Partial<{ private willAttach: Partial<{
@ -70,22 +72,26 @@ export default class PopupNewMedia extends PopupElement {
group: boolean, group: boolean,
sendFileDetails: SendFileParams[] sendFileDetails: SendFileParams[]
}>; }>;
private inputField: InputField; private messageInputField: InputFieldAnimated;
private captionLengthMax: number; private captionLengthMax: number;
private animationGroup: AnimationItemGroup; private animationGroup: AnimationItemGroup;
private _scrollable: Scrollable;
private inputContainer: HTMLDivElement;
constructor( constructor(
private chat: Chat, private chat: Chat,
private files: File[], private files: File[],
willAttachType: PopupNewMedia['willAttach']['type'] willAttachType: PopupNewMedia['willAttach']['type'],
private ignoreInputValue?: boolean
) { ) {
super('popup-send-photo popup-new-media', { super('popup-send-photo popup-new-media', {
closable: true, closable: true,
withConfirm: 'Modal.Send', withConfirm: 'Modal.Send',
confirmShortcutIsSendShortcut: true, confirmShortcutIsSendShortcut: true,
body: true, body: true,
title: true title: true,
scrollable: true
}); });
this.animationGroup = ''; this.animationGroup = '';
@ -96,7 +102,7 @@ export default class PopupNewMedia extends PopupElement {
this.willAttach = { this.willAttach = {
type: willAttachType, type: willAttachType,
sendFileDetails: [], sendFileDetails: [],
group: false group: true
}; };
const captionMaxLength = await this.managers.apiManager.getLimit('caption'); const captionMaxLength = await this.managers.apiManager.getLimit('caption');
@ -125,25 +131,93 @@ export default class PopupNewMedia extends PopupElement {
this.header.append(sendMenu.sendMenu); this.header.append(sendMenu.sendMenu);
} }
const btnMenu = await ButtonMenuToggle({
listenerSetter: this.listenerSetter,
direction: 'bottom-left',
buttons: [{
icon: 'image',
text: 'Popup.Attach.AsMedia',
onClick: () => this.changeType('media'),
verify: () => this.hasAnyMedia() && this.willAttach.type === 'document'
}, {
icon: 'document',
text: 'SendAsFile',
onClick: () => this.changeType('document'),
verify: () => this.files.length === 1 && this.willAttach.type !== 'document'
}, {
icon: 'document',
text: 'SendAsFiles',
onClick: () => this.changeType('document'),
verify: () => this.files.length > 1 && this.willAttach.type !== 'document'
}, {
icon: 'groupmedia',
text: 'Popup.Attach.GroupMedia',
onClick: () => this.changeGroup(true),
verify: () => !this.willAttach.group && this.canGroupSomething()
}, {
icon: 'groupmediaoff',
text: 'Popup.Attach.UngroupMedia',
onClick: () => this.changeGroup(false),
verify: () => this.willAttach.group && this.canGroupSomething()
}, {
icon: 'mediaspoiler',
text: 'EnablePhotoSpoiler',
onClick: () => this.changeSpoilers(true),
verify: () => this.canToggleSpoilers(true, true)
}, {
icon: 'mediaspoiler',
text: 'Popup.Attach.EnableSpoilers',
onClick: () => this.changeSpoilers(true),
verify: () => this.canToggleSpoilers(true, false)
}, {
icon: 'mediaspoileroff',
text: 'DisablePhotoSpoiler',
onClick: () => this.changeSpoilers(false),
verify: () => this.canToggleSpoilers(false, true)
}, {
icon: 'mediaspoileroff',
text: 'Popup.Attach.RemoveSpoilers',
onClick: () => this.changeSpoilers(false),
verify: () => this.canToggleSpoilers(false, false)
}]
});
this.header.append(btnMenu);
this.btnConfirm.remove();
this.mediaContainer = document.createElement('div'); this.mediaContainer = document.createElement('div');
this.mediaContainer.classList.add('popup-photo'); this.mediaContainer.classList.add('popup-photo');
const scrollable = new Scrollable(null); this.scrollable.container.append(this.mediaContainer);
scrollable.container.append(this.mediaContainer);
this.inputField = new InputField({ const inputContainer = this.inputContainer = document.createElement('div');
inputContainer.classList.add('popup-input-container');
const c = document.createElement('div');
c.classList.add('popup-input-inputs', 'input-message-container');
this.messageInputField = new InputFieldAnimated({
placeholder: 'PreviewSender.CaptionPlaceholder', placeholder: 'PreviewSender.CaptionPlaceholder',
label: 'Caption', name: 'message',
name: 'photo-caption', withLinebreaks: true,
maxLength: this.captionLengthMax, maxLength: this.captionLengthMax
withLinebreaks: true
}); });
this.input = this.inputField.input;
this.inputField.value = this.wasInputValue = this.chat.input.messageInputField.input.innerHTML; this.listenerSetter.add(this.scrollable.container)('scroll', this.onScroll);
this.chat.input.messageInputField.value = ''; this.listenerSetter.add(this.messageInputField.input)('scroll', this.onScroll);
this.body.append(scrollable.container); this.messageInputField.input.classList.replace('input-field-input', 'input-message-input');
this.container.append(this.inputField.container); this.messageInputField.inputFake.classList.replace('input-field-input', 'input-message-input');
c.append(this.messageInputField.input, this.messageInputField.inputFake);
inputContainer.append(c, this.btnConfirm);
if(!this.ignoreInputValue) {
this.messageInputField.value = this.wasInputValue = this.chat.input.messageInputField.input.innerHTML;
this.chat.input.messageInputField.value = '';
}
this.container.append(inputContainer);
this.attachFiles(); this.attachFiles();
@ -169,13 +243,7 @@ export default class PopupNewMedia extends PopupElement {
icon: 'mediaspoileroff', icon: 'mediaspoileroff',
text: 'DisablePhotoSpoiler', text: 'DisablePhotoSpoiler',
onClick: () => { onClick: () => {
toggleMediaSpoiler({ this.removeMediaSpoiler(item);
mediaSpoiler: item.mediaSpoiler,
reveal: true,
destroyAfter: true
});
item.mediaSpoiler = undefined;
}, },
verify: () => !!(isMedia && item.mediaSpoiler) verify: () => !!(isMedia && item.mediaSpoiler)
}], }],
@ -192,6 +260,14 @@ export default class PopupNewMedia extends PopupElement {
currentPopup = this; currentPopup = this;
} }
private onScroll = () => {
const {input} = this.messageInputField;
this.scrollable.onAdditionalScroll();
if(input.scrollTop > 0 && input.scrollHeight > 130) {
this.scrollable.container.classList.remove('scrolled-bottom');
}
};
private async applyMediaSpoiler(item: SendFileParams, noAnimation?: boolean) { private async applyMediaSpoiler(item: SendFileParams, noAnimation?: boolean) {
const middleware = item.middlewareHelper.get(); const middleware = item.middlewareHelper.get();
const {width: widthStr, height: heightStr} = item.itemDiv.style; const {width: widthStr, height: heightStr} = item.itemDiv.style;
@ -268,6 +344,16 @@ export default class PopupNewMedia extends PopupElement {
}); });
} }
private removeMediaSpoiler(item: SendFileParams) {
toggleMediaSpoiler({
mediaSpoiler: item.mediaSpoiler,
reveal: true,
destroyAfter: true
});
item.mediaSpoiler = undefined;
}
public appendDrops(element: HTMLElement) { public appendDrops(element: HTMLElement) {
this.body.append(element); this.body.append(element);
} }
@ -280,59 +366,66 @@ export default class PopupNewMedia extends PopupElement {
this.willAttach.type = type; this.willAttach.type = type;
} }
private appendGroupCheckboxField() { private partition() {
const good = this.files.length > 1; const [media, files] = partition(this.willAttach.sendFileDetails, (d) => MEDIA_MIME_TYPES_SUPPORTED.has(d.file.type));
if(good && !this.groupCheckboxField) { return {
this.groupCheckboxField = new CheckboxField({ media,
text: 'PreviewSender.GroupItems', files
name: 'group-items' };
});
this.container.append(...[
this.groupCheckboxField.label,
this.mediaCheckboxField?.label,
this.inputField.container
].filter(Boolean));
this.willAttach.group = true;
this.groupCheckboxField.setValueSilently(this.willAttach.group);
this.listenerSetter.add(this.groupCheckboxField.input)('change', () => {
const checked = this.groupCheckboxField.checked;
this.willAttach.group = checked;
this.attachFiles();
});
} else if(this.groupCheckboxField) {
this.groupCheckboxField.label.classList.toggle('hide', !good);
}
} }
private appendMediaCheckboxField() { private mediaCount() {
const good = !!this.files.find((file) => MEDIA_MIME_TYPES_SUPPORTED.has(file.type)); return this.partition().media.length;
if(good && !this.mediaCheckboxField) { }
this.mediaCheckboxField = new CheckboxField({
text: 'PreviewSender.CompressFile',
name: 'compress-items'
});
this.container.append(...[
this.groupCheckboxField?.label,
this.mediaCheckboxField.label,
this.inputField.container
].filter(Boolean));
this.mediaCheckboxField.setValueSilently(this.willAttach.type === 'media'); private hasAnyMedia() {
return this.mediaCount() > 0;
}
this.listenerSetter.add(this.mediaCheckboxField.input)('change', () => { private canGroupSomething() {
const checked = this.mediaCheckboxField.checked; const {media, files} = this.partition();
return media.length > 1 || files.length > 1;
}
this.willAttach.type = checked ? 'media' : 'document'; private canToggleSpoilers(toggle: boolean, single: boolean) {
let good = this.willAttach.type === 'media' && this.hasAnyMedia();
this.attachFiles(); if(single && good) {
}); good = this.files.length === 1;
} else if(this.mediaCheckboxField) {
this.mediaCheckboxField.label.classList.toggle('hide', !good);
} }
if(good) {
const media = this.willAttach.sendFileDetails
.filter((d) => MEDIA_MIME_TYPES_SUPPORTED.has(d.file.type))
const mediaWithSpoilers = media.filter((d) => d.mediaSpoiler);
good = single ? true : media.length > 1;
if(good) {
good = toggle ? media.length !== mediaWithSpoilers.length : media.length === mediaWithSpoilers.length;
}
}
return good;
}
private changeType(type: PopupNewMedia['willAttach']['type']) {
this.willAttach.type = type;
this.attachFiles();
}
public changeGroup(group: boolean) {
this.willAttach.group = group;
this.attachFiles();
}
public changeSpoilers(toggle: boolean) {
this.partition().media.forEach((item) => {
if(toggle && !item.mediaSpoiler) {
this.applyMediaSpoiler(item);
} else if(!toggle && item.mediaSpoiler) {
this.removeMediaSpoiler(item);
}
});
} }
public addFiles(files: File[]) { public addFiles(files: File[]) {
@ -352,13 +445,14 @@ export default class PopupNewMedia extends PopupElement {
private onKeyDown = (e: KeyboardEvent) => { private onKeyDown = (e: KeyboardEvent) => {
const target = e.target as HTMLElement; const target = e.target as HTMLElement;
if(target !== this.input) { const {input} = this.messageInputField;
if(target !== input) {
if(target.tagName === 'INPUT' || target.isContentEditable) { if(target.tagName === 'INPUT' || target.isContentEditable) {
return; return;
} }
this.input.focus(); input.focus();
placeCaretAtEnd(this.input); placeCaretAtEnd(input);
} }
}; };
@ -371,7 +465,7 @@ export default class PopupNewMedia extends PopupElement {
return; return;
} }
let caption = this.inputField.value; let caption = this.messageInputField.value;
if(caption.length > this.captionLengthMax) { if(caption.length > this.captionLengthMax) {
toast(I18n.format('Error.PreviewSender.CaptionTooLong', true)); toast(I18n.format('Error.PreviewSender.CaptionTooLong', true));
return; return;
@ -420,20 +514,27 @@ export default class PopupNewMedia extends PopupElement {
input.replyToMsgId = this.chat.threadId; input.replyToMsgId = this.chat.threadId;
input.onMessageSent(); input.onMessageSent();
this.wasInputValue = undefined;
this.hide(); this.hide();
} }
private async scaleImageForTelegram(image: HTMLImageElement, params: SendFileParams) { private modifyMimeTypeForTelegram(mimeType: string) {
return mimeType === 'image/webp' ? 'image/jpeg' : mimeType;
}
private async scaleImageForTelegram(image: HTMLImageElement, mimeType: string, convertWebp?: boolean) {
const PHOTO_SIDE_LIMIT = 2560; const PHOTO_SIDE_LIMIT = 2560;
const mimeType = params.file.type;
let url = image.src, scaledBlob: Blob; let url = image.src, scaledBlob: Blob;
if(mimeType !== 'image/gif' && Math.max(image.naturalWidth, image.naturalHeight) > PHOTO_SIDE_LIMIT) { if(
mimeType !== 'image/gif' &&
(Math.max(image.naturalWidth, image.naturalHeight) > PHOTO_SIDE_LIMIT || (convertWebp && mimeType === 'image/webp'))
) {
const {blob} = await scaleMediaElement({ const {blob} = await scaleMediaElement({
media: image, media: image,
boxSize: makeMediaSize(PHOTO_SIDE_LIMIT, PHOTO_SIDE_LIMIT), boxSize: makeMediaSize(PHOTO_SIDE_LIMIT, PHOTO_SIDE_LIMIT),
mediaSize: makeMediaSize(image.naturalWidth, image.naturalHeight), mediaSize: makeMediaSize(image.naturalWidth, image.naturalHeight),
mimeType: mimeType as any mimeType: this.modifyMimeTypeForTelegram(mimeType) as any
}); });
scaledBlob = blob; scaledBlob = blob;
@ -442,8 +543,7 @@ export default class PopupNewMedia extends PopupElement {
await renderImageFromUrlPromise(image, url); await renderImageFromUrlPromise(image, url);
} }
params.objectURL = url; return scaledBlob && {url, blob: scaledBlob};
params.scaledBlob = scaledBlob;
} }
private async attachMedia(params: SendFileParams) { private async attachMedia(params: SendFileParams) {
@ -453,11 +553,9 @@ export default class PopupNewMedia extends PopupElement {
const file = params.file; const file = params.file;
const isVideo = file.type.startsWith('video/'); const isVideo = file.type.startsWith('video/');
let promise: Promise<void>;
if(isVideo) { if(isVideo) {
const video = createVideo(); const video = createVideo();
const source = document.createElement('source'); video.src = params.objectURL = await apiManagerProxy.invoke('createObjectURL', file);
source.src = params.objectURL = await apiManagerProxy.invoke('createObjectURL', file);
video.autoplay = true; video.autoplay = true;
video.controls = false; video.controls = false;
video.muted = true; video.muted = true;
@ -466,36 +564,49 @@ export default class PopupNewMedia extends PopupElement {
video.pause(); video.pause();
}, {once: true}); }, {once: true});
promise = onMediaLoad(video).then(async() => { itemDiv.append(video);
params.width = video.videoWidth;
params.height = video.videoHeight;
params.duration = Math.floor(video.duration);
const audioDecodedByteCount = (video as any).webkitAudioDecodedByteCount; let error: Error;
if(audioDecodedByteCount !== undefined) { try {
params.noSound = !audioDecodedByteCount; await onMediaLoad(video);
} } catch(err) {
error = err as any;
}
itemDiv.append(video); params.width = video.videoWidth;
const thumb = await createPosterFromVideo(video); params.height = video.videoHeight;
params.thumb = { params.duration = Math.floor(video.duration);
url: await apiManagerProxy.invoke('createObjectURL', thumb.blob),
...thumb
};
});
video.append(source); if(error) {
throw error;
}
const audioDecodedByteCount = (video as any).webkitAudioDecodedByteCount;
if(audioDecodedByteCount !== undefined) {
params.noSound = !audioDecodedByteCount;
}
const thumb = await createPosterFromVideo(video);
params.thumb = {
url: await apiManagerProxy.invoke('createObjectURL', thumb.blob),
...thumb
};
} else { } else {
const img = new Image(); const img = new Image();
itemDiv.append(img);
const url = await apiManagerProxy.invoke('createObjectURL', file); const url = await apiManagerProxy.invoke('createObjectURL', file);
await renderImageFromUrlPromise(img, url);
await this.scaleImageForTelegram(img, params); await renderImageFromUrlPromise(img, url);
const mimeType = params.file.type;
const scaled = await this.scaleImageForTelegram(img, mimeType, true);
if(scaled) {
params.objectURL = scaled.url;
params.scaledBlob = scaled.blob;
}
params.width = img.naturalWidth; params.width = img.naturalWidth;
params.height = img.naturalHeight; params.height = img.naturalHeight;
itemDiv.append(img);
if(file.type === 'image/gif') { if(file.type === 'image/gif') {
params.noSound = true; params.noSound = true;
@ -510,11 +621,9 @@ export default class PopupNewMedia extends PopupElement {
...thumb ...thumb
}; };
}) })
]); ]).then(() => {});
} }
} }
return promise;
} }
private async attachDocument(params: SendFileParams): ReturnType<PopupNewMedia['attachMedia']> { private async attachDocument(params: SendFileParams): ReturnType<PopupNewMedia['attachMedia']> {
@ -532,8 +641,10 @@ export default class PopupNewMedia extends PopupElement {
if(isPhoto) { if(isPhoto) {
img = new Image(); img = new Image();
await renderImageFromUrlPromise(img, params.objectURL); await renderImageFromUrlPromise(img, params.objectURL);
await this.scaleImageForTelegram(img, params); const scaled = await this.scaleImageForTelegram(img, params.file.type);
params.scaledBlob = undefined; if(scaled) {
params.objectURL = scaled.url;
}
} }
const doc = { const doc = {
@ -596,7 +707,10 @@ export default class PopupNewMedia extends PopupElement {
const promise = shouldCompress ? this.attachMedia(params) : this.attachDocument(params); const promise = shouldCompress ? this.attachMedia(params) : this.attachDocument(params);
willAttach.sendFileDetails.push(params); willAttach.sendFileDetails.push(params);
return promise; return promise.catch((err) => {
itemDiv.style.backgroundColor = '#000';
console.error('error rendering file', err);
});
}; };
private shouldCompress(mimeType: string) { private shouldCompress(mimeType: string) {
@ -607,7 +721,7 @@ export default class PopupNewMedia extends PopupElement {
// show now // show now
if(!this.element.classList.contains('active')) { if(!this.element.classList.contains('active')) {
this.listenerSetter.add(document.body)('keydown', this.onKeyDown); this.listenerSetter.add(document.body)('keydown', this.onKeyDown);
this.addEventListener('close', () => { !this.ignoreInputValue && this.addEventListener('close', () => {
if(this.wasInputValue) { if(this.wasInputValue) {
this.chat.input.messageInputField.value = this.wasInputValue; this.chat.input.messageInputField.value = this.wasInputValue;
} }
@ -655,7 +769,7 @@ export default class PopupNewMedia extends PopupElement {
private appendMediaToContainer(params: SendFileParams) { private appendMediaToContainer(params: SendFileParams) {
if(this.shouldCompress(params.file.type)) { if(this.shouldCompress(params.file.type)) {
const size = calcImageInBox(params.width, params.height, 380, 320); const size = calcImageInBox(params.width, params.height, MAX_WIDTH, 320);
params.itemDiv.style.width = size.width + 'px'; params.itemDiv.style.width = size.width + 'px';
params.itemDiv.style.height = size.height + 'px'; params.itemDiv.style.height = size.height + 'px';
} }
@ -693,9 +807,6 @@ export default class PopupNewMedia extends PopupElement {
params.middlewareHelper.destroy(); params.middlewareHelper.destroy();
}); });
this.appendGroupCheckboxField();
this.appendMediaCheckboxField();
const promises = files.map((file) => this.attachFile(file)); const promises = files.map((file) => this.attachFile(file));
Promise.all(promises).then(() => { Promise.all(promises).then(() => {
@ -717,7 +828,7 @@ export default class PopupNewMedia extends PopupElement {
prepareAlbum({ prepareAlbum({
container: albumContainer, container: albumContainer,
items: sendFileDetails.map((o) => ({w: o.width, h: o.height})), items: sendFileDetails.map((o) => ({w: o.width, h: o.height})),
maxWidth: 380, maxWidth: MAX_WIDTH,
minWidth: 100, minWidth: 100,
spacing: 4 spacing: 4
}); });
@ -746,6 +857,7 @@ export default class PopupNewMedia extends PopupElement {
}); });
}).then(() => { }).then(() => {
this.onRender(); this.onRender();
this.onScroll();
}); });
} }
} }

View File

@ -47,7 +47,7 @@ export default class PopupReportMessagesConfirm extends PopupPeer {
this.show(); this.show();
}); });
this.header.append(div); this.header.replaceWith(div);
const inputField = new InputField({ const inputField = new InputField({
label: 'ReportHint', label: 'ReportHint',

View File

@ -153,8 +153,8 @@ export class ScrollableBase {
this.onScrolledBottom = undefined; this.onScrolledBottom = undefined;
} }
public append(element: HTMLElement) { public append(...args: Parameters<HTMLElement['append']>) {
this.container.append(element); this.container.append(...args);
} }
public scrollIntoViewNew(options: Omit<ScrollOptions, 'container'>) { public scrollIntoViewNew(options: Omit<ScrollOptions, 'container'>) {

View File

@ -54,6 +54,7 @@ import SettingSection, {SettingSectionOptions} from '../settingSection';
import {FOLDER_ID_ARCHIVE} from '../../lib/mtproto/mtproto_config'; import {FOLDER_ID_ARCHIVE} from '../../lib/mtproto/mtproto_config';
import mediaSizes from '../../helpers/mediaSizes'; import mediaSizes from '../../helpers/mediaSizes';
import {fastRaf} from '../../helpers/schedulers'; import {fastRaf} from '../../helpers/schedulers';
import {getInstallPrompt} from '../../helpers/dom/installPrompt';
export const LEFT_COLUMN_ACTIVE_CLASSNAME = 'is-left-column-shown'; export const LEFT_COLUMN_ACTIVE_CLASSNAME = 'is-left-column-shown';
@ -218,6 +219,14 @@ export class AppSidebarLeft extends SidebarSlider {
}); });
}, },
verify: () => App.isMainDomain verify: () => App.isMainDomain
}, {
icon: 'download',
text: 'PWA.Install',
onClick: () => {
const installPrompt = getInstallPrompt();
installPrompt?.();
},
verify: () => !!getInstallPrompt()
}]; }];
const filteredButtons = menuButtons.filter(Boolean); const filteredButtons = menuButtons.filter(Boolean);

View File

@ -24,7 +24,7 @@ import ProgressivePreloader from '../../preloader';
import {SliderSuperTab} from '../../slider'; import {SliderSuperTab} from '../../slider';
import AppBackgroundColorTab from './backgroundColor'; import AppBackgroundColorTab from './backgroundColor';
import choosePhotoSize from '../../../lib/appManagers/utils/photos/choosePhotoSize'; import choosePhotoSize from '../../../lib/appManagers/utils/photos/choosePhotoSize';
import {STATE_INIT, Theme} from '../../../config/state'; import {STATE_INIT, AppTheme} from '../../../config/state';
import themeController from '../../../helpers/themeController'; import themeController from '../../../helpers/themeController';
import requestFile from '../../../helpers/files/requestFile'; import requestFile from '../../../helpers/files/requestFile';
import {renderImageFromUrlPromise} from '../../../helpers/dom/renderImageFromUrl'; import {renderImageFromUrlPromise} from '../../../helpers/dom/renderImageFromUrl';
@ -33,10 +33,29 @@ import {MediaSize} from '../../../helpers/mediaSize';
import wrapPhoto from '../../wrappers/photo'; import wrapPhoto from '../../wrappers/photo';
import {CreateRowFromCheckboxField} from '../../row'; import {CreateRowFromCheckboxField} from '../../row';
import {generateSection} from '../../settingSection'; import {generateSection} from '../../settingSection';
import {hexToRgb} from '../../../helpers/color';
export function getHexColorFromTelegramColor(color: number) {
const hex = (color < 0 ? 0xFFFFFF + color : color).toString(16);
return '#' + (hex.length >= 6 ? hex : '0'.repeat(6 - hex.length) + hex);
}
export function getRgbColorFromTelegramColor(color: number) {
return hexToRgb(getHexColorFromTelegramColor(color));
}
export function getColorsFromWallPaper(wallPaper: WallPaper) {
return wallPaper.settings ? [
wallPaper.settings.background_color,
wallPaper.settings.second_background_color,
wallPaper.settings.third_background_color,
wallPaper.settings.fourth_background_color
].filter(Boolean).map(getHexColorFromTelegramColor).join(',') : '';
}
export default class AppBackgroundTab extends SliderSuperTab { export default class AppBackgroundTab extends SliderSuperTab {
public static tempId = 0;
private grid: HTMLElement; private grid: HTMLElement;
private tempId = 0;
private clicked: Set<DocId> = new Set(); private clicked: Set<DocId> = new Set();
private blurCheckboxField: CheckboxField; private blurCheckboxField: CheckboxField;
@ -72,14 +91,15 @@ export default class AppBackgroundTab extends SliderSuperTab {
attachClickEvent(resetButton, this.onResetClick, {listenerSetter: this.listenerSetter}); attachClickEvent(resetButton, this.onResetClick, {listenerSetter: this.listenerSetter});
const wallPaper = this.theme.settings?.wallpaper;
const blurCheckboxField = this.blurCheckboxField = new CheckboxField({ const blurCheckboxField = this.blurCheckboxField = new CheckboxField({
text: 'ChatBackground.Blur', text: 'ChatBackground.Blur',
name: 'blur', name: 'blur',
checked: this.theme.background.blur checked: (wallPaper as WallPaper.wallPaper)?.settings?.pFlags?.blur
}); });
this.listenerSetter.add(blurCheckboxField.input)('change', async() => { this.listenerSetter.add(blurCheckboxField.input)('change', async() => {
this.theme.background.blur = blurCheckboxField.input.checked; this.theme.settings.wallpaper.settings.pFlags.blur = blurCheckboxField.input.checked || undefined;
await this.managers.appStateManager.pushToState('settings', rootScope.settings); await this.managers.appStateManager.pushToState('settings', rootScope.settings);
// * wait for animation end // * wait for animation end
@ -92,7 +112,7 @@ export default class AppBackgroundTab extends SliderSuperTab {
return; return;
} }
this.setBackgroundDocument(wallpaper); AppBackgroundTab.setBackgroundDocument(wallpaper);
}, 100); }, 100);
}); });
@ -149,7 +169,7 @@ export default class AppBackgroundTab extends SliderSuperTab {
const newKey = this.getWallPaperKey(wallPaper); const newKey = this.getWallPaperKey(wallPaper);
this.elementsByKey.set(newKey, container); this.elementsByKey.set(newKey, container);
this.setBackgroundDocument(wallPaper).then(deferred.resolve, deferred.reject); AppBackgroundTab.setBackgroundDocument(wallPaper).then(deferred.resolve, deferred.reject);
}, deferred.reject); }, deferred.reject);
const key = this.getWallPaperKey(wallPaper); const key = this.getWallPaperKey(wallPaper);
@ -163,7 +183,7 @@ export default class AppBackgroundTab extends SliderSuperTab {
tryAgainOnFail: false tryAgainOnFail: false
}); });
const container = await this.addWallPaper(wallPaper, false); const {container} = await this.addWallPaper(wallPaper, false);
this.clicked.add(key); this.clicked.add(key);
preloader.attach(container, false, deferred); preloader.attach(container, false, deferred);
@ -173,33 +193,27 @@ export default class AppBackgroundTab extends SliderSuperTab {
private onResetClick = () => { private onResetClick = () => {
const defaultTheme = STATE_INIT.settings.themes.find((t) => t.name === this.theme.name); const defaultTheme = STATE_INIT.settings.themes.find((t) => t.name === this.theme.name);
if(defaultTheme) { if(defaultTheme) {
++this.tempId; ++AppBackgroundTab.tempId;
this.theme.background = copy(defaultTheme.background); this.theme.settings = copy(defaultTheme.settings);
this.managers.appStateManager.pushToState('settings', rootScope.settings); this.managers.appStateManager.pushToState('settings', rootScope.settings);
appImManager.applyCurrentTheme(undefined, undefined, true); appImManager.applyCurrentTheme(undefined, undefined, true);
this.blurCheckboxField.setValueSilently(this.theme.background.blur); this.blurCheckboxField.setValueSilently(this.theme.settings?.wallpaper?.settings?.pFlags?.blur);
} }
}; };
private getColorsFromWallPaper(wallPaper: WallPaper) {
return wallPaper.settings ? [
wallPaper.settings.background_color,
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) { private getWallPaperKey(wallPaper: WallPaper) {
return '' + wallPaper.id; return '' + wallPaper.id;
} }
private getWallPaperKeyFromTheme(theme: Theme) { private getWallPaperKeyFromTheme(theme: AppTheme) {
return '' + theme.background.id; return '' + (this.getWallPaperKey(theme.settings?.wallpaper) || '');
} }
private addWallPaper(wallPaper: WallPaper, append = true) { public static addWallPaper(
const colors = this.getColorsFromWallPaper(wallPaper); wallPaper: WallPaper,
container = document.createElement('div')
) {
const colors = getColorsFromWallPaper(wallPaper);
const hasFile = wallPaper._ === 'wallPaper'; const hasFile = wallPaper._ === 'wallPaper';
if((hasFile && wallPaper.pFlags.pattern && !colors)/* || if((hasFile && wallPaper.pFlags.pattern && !colors)/* ||
(wallpaper.document as MyDocument).mime_type.indexOf('application/') === 0 */) { (wallpaper.document as MyDocument).mime_type.indexOf('application/') === 0 */) {
@ -210,17 +224,11 @@ export default class AppBackgroundTab extends SliderSuperTab {
const doc = hasFile ? wallPaper.document as Document.document : undefined; const doc = hasFile ? wallPaper.document as Document.document : undefined;
const container = document.createElement('div'); container.classList.add('background-item');
container.classList.add('grid-item');
container.dataset.id = '' + wallPaper.id; 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('background-item-media');
const loadPromises: Promise<any>[] = []; const loadPromises: Promise<any>[] = [];
let wrapped: ReturnType<typeof wrapPhoto>, size: ReturnType<typeof choosePhotoSize>; let wrapped: ReturnType<typeof wrapPhoto>, size: ReturnType<typeof choosePhotoSize>;
@ -271,7 +279,7 @@ export default class AppBackgroundTab extends SliderSuperTab {
if(isDark && hasFile) { if(isDark && hasFile) {
const promise = wrapped.then(({loadPromises}) => { const promise = wrapped.then(({loadPromises}) => {
return loadPromises.full.then(async() => { return loadPromises.full.then(async() => {
const cacheContext = await this.managers.thumbsStorage.getCacheContext(doc, size.type); const cacheContext = await rootScope.managers.thumbsStorage.getCacheContext(doc, size.type);
canvas.style.webkitMaskImage = `url(${cacheContext.url})`; canvas.style.webkitMaskImage = `url(${cacheContext.url})`;
canvas.style.opacity = '' + (wallPaper.pFlags.dark ? 100 + wallPaper.settings.intensity : wallPaper.settings.intensity) / 100; canvas.style.opacity = '' + (wallPaper.pFlags.dark ? 100 + wallPaper.settings.intensity : wallPaper.settings.intensity) / 100;
media.append(canvas); media.append(canvas);
@ -284,13 +292,32 @@ export default class AppBackgroundTab extends SliderSuperTab {
} }
} }
if(this.getWallPaperKeyFromTheme(this.theme) === key) { return {
container.classList.add('active'); container,
media,
loadPromise: Promise.all(loadPromises)
};
}
private addWallPaper(wallPaper: WallPaper, append = true) {
const result = AppBackgroundTab.addWallPaper(wallPaper);
if(result) {
const {container, media} = result;
container.classList.add('grid-item');
media.classList.add('grid-item-media');
const key = this.getWallPaperKey(wallPaper);
this.wallPapersByElement.set(container, wallPaper);
this.elementsByKey.set(key, container);
if(this.getWallPaperKeyFromTheme(this.theme) === key) {
container.classList.add('active');
}
this.grid[append ? 'append' : 'prepend'](container);
} }
this.grid[append ? 'append' : 'prepend'](container); return result && result.loadPromise.then(() => result);
return Promise.all(loadPromises).then(() => container);
} }
private onGridClick = (e: MouseEvent | TouchEvent) => { private onGridClick = (e: MouseEvent | TouchEvent) => {
@ -299,7 +326,7 @@ export default class AppBackgroundTab extends SliderSuperTab {
const wallpaper = this.wallPapersByElement.get(target); const wallpaper = this.wallPapersByElement.get(target);
if(wallpaper._ === 'wallPaperNoFile') { if(wallpaper._ === 'wallPaperNoFile') {
this.setBackgroundDocument(wallpaper); AppBackgroundTab.setBackgroundDocument(wallpaper);
return; return;
} }
@ -314,9 +341,9 @@ export default class AppBackgroundTab extends SliderSuperTab {
}); });
const load = async() => { const load = async() => {
const promise = this.setBackgroundDocument(wallpaper); const promise = AppBackgroundTab.setBackgroundDocument(wallpaper);
const cacheContext = await this.managers.thumbsStorage.getCacheContext(doc); const cacheContext = await this.managers.thumbsStorage.getCacheContext(doc);
if(!cacheContext.url || this.theme.background.blur) { if(!cacheContext.url || this.theme.settings?.wallpaper?.settings?.pFlags?.blur) {
preloader.attach(target, true, promise); preloader.attach(target, true, promise);
} }
}; };
@ -337,13 +364,7 @@ export default class AppBackgroundTab extends SliderSuperTab {
// console.log(doc); // console.log(doc);
}; };
private saveToCache = (slug: string, url: string) => { public static setBackgroundDocument = (wallPaper: WallPaper) => {
fetch(url).then((response) => {
appImManager.cacheStorage.save('backgrounds/' + slug, response);
});
};
private setBackgroundDocument = (wallPaper: WallPaper) => {
const _tempId = ++this.tempId; const _tempId = ++this.tempId;
const middleware = () => _tempId === this.tempId; const middleware = () => _tempId === this.tempId;
@ -351,24 +372,32 @@ export default class AppBackgroundTab extends SliderSuperTab {
const deferred = deferredPromise<void>(); const deferred = deferredPromise<void>();
let download: Promise<void> | ReturnType<AppDownloadManager['downloadMediaURL']>; let download: Promise<void> | ReturnType<AppDownloadManager['downloadMediaURL']>;
if(doc) { if(doc) {
download = appDownloadManager.downloadMediaURL({media: doc, queueId: appImManager.chat.bubbles ? appImManager.chat.bubbles.lazyLoadQueue.queueId : 0}); download = appDownloadManager.downloadMediaURL({
media: doc,
queueId: 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 { } else {
download = Promise.resolve(); download = Promise.resolve();
} }
const saveToCache = (slug: string, url: string) => {
fetch(url).then((response) => {
appImManager.cacheStorage.save('backgrounds/' + slug, response);
});
};
download.then(async() => { download.then(async() => {
if(!middleware()) { if(!middleware()) {
deferred.resolve(); deferred.resolve();
return; return;
} }
const background = this.theme.background; const themeSettings = themeController.getTheme().settings;
const onReady = (url?: string) => { const onReady = (url?: string) => {
// const perf = performance.now();
let getPixelPromise: Promise<Uint8ClampedArray>; let getPixelPromise: Promise<Uint8ClampedArray>;
const backgroundColor = this.getColorsFromWallPaper(wallPaper); const backgroundColor = getColorsFromWallPaper(wallPaper);
if(url && !backgroundColor) { if(url && !backgroundColor) {
getPixelPromise = averageColor(url); getPixelPromise = averageColor(url);
} else { } else {
@ -383,19 +412,14 @@ export default class AppBackgroundTab extends SliderSuperTab {
} }
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);
const slug = (wallPaper as WallPaper.wallPaper).slug ?? ''; const slug = (wallPaper as WallPaper.wallPaper).slug ?? '';
background.id = wallPaper.id; themeSettings.wallpaper = wallPaper;
background.intensity = wallPaper.settings?.intensity ?? 0; themeSettings.highlightningColor = hsla;
background.color = backgroundColor; rootScope.managers.appStateManager.pushToState('settings', rootScope.settings);
background.slug = slug;
background.highlightningColor = hsla;
this.managers.appStateManager.pushToState('settings', rootScope.settings);
if(slug) { if(slug) {
this.saveToCache(slug, url); saveToCache(slug, url);
} }
appImManager.applyCurrentTheme(slug, url, true).then(deferred.resolve); appImManager.applyCurrentTheme(slug, url, true).then(deferred.resolve);
@ -407,10 +431,10 @@ export default class AppBackgroundTab extends SliderSuperTab {
return; return;
} }
const cacheContext = await this.managers.thumbsStorage.getCacheContext(doc); const cacheContext = await rootScope.managers.thumbsStorage.getCacheContext(doc);
if(background.blur) { if(themeSettings.wallpaper?.settings?.pFlags?.blur) {
setTimeout(() => { setTimeout(() => {
const {canvas, promise} = blur(cacheContext.url, 12, 4) const {canvas, promise} = blur(cacheContext.url, 12, 4);
promise.then(() => { promise.then(() => {
if(!middleware()) { if(!middleware()) {
deferred.resolve(); deferred.resolve();

View File

@ -4,7 +4,7 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
import {Theme} from '../../../config/state'; import {AppTheme} from '../../../config/state';
import {hexaToRgba} from '../../../helpers/color'; import {hexaToRgba} from '../../../helpers/color';
import {attachClickEvent} from '../../../helpers/dom/clickEvent'; import {attachClickEvent} from '../../../helpers/dom/clickEvent';
import findUpClassName from '../../../helpers/dom/findUpClassName'; import findUpClassName from '../../../helpers/dom/findUpClassName';
@ -16,12 +16,13 @@ import rootScope from '../../../lib/rootScope';
import ColorPicker, {ColorPickerColor} from '../../colorPicker'; import ColorPicker, {ColorPickerColor} from '../../colorPicker';
import SettingSection from '../../settingSection'; import SettingSection from '../../settingSection';
import {SliderSuperTab} from '../../slider'; import {SliderSuperTab} from '../../slider';
import {WallPaper} from '../../../layer';
export default class AppBackgroundColorTab extends SliderSuperTab { export default class AppBackgroundColorTab extends SliderSuperTab {
private colorPicker: ColorPicker; private colorPicker: ColorPicker;
private grid: HTMLElement; private grid: HTMLElement;
private applyColor: (hex: string, updateColorPicker?: boolean) => void; private applyColor: (hex: string, updateColorPicker?: boolean) => void;
private theme: Theme; private theme: AppTheme;
init() { init() {
this.container.classList.add('background-container', 'background-color-container'); this.container.classList.add('background-container', 'background-color-container');
@ -92,8 +93,10 @@ 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.settings;
const target = background.color ? this.grid.querySelector(`.grid-item[data-color="${background.color}"]`) : null; const wallPaper = background.wallpaper;
const color = wallPaper.settings.background_color;
const target = color ? this.grid.querySelector(`.grid-item[data-color="${color}"]`) : null;
if(active === target) { if(active === target) {
return; return;
} }
@ -112,14 +115,22 @@ export default class AppBackgroundColorTab extends SliderSuperTab {
this.colorPicker.setColor(hex); this.colorPicker.setColor(hex);
} else { } else {
const rgba = hexaToRgba(hex); const rgba = hexaToRgba(hex);
const background = this.theme.background; const settings = this.theme.settings;
const hsla = highlightningColor(rgba); const hsla = highlightningColor(rgba);
background.id = '2'; const wallPaper: WallPaper.wallPaperNoFile = {
background.intensity = 0; _: 'wallPaperNoFile',
background.slug = ''; id: 0,
background.color = hex.toLowerCase(); pFlags: {},
background.highlightningColor = hsla; settings: {
_: 'wallPaperSettings',
background_color: parseInt(hex.slice(1), 16)
}
};
settings.wallpaper = wallPaper;
settings.highlightningColor = hsla;
this.managers.appStateManager.pushToState('settings', rootScope.settings); this.managers.appStateManager.pushToState('settings', rootScope.settings);
appImManager.applyCurrentTheme(undefined, undefined, true); appImManager.applyCurrentTheme(undefined, undefined, true);
@ -133,17 +144,17 @@ export default class AppBackgroundColorTab extends SliderSuperTab {
onOpen() { onOpen() {
setTimeout(() => { setTimeout(() => {
const background = this.theme.background; const settings = this.theme.settings;
const color = settings?.wallpaper?.settings?.background_color;
const color = (background.color || '').split(',')[0]; const isColored = !!color && settings.wallpaper._ === 'wallPaperNoFile';
const isColored = !!color && !background.slug;
// * set active if type is color // * set active if type is color
if(isColored) { if(isColored) {
this.colorPicker.onChange = this.onColorChange; this.colorPicker.onChange = this.onColorChange;
} }
this.colorPicker.setColor(color || '#cccccc'); this.colorPicker.setColor((color && '#' + color.toString(16)) || '#cccccc');
if(!isColored) { if(!isColored) {
this.colorPicker.onChange = this.onColorChange; this.colorPicker.onChange = this.onColorChange;

View File

@ -61,6 +61,7 @@ export default class AppEditFolderTab extends SliderSuperTab {
this.stickerContainer.classList.add('sticker-container'); this.stickerContainer.classList.add('sticker-container');
this.confirmBtn = ButtonIcon('check btn-confirm hide blue'); this.confirmBtn = ButtonIcon('check btn-confirm hide blue');
let deleting = false;
const deleteFolderButton: ButtonMenuItemOptions = { const deleteFolderButton: ButtonMenuItemOptions = {
icon: 'delete danger', icon: 'delete danger',
text: 'FilterMenuDelete', text: 'FilterMenuDelete',
@ -71,13 +72,16 @@ export default class AppEditFolderTab extends SliderSuperTab {
buttons: [{ buttons: [{
langKey: 'Delete', langKey: 'Delete',
callback: () => { callback: () => {
deleteFolderButton.element.setAttribute('disabled', 'true'); if(deleting) {
return;
}
deleting = true;
this.managers.filtersStorage.updateDialogFilter(this.filter, true).then((bool) => { this.managers.filtersStorage.updateDialogFilter(this.filter, true).then((bool) => {
if(bool) { this.close();
this.close();
}
}).finally(() => { }).finally(() => {
deleteFolderButton.element.removeAttribute('disabled'); deleting = false;
}); });
}, },
isDanger: true isDanger: true
@ -212,7 +216,7 @@ export default class AppEditFolderTab extends SliderSuperTab {
this.confirmBtn.setAttribute('disabled', 'true'); this.confirmBtn.setAttribute('disabled', 'true');
let promise: Promise<boolean>; let promise: Promise<void>;
if(!this.filter.id) { if(!this.filter.id) {
promise = this.managers.filtersStorage.createDialogFilter(this.filter); promise = this.managers.filtersStorage.createDialogFilter(this.filter);
} else { } else {
@ -220,9 +224,7 @@ export default class AppEditFolderTab extends SliderSuperTab {
} }
promise.then((bool) => { promise.then((bool) => {
if(bool) { this.close();
this.close();
}
}).catch((err) => { }).catch((err) => {
if(err.type === 'DIALOG_FILTERS_TOO_MUCH') { if(err.type === 'DIALOG_FILTERS_TOO_MUCH') {
toast('Sorry, you can\'t create more folders.'); toast('Sorry, you can\'t create more folders.');
@ -266,6 +268,7 @@ export default class AppEditFolderTab extends SliderSuperTab {
this.setFilter(this.originalFilter, true); this.setFilter(this.originalFilter, true);
this.onEditOpen(); this.onEditOpen();
} else { } else {
this.setInitFilter();
this.onCreateOpen(); this.onCreateOpen();
} }
}); });
@ -283,7 +286,6 @@ export default class AppEditFolderTab extends SliderSuperTab {
this.setTitle('FilterNew'); this.setTitle('FilterNew');
this.menuBtn.classList.add('hide'); this.menuBtn.classList.add('hide');
this.confirmBtn.classList.remove('hide'); this.confirmBtn.classList.remove('hide');
this.nameInputField.value = '';
for(const flag in this.flags) { for(const flag in this.flags) {
// @ts-ignore // @ts-ignore

View File

@ -11,11 +11,11 @@ import RadioField from '../../radioField';
import rootScope from '../../../lib/rootScope'; import rootScope from '../../../lib/rootScope';
import {IS_APPLE} from '../../../environment/userAgent'; import {IS_APPLE} from '../../../environment/userAgent';
import Row, {CreateRowFromCheckboxField} from '../../row'; import Row, {CreateRowFromCheckboxField} from '../../row';
import AppBackgroundTab from './background'; import AppBackgroundTab, {getHexColorFromTelegramColor, getRgbColorFromTelegramColor} from './background';
import {LangPackKey, _i18n} from '../../../lib/langPack'; import {LangPackKey, _i18n} from '../../../lib/langPack';
import {attachClickEvent} from '../../../helpers/dom/clickEvent'; import {attachClickEvent} from '../../../helpers/dom/clickEvent';
import assumeType from '../../../helpers/assumeType'; import assumeType from '../../../helpers/assumeType';
import {AvailableReaction, MessagesAllStickers, StickerSet} from '../../../layer'; import {AvailableReaction, BaseTheme, MessagesAllStickers, StickerSet} from '../../../layer';
import LazyLoadQueue from '../../lazyLoadQueue'; import LazyLoadQueue from '../../lazyLoadQueue';
import PopupStickers from '../../popups/stickers'; import PopupStickers from '../../popups/stickers';
import eachMinute from '../../../helpers/eachMinute'; import eachMinute from '../../../helpers/eachMinute';
@ -27,6 +27,14 @@ import {State} from '../../../config/state';
import wrapStickerSetThumb from '../../wrappers/stickerSetThumb'; import wrapStickerSetThumb from '../../wrappers/stickerSetThumb';
import wrapStickerToRow from '../../wrappers/stickerToRow'; import wrapStickerToRow from '../../wrappers/stickerToRow';
import SettingSection, {generateSection} from '../../settingSection'; import SettingSection, {generateSection} from '../../settingSection';
import {ScrollableX} from '../../scrollable';
import wrapStickerEmoji from '../../wrappers/stickerEmoji';
import {Theme} from '../../../layer';
import findUpClassName from '../../../helpers/dom/findUpClassName';
import RLottiePlayer from '../../../lib/rlottie/rlottiePlayer';
import {hexToRgb, ColorRgb, rgbaToHexa, rgbaToHsla, rgbToHsv, hsvToRgb} from '../../../helpers/color';
import clamp from '../../../helpers/number/clamp';
import themeController from '../../../helpers/themeController';
export class RangeSettingSelector { export class RangeSettingSelector {
public container: HTMLDivElement; public container: HTMLDivElement;
@ -87,11 +95,20 @@ export class RangeSettingSelector {
} }
export default class AppGeneralSettingsTab extends SliderSuperTabEventable { export default class AppGeneralSettingsTab extends SliderSuperTabEventable {
init() { public static getInitArgs() {
return {
accountThemes: rootScope.managers.apiManager.invokeApi('account.getThemes', {format: 'android', hash: 0}),
allStickers: rootScope.managers.appStickersManager.getAllStickers(),
quickReaction: rootScope.managers.appReactionsManager.getQuickReaction()
};
}
public init(p: ReturnType<typeof AppGeneralSettingsTab['getInitArgs']>) {
this.container.classList.add('general-settings-container'); this.container.classList.add('general-settings-container');
this.setTitle('General'); this.setTitle('General');
const section = generateSection.bind(null, this.scrollable); const section = generateSection.bind(null, this.scrollable);
const promises: Promise<any>[] = [];
{ {
const container = section('Settings'); const container = section('Settings');
@ -122,6 +139,320 @@ export default class AppGeneralSettingsTab extends SliderSuperTabEventable {
); );
} }
if(false) {
const container = section('ColorTheme');
const scrollable = new ScrollableX(null);
const themesContainer = scrollable.container;
themesContainer.classList.add('themes-container');
type K = {theme: Theme, player?: RLottiePlayer};
const themesMap = new Map<HTMLElement, K>();
type AppColorName = 'primary-color' | 'message-out-primary-color';
type AppColor = {
rgb?: boolean,
light?: boolean,
lightFilled?: boolean,
dark?: boolean,
darkRgb?: boolean,
darkFilled?: boolean
};
const appColorMap: {[name in AppColorName]: AppColor} = {
'primary-color': {
rgb: true,
light: true,
lightFilled: true,
dark: true,
darkRgb: true
},
'message-out-primary-color': {
rgb: true,
light: true,
lightFilled: true,
dark: true
}
};
var mix = function(color1: ColorRgb, color2: ColorRgb, weight: number) {
const out = new Array<number>(3) as ColorRgb;
for(let i = 0; i < 3; ++i) {
const v1 = color1[i], v2 = color2[i];
out[i] = Math.floor(v2 + (v1 - v2) * (weight / 100.0));
}
return out;
};
function computePerceivedBrightness(color: ColorRgb) {
return (color[0] * 0.2126 + color[1] * 0.7152 + color[2] * 0.0722) / 255;
}
function getAverageColor(color1: ColorRgb, color2: ColorRgb): ColorRgb {
return color1.map((v, i) => Math.round((v + color2[i]) / 2)) as ColorRgb;
}
const getAccentColor = (baseHsv: number[], baseColor: ColorRgb, elementColor: ColorRgb): ColorRgb => {
const hsvTemp3 = rgbToHsv(...baseColor);
const hsvTemp4 = rgbToHsv(...elementColor);
const dist = Math.min(1.5 * hsvTemp3[1] / baseHsv[1], 1);
hsvTemp3[0] = Math.min(360, hsvTemp4[0] - hsvTemp3[0] + baseHsv[0]);
hsvTemp3[1] = Math.min(1, hsvTemp4[1] * baseHsv[1] / hsvTemp3[1]);
hsvTemp3[2] = Math.min(1, (hsvTemp4[2] / hsvTemp3[2] + dist - 1) * baseHsv[2] / dist);
if(hsvTemp3[2] < 0.3) {
return elementColor;
}
return hsvToRgb(...hsvTemp3);
};
const changeColorAccent = (baseHsv: number[], accentHsv: number[], color: ColorRgb, isDarkTheme = themeController.isNight()) => {
const colorHsv = rgbToHsv(...color);
const diffH = Math.min(Math.abs(colorHsv[0] - baseHsv[0]), Math.abs(colorHsv[0] - baseHsv[0] - 360));
if(diffH > 30) {
return color;
}
const dist = Math.min(1.5 * colorHsv[1] / baseHsv[1], 1);
colorHsv[0] = Math.min(360, colorHsv[0] + accentHsv[0] - baseHsv[0]);
colorHsv[1] = Math.min(1, colorHsv[1] * accentHsv[1] / baseHsv[1]);
colorHsv[2] = Math.min(1, colorHsv[2] * (1 - dist + dist * accentHsv[2] / baseHsv[2]));
let newColor = hsvToRgb(...colorHsv);
const origBrightness = computePerceivedBrightness(color);
const newBrightness = computePerceivedBrightness(newColor);
// We need to keep colors lighter in dark themes and darker in light themes
const needRevertBrightness = isDarkTheme ? origBrightness > newBrightness : origBrightness < newBrightness;
if(needRevertBrightness) {
const amountOfNew = 0.6;
const fallbackAmount = (1 - amountOfNew) * origBrightness / newBrightness + amountOfNew;
newColor = changeBrightness(newColor, fallbackAmount);
}
return newColor;
};
const changeBrightness = (color: ColorRgb, amount: number) => {
return color.map((v) => clamp(Math.round(v * amount), 0, 255)) as ColorRgb;
};
const applyAppColor = ({
name,
hex,
element = document.documentElement,
lightenAlpha = 0.08,
darkenAlpha = lightenAlpha
}: {
name: AppColorName,
hex: string,
element?: HTMLElement,
lightenAlpha?: number
darkenAlpha?: number
}) => {
const appColor = appColorMap[name];
const rgb = hexToRgb(hex);
const hsla = rgbaToHsla(...rgb);
const mixColor2 = hexToRgb(themeController.isNight() ? '#212121' : '#ffffff');
const lightenedRgb = mix(rgb, mixColor2, lightenAlpha * 100);
const darkenedHsla: typeof hsla = {
...hsla,
l: hsla.l - darkenAlpha * 100
};
element.style.setProperty('--' + name, hex);
appColor.rgb && element.style.setProperty('--' + name + '-rgb', rgb.join(','));
appColor.light && element.style.setProperty('--light-' + name, `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${lightenAlpha})`);
appColor.lightFilled && element.style.setProperty('--light-filled-' + name, `rgb(${lightenedRgb[0]}, ${lightenedRgb[1]}, ${lightenedRgb[2]})`);
appColor.dark && element.style.setProperty('--dark-' + name, `hsl(${darkenedHsla.h}, ${darkenedHsla.s}%, ${darkenedHsla.l}%)`);
// appColor.darkFilled && element.style.setProperty('--dark-' + name, `hsl(${darkenedHsla.h}, ${darkenedHsla.s}%, ${darkenedHsla.l}%)`);
};
const applyTheme = (theme: Theme, element = document.documentElement) => {
const isNight = themeController.isNight();
const themeSettings = theme.settings.find((settings) => settings.base_theme._ === (isNight ? 'baseThemeNight' : 'baseThemeClassic'));
console.log('applyTheme', theme, themeSettings);
// android `accentBaseColor` and `key_chat_outBubble`
const PRIMARY_COLOR = isNight ? '#3e88f6' : '#328ace';
const LIGHT_PRIMARY_COLOR = isNight ? '#366cae' : '#e6f2fb';
const hsvTemp1 = rgbToHsv(...hexToRgb(PRIMARY_COLOR)); // primary base
let hsvTemp2 = rgbToHsv(...getRgbColorFromTelegramColor(themeSettings.accent_color)); // new primary
const newAccentRgb = changeColorAccent(
hsvTemp1,
hsvTemp2,
hexToRgb(PRIMARY_COLOR)
// hexToRgb('#eeffde')
);
const newAccentHex = rgbaToHexa(newAccentRgb);
let h = getHexColorFromTelegramColor(themeSettings.accent_color);
console.log(h, newAccentHex);
h = newAccentHex;
applyAppColor({
name: 'primary-color',
hex: h,
// hex: newAccentHex,
element,
darkenAlpha: 0.04
});
if(element === document.documentElement) {
AppBackgroundTab.setBackgroundDocument(themeSettings.wallpaper);
}
if(!themeSettings.message_colors?.length) {
return;
}
const messageOutRgbColor = hexToRgb(LIGHT_PRIMARY_COLOR); // light primary
const firstColor = getRgbColorFromTelegramColor(themeSettings.message_colors[0]);
let messageColor = firstColor;
if(themeSettings.message_colors.length > 1) {
themeSettings.message_colors.slice(1).forEach((nextColor) => {
messageColor = getAverageColor(messageColor, getRgbColorFromTelegramColor(nextColor));
});
messageColor = getAccentColor(hsvTemp1, messageOutRgbColor, firstColor);
}
const o = messageColor;
// const hsvTemp1 = rgbToHsv(...hexToRgb('#4fae4e'));
// const hsvTemp1 = rgbToHsv(...hexToRgb('#328ace'));
hsvTemp2 = rgbToHsv(...o);
const c = changeColorAccent(
hsvTemp1,
hsvTemp2,
messageOutRgbColor
// hexToRgb('#eeffde')
);
console.log(o, c);
applyAppColor({
name: 'message-out-primary-color',
hex: rgbaToHexa(messageColor),
element,
lightenAlpha: isNight ? 0.76 : 0.12
});
};
attachClickEvent(themesContainer, (e) => {
const container = findUpClassName(e.target, 'theme-container');
if(!container) {
return;
}
const lastActive = themesContainer.querySelector('.active');
if(lastActive) {
lastActive.classList.remove('active');
}
const item = themesMap.get(container);
container.classList.add('active');
if(item.player) {
if(item.player.paused) {
item.player.restart();
}
}
applyTheme(item.theme);
}, {listenerSetter: this.listenerSetter});
const promise = p.accountThemes.then(async(accountThemes) => {
if(accountThemes._ === 'account.themesNotModified') {
return;
}
console.log(accountThemes);
const defaultThemes = accountThemes.themes.filter((theme) => theme.pFlags.default);
const promises = defaultThemes.map(async(theme, idx) => {
const baseTheme: BaseTheme['_'] = themeController.isNight() ? 'baseThemeNight' : 'baseThemeClassic';
const wallpaper = theme.settings.find((settings) => settings.base_theme._ === baseTheme).wallpaper;
const result = AppBackgroundTab.addWallPaper(wallpaper);
const container = result.container;
const k: K = {theme};
themesMap.set(container, k);
applyTheme(theme, container);
if(idx === 0) {
container.classList.add('active');
}
const emoticon = theme.emoticon;
const loadPromises: Promise<any>[] = [];
let emoticonContainer: HTMLElement;
if(emoticon) {
emoticonContainer = document.createElement('div');
emoticonContainer.classList.add('theme-emoticon');
const size = 28;
wrapStickerEmoji({
div: emoticonContainer,
width: size,
height: size,
emoji: theme.emoticon,
managers: this.managers,
loadPromises,
middleware: this.middlewareHelper.get()
}).then(({render}) => render).then((player) => {
k.player = player as RLottiePlayer;
});
}
const bubble = document.createElement('div');
bubble.classList.add('theme-bubble');
const bubbleIn = bubble.cloneNode() as HTMLElement;
bubbleIn.classList.add('is-in');
bubble.classList.add('is-out');
loadPromises.push(result.loadPromise);
container.classList.add('theme-container');
await Promise.all(loadPromises);
if(emoticonContainer) {
container.append(emoticonContainer);
}
container.append(bubbleIn, bubble);
return container;
});
const containers = await Promise.all(promises);
scrollable.append(...containers);
});
promises.push(promise);
container.append(
themesContainer
);
}
{ {
const container = section('General.Keyboard'); const container = section('General.Keyboard');
@ -264,7 +595,7 @@ export default class AppGeneralSettingsTab extends SliderSuperTabEventable {
}); });
const renderQuickReaction = () => { const renderQuickReaction = () => {
this.managers.appReactionsManager.getQuickReaction().then((reaction) => { p.quickReaction.then((reaction) => {
if(reaction._ === 'availableReaction') { if(reaction._ === 'availableReaction') {
return reaction.static_icon; return reaction.static_icon;
} else { } else {
@ -325,7 +656,8 @@ export default class AppGeneralSettingsTab extends SliderSuperTabEventable {
lazyLoadQueue, lazyLoadQueue,
width: 36, width: 36,
height: 36, height: 36,
autoplay: true autoplay: true,
middleware: this.middlewareHelper.get()
}); });
row.container.append(div); row.container.append(div);
@ -333,13 +665,14 @@ export default class AppGeneralSettingsTab extends SliderSuperTabEventable {
stickersContent[method](row.container); stickersContent[method](row.container);
}; };
this.managers.appStickersManager.getAllStickers().then((allStickers) => { const promise = p.allStickers.then((allStickers) => {
assumeType<MessagesAllStickers.messagesAllStickers>(allStickers); assumeType<MessagesAllStickers.messagesAllStickers>(allStickers);
for(const stickerSet of allStickers.sets) { const promises = allStickers.sets.map((stickerSet) => renderStickerSet(stickerSet));
renderStickerSet(stickerSet); return Promise.all(promises);
}
}); });
promises.push(promise);
this.listenerSetter.add(rootScope)('stickers_installed', (set) => { this.listenerSetter.add(rootScope)('stickers_installed', (set) => {
if(!stickerSets[set.id]) { if(!stickerSets[set.id]) {
renderStickerSet(set, 'prepend'); renderStickerSet(set, 'prepend');
@ -360,12 +693,7 @@ export default class AppGeneralSettingsTab extends SliderSuperTabEventable {
); );
this.scrollable.append(section.container); this.scrollable.append(section.container);
} }
}
onOpen() { return Promise.all(promises);
if(this.init) {
this.init();
this.init = null;
}
} }
} }

View File

@ -381,8 +381,10 @@ export default async function wrapSticker({doc, div, middleware, loadStickerMidd
needUpscale, needUpscale,
skipRatio, skipRatio,
toneIndex, toneIndex,
sync: isCustomEmoji sync: isCustomEmoji,
}, group, loadStickerMiddleware ?? middleware); middleware: loadStickerMiddleware ?? middleware,
group
});
// const deferred = deferredPromise<void>(); // const deferred = deferredPromise<void>();
@ -557,7 +559,7 @@ export default async function wrapSticker({doc, div, middleware, loadStickerMidd
} }
if(isAnimated) { if(isAnimated) {
animationIntersector.addAnimation(media as HTMLVideoElement, group); animationIntersector.addAnimation(media as HTMLVideoElement, group, undefined, middleware);
} }
if(loaded.push(media) === mediaLength) { if(loaded.push(media) === mediaLength) {

View File

@ -7,14 +7,17 @@
import {AppManagers} from '../../lib/appManagers/managers'; import {AppManagers} from '../../lib/appManagers/managers';
import rootScope from '../../lib/rootScope'; import rootScope from '../../lib/rootScope';
import wrapSticker from './sticker' import wrapSticker from './sticker'
import {Modify} from '../../types';
export default async function wrapStickerEmoji({emoji, div, width, height, managers = rootScope.managers}: { export default async function wrapStickerEmoji(options: Modify<Parameters<typeof wrapSticker>[0], {
emoji: string,
div: HTMLElement, div: HTMLElement,
managers?: AppManagers, doc?: never
width: number, }>) {
height: number const {
}) { emoji,
div,
managers = rootScope.managers
} = options;
const doc = await managers.appStickersManager.getAnimatedEmojiSticker(emoji); const doc = await managers.appStickersManager.getAnimatedEmojiSticker(emoji);
if(!doc) { if(!doc) {
div.classList.add('media-sticker-wrapper'); div.classList.add('media-sticker-wrapper');
@ -22,11 +25,8 @@ export default async function wrapStickerEmoji({emoji, div, width, height, manag
} }
return wrapSticker({ return wrapSticker({
...options,
doc, doc,
div,
emoji,
width,
height,
loop: false, loop: false,
play: true play: true
}); });

View File

@ -14,8 +14,9 @@ import rootScope from '../../lib/rootScope';
import animationIntersector, {AnimationItemGroup} from '../animationIntersector'; import animationIntersector, {AnimationItemGroup} from '../animationIntersector';
import LazyLoadQueue from '../lazyLoadQueue'; import LazyLoadQueue from '../lazyLoadQueue';
import wrapSticker from './sticker'; import wrapSticker from './sticker';
import {Middleware} from '../../helpers/middleware';
export default async function wrapStickerSetThumb({set, lazyLoadQueue, container, group, autoplay, width, height, managers = rootScope.managers}: { export default async function wrapStickerSetThumb({set, lazyLoadQueue, container, group, autoplay, width, height, managers = rootScope.managers, middleware}: {
set: StickerSet.stickerSet, set: StickerSet.stickerSet,
lazyLoadQueue: LazyLoadQueue, lazyLoadQueue: LazyLoadQueue,
container: HTMLElement, container: HTMLElement,
@ -24,6 +25,7 @@ export default async function wrapStickerSetThumb({set, lazyLoadQueue, container
width: number, width: number,
height: number, height: number,
managers?: AppManagers managers?: AppManagers
middleware?: Middleware
}) { }) {
if(set.thumbs?.length) { if(set.thumbs?.length) {
container.classList.add('media-sticker-wrapper'); container.classList.add('media-sticker-wrapper');
@ -44,8 +46,10 @@ export default async function wrapStickerSetThumb({set, lazyLoadQueue, container
width, width,
height, height,
needUpscale: true, needUpscale: true,
name: 'setThumb' + set.id name: 'setThumb' + set.id,
}, group); group,
middleware
});
}); });
} else { } else {
let media: HTMLElement; let media: HTMLElement;
@ -93,7 +97,8 @@ export default async function wrapStickerSetThumb({set, lazyLoadQueue, container
lazyLoadQueue, lazyLoadQueue,
managers, managers,
width, width,
height height,
middleware
}); // kostil }); // kostil
} }
} }

View File

@ -21,7 +21,7 @@ const App = {
version: process.env.VERSION, version: process.env.VERSION,
versionFull: process.env.VERSION_FULL, versionFull: process.env.VERSION_FULL,
build: +process.env.BUILD, build: +process.env.BUILD,
langPackVersion: '0.8.0', langPackVersion: '0.8.3',
langPack: 'webk', langPack: 'webk',
langPackCode: 'en', langPackCode: 'en',
domains: MAIN_DOMAINS, domains: MAIN_DOMAINS,

View File

@ -8,16 +8,17 @@ import {AppMediaPlaybackController} from '../components/appMediaPlaybackControll
import {IS_MOBILE} from '../environment/userAgent'; import {IS_MOBILE} from '../environment/userAgent';
import getTimeFormat from '../helpers/getTimeFormat'; import getTimeFormat from '../helpers/getTimeFormat';
import {nextRandomUint} from '../helpers/random'; import {nextRandomUint} from '../helpers/random';
import {AutoDownloadSettings, NotifyPeer, PeerNotifySettings} from '../layer'; import {AutoDownloadSettings, BaseTheme, NotifyPeer, PeerNotifySettings, Theme, ThemeSettings, WallPaper} from '../layer';
import {TopPeerType, MyTopPeer} from '../lib/appManagers/appUsersManager'; import {TopPeerType, MyTopPeer} from '../lib/appManagers/appUsersManager';
import DialogsStorage from '../lib/storages/dialogs'; import DialogsStorage from '../lib/storages/dialogs';
import FiltersStorage from '../lib/storages/filters'; import FiltersStorage from '../lib/storages/filters';
import {AuthState} from '../types'; import {AuthState, Modify} from '../types';
import App from './app'; import App from './app';
const STATE_VERSION = App.version; const STATE_VERSION = App.version;
const BUILD = App.build; const BUILD = App.build;
// ! DEPRECATED
export type Background = { export type Background = {
type?: 'color' | 'image' | 'default', // ! DEPRECATED type?: 'color' | 'image' | 'default', // ! DEPRECATED
blur: boolean, blur: boolean,
@ -28,10 +29,12 @@ export type Background = {
id: string | number, // wallpaper id id: string | number, // wallpaper id
}; };
export type Theme = { export type AppTheme = Modify<Theme, {
name: 'day' | 'night' | 'system', name: 'day' | 'night' | 'system',
background: Background settings?: Modify<ThemeSettings, {
}; highlightningColor: string
}>
}>;
export type AutoDownloadPeerTypeSettings = { export type AutoDownloadPeerTypeSettings = {
contacts: boolean, contacts: boolean,
@ -95,8 +98,8 @@ export type State = {
big: boolean big: boolean
}, },
background?: Background, // ! DEPRECATED background?: Background, // ! DEPRECATED
themes: Theme[], themes: AppTheme[],
theme: Theme['name'], theme: AppTheme['name'],
notifications: { notifications: {
sound: boolean sound: boolean
}, },
@ -110,41 +113,101 @@ export type State = {
notifySettings: {[k in Exclude<NotifyPeer['_'], 'notifyPeer'>]?: PeerNotifySettings.peerNotifySettings} notifySettings: {[k in Exclude<NotifyPeer['_'], 'notifyPeer'>]?: PeerNotifySettings.peerNotifySettings}
}; };
const BACKGROUND_DAY_DESKTOP: Background = { // const BACKGROUND_DAY_MOBILE: Background = {
blur: false, // blur: false,
slug: 'pattern', // slug: '',
color: '#dbddbb,#6ba587,#d5d88d,#88b884', // color: '#dbddbb,#6ba587,#d5d88d,#88b884',
highlightningColor: 'hsla(86.4, 43.846153%, 45.117647%, .4)', // highlightningColor: 'hsla(86.4, 43.846153%, 45.117647%, .4)',
intensity: 50, // intensity: 0,
id: '1' // id: '1'
}; // };
const BACKGROUND_DAY_MOBILE: Background = { // const BACKGROUND_NIGHT_MOBILE: Background = {
blur: false, // blur: false,
// slug: '',
// color: '#0f0f0f',
// highlightningColor: 'hsla(0, 0%, 3.82353%, 0.4)',
// intensity: 0,
// id: '-1'
// };
export const DEFAULT_THEME: Theme = {
_: 'theme',
access_hash: '',
id: '',
settings: [{
_: 'themeSettings',
pFlags: {},
base_theme: {_: 'baseThemeClassic'},
accent_color: 0x3390ec,
message_colors: [0x4fae4e],
wallpaper: {
_: 'wallPaper',
pFlags: {
default: true,
pattern: true
},
access_hash: '',
document: undefined,
id: '',
slug: 'pattern',
settings: {
_: 'wallPaperSettings',
pFlags: {},
intensity: 50,
background_color: 0xdbddbb,
second_background_color: 0x6ba587,
third_background_color: 0xd5d88d,
fourth_background_color: 0x88b884
}
}
}, {
_: 'themeSettings',
pFlags: {},
base_theme: {_: 'baseThemeNight'},
accent_color: 0x8774E1,
message_colors: [0x8774E1],
wallpaper: {
_: 'wallPaper',
pFlags: {
default: true,
pattern: true,
dark: true
},
access_hash: '',
document: undefined,
id: '',
slug: 'pattern',
settings: {
_: 'wallPaperSettings',
pFlags: {},
intensity: -50,
background_color: 0xfec496,
second_background_color: 0xdd6cb9,
third_background_color: 0x962fbf,
fourth_background_color: 0x4f5bd5
}
}
}],
slug: '', slug: '',
color: '#dbddbb,#6ba587,#d5d88d,#88b884', title: '',
highlightningColor: 'hsla(86.4, 43.846153%, 45.117647%, .4)', emoticon: '🏠',
intensity: 0, pFlags: {default: true}
id: '1'
}; };
const BACKGROUND_NIGHT_DESKTOP: Background = { const makeDefaultAppTheme = (
blur: false, name: AppTheme['name'],
slug: 'pattern', baseTheme: BaseTheme['_'],
// color: '#dbddbb,#6ba587,#d5d88d,#88b884', highlightningColor: string
color: '#fec496,#dd6cb9,#962fbf,#4f5bd5', ): AppTheme => {
highlightningColor: 'hsla(299.142857, 44.166666%, 37.470588%, .4)', return {
intensity: -50, ...DEFAULT_THEME,
id: '-1' name,
}; settings: {
...DEFAULT_THEME.settings.find((s) => s.base_theme._ === baseTheme),
const BACKGROUND_NIGHT_MOBILE: Background = { highlightningColor
blur: false, }
slug: '', };
color: '#0f0f0f',
highlightningColor: 'hsla(0, 0%, 3.82353%, 0.4)',
intensity: 0,
id: '-1'
}; };
export const STATE_INIT: State = { export const STATE_INIT: State = {
@ -214,13 +277,10 @@ export const STATE_INIT: State = {
suggest: true, suggest: true,
big: true big: true
}, },
themes: [{ themes: [
name: 'day', makeDefaultAppTheme('day', 'baseThemeClassic', 'hsla(86.4, 43.846153%, 45.117647%, .4)'),
background: IS_MOBILE ? BACKGROUND_DAY_MOBILE : BACKGROUND_DAY_DESKTOP makeDefaultAppTheme('night', 'baseThemeNight', 'hsla(299.142857, 44.166666%, 37.470588%, .4)')
}, { ],
name: 'night',
background: IS_MOBILE ? BACKGROUND_NIGHT_MOBILE : BACKGROUND_NIGHT_DESKTOP
}],
theme: 'system', theme: 'system',
notifications: { notifications: {
sound: false sound: false

View File

@ -0,0 +1,2 @@
const IS_INSTALL_PROMPT_SUPPORTED = 'onbeforeinstallprompt' in window;
export default IS_INSTALL_PROMPT_SUPPORTED;

View File

@ -0,0 +1,2 @@
const IS_STANDALONE = window.matchMedia('(display-mode: standalone)').matches;
export default IS_STANDALONE;

View File

@ -21,4 +21,4 @@ export const IS_FIREFOX = navigator.userAgent.toLowerCase().indexOf('firefox') >
export const IS_MOBILE_SAFARI = IS_SAFARI && IS_APPLE_MOBILE; export const IS_MOBILE_SAFARI = IS_SAFARI && IS_APPLE_MOBILE;
export const IS_MOBILE = /* screen.width && screen.width < 480 || */navigator.maxTouchPoints > 0 && navigator.userAgent.search(/iOS|iPhone OS|Android|BlackBerry|BB10|Series ?[64]0|J2ME|MIDP|opera mini|opera mobi|mobi.+Gecko|Windows Phone/i) != -1; export const IS_MOBILE = (navigator.maxTouchPoints === undefined || navigator.maxTouchPoints > 0) && navigator.userAgent.search(/iOS|iPhone OS|Android|BlackBerry|BB10|Series ?[64]0|J2ME|MIDP|opera mini|opera mobi|mobi.+Gecko|Windows Phone/i) != -1;

View File

@ -1,6 +1,7 @@
import IS_MOV_SUPPORTED from './movSupport'; import IS_MOV_SUPPORTED from './movSupport';
const VIDEO_MIME_TYPES_SUPPORTED = new Set([ export type VIDEO_MIME_TYPE = 'image/gif' | 'video/mp4' | 'video/webm' | 'video/quicktime';
const VIDEO_MIME_TYPES_SUPPORTED: Set<VIDEO_MIME_TYPE> = new Set([
'image/gif', // have to display it as video 'image/gif', // have to display it as video
'video/mp4', 'video/mp4',
'video/webm' 'video/webm'

View File

@ -0,0 +1,55 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import type {VIDEO_MIME_TYPE} from '../../environment/videoMimeTypesSupport';
export default function canvasToVideo({
canvas,
timeslice,
duration,
// mimeType = 'video/webm; codecs="vp8"',
mimeType = 'video/webm; codecs="vp8"',
audioBitsPerSecond = 0,
videoBitsPerSecond = 25000000
}: {
canvas: HTMLCanvasElement
timeslice: number,
duration: number,
mimeType?: string,
audioBitsPerSecond?: number,
videoBitsPerSecond?: number
}) {
return new Promise<Blob>((resolve, reject) => {
try {
const stream = canvas.captureStream();
const blobs: Blob[] = [];
const recorder = new MediaRecorder(stream, {
mimeType,
audioBitsPerSecond,
videoBitsPerSecond
});
recorder.ondataavailable = (event) => {
if(event.data && event.data.size > 0) {
blobs.push(event.data);
}
if(blobs.length === duration / timeslice) {
stream.getTracks()[0].stop();
recorder.stop();
resolve(new Blob(blobs, {type: mimeType}));
}
};
recorder.start(timeslice);
} catch(e) {
reject(e);
}
});
}
(window as any).canvasToVideo = canvasToVideo;

View File

@ -14,6 +14,31 @@ export type ColorHsla = {
export type ColorRgba = [number, number, number, number]; export type ColorRgba = [number, number, number, number];
export type ColorRgb = [number, number, number]; export type ColorRgb = [number, number, number];
/**
* https://stackoverflow.com/a/54070620/6758968
* r, g, b in [0, 255]
* @returns h in [0,360) and s, v in [0,1]
*/
export function rgbToHsv(r: number, g: number, b: number): [number, number, number] {
r /= 255, g /= 255, b /= 255;
const v = Math.max(r, g, b),
c = v - Math.min(r, g, b);
const h = c && ((v === r) ? (g - b ) / c : ((v == g) ? 2 + (b - r) / c : 4 + (r - g) / c));
return [60 * (h < 0 ? h + 6 : h), v && c / v, v];
}
/**
* https://stackoverflow.com/a/54024653/6758968
* @param h [0, 360]
* @param s [0, 1]
* @param v [0, 1]
* @returns r, g, b in [0, 255]
*/
export function hsvToRgb(h: number, s: number, v: number): ColorRgb {
const f = (n: number, k: number = (n + h / 60) % 6) => Math.round((v - v * s * Math.max(Math.min(k, 4 - k, 1), 0)) * 255);
return [f(5), f(3), f(1)];
}
/** /**
* @returns h [0, 360], s [0, 100], l [0, 100], a [0, 1] * @returns h [0, 360], s [0, 100], l [0, 100], a [0, 1]
*/ */
@ -55,13 +80,11 @@ export function rgbaToHsla(r: number, g: number, b: number, a: number = 1): Colo
/** /**
* Converts an HSL color value to RGB. Conversion formula * Converts an HSL color value to RGB. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space. * adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes h in [0, 360], s, and l are contained in the set [0, 1], a in [0, 1] and
* returns r, g, and b in the set [0, 255].
* *
* @param {number} h The hue * @param {number} h The hue [0, 360]
* @param {number} s The saturation * @param {number} s The saturation [0, 1]
* @param {number} l The lightness * @param {number} l The lightness [0, 1]
* @return {Array} The RGB representation * @return {Array} The RGB representation [0, 255]
*/ */
export function hslaToRgba(h: number, s: number, l: number, a: number): ColorRgba { export function hslaToRgba(h: number, s: number, l: number, a: number): ColorRgba {
h /= 360, s /= 100, l /= 100; h /= 360, s /= 100, l /= 100;
@ -86,7 +109,7 @@ export function hslaToRgba(h: number, s: number, l: number, a: number): ColorRgb
b = hue2rgb(p, q, h - 1/3); b = hue2rgb(p, q, h - 1/3);
} }
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255), Math.round(a * 255)]; return [r, g, b, a].map((v) => Math.round(v * 255)) as ColorRgba;
} }
export function hslaStringToRgba(hsla: string) { export function hslaStringToRgba(hsla: string) {

View File

@ -0,0 +1,17 @@
let callback: () => Promise<void>;
export default function cacheInstallPrompt() {
window.addEventListener('beforeinstallprompt', (deferredPrompt: any) => {
callback = async() => {
deferredPrompt.prompt();
const {outcome} = await deferredPrompt.userChoice;
const installed = outcome === 'accepted';
if(installed) {
callback = undefined;
}
};
});
}
export function getInstallPrompt() {
return callback;
}

View File

@ -1,13 +1,25 @@
import copy from './copy'; import copy from './copy';
import isObject from './isObject'; import isObject from './isObject';
export default function validateInitObject(initObject: any, currentObject: any, onReplace?: (key: string) => void, previousKey?: string) { export default function validateInitObject(
initObject: any,
currentObject: any,
onReplace?: (key: string) => void,
previousKey?: string,
ignorePaths?: Set<string>,
path?: string
) {
for(const key in initObject) { for(const key in initObject) {
const _path = path ? `${path}.${key}` : key;
if(ignorePaths?.has(_path)) {
continue;
}
if(typeof(currentObject[key]) !== typeof(initObject[key])) { if(typeof(currentObject[key]) !== typeof(initObject[key])) {
currentObject[key] = copy(initObject[key]); currentObject[key] = copy(initObject[key]);
onReplace && onReplace(previousKey || key); onReplace?.(previousKey || key);
} else if(isObject(initObject[key])) { } else if(isObject(initObject[key])) {
validateInitObject(initObject[key], currentObject[key], onReplace, previousKey || key); validateInitObject(initObject[key], currentObject[key], onReplace, previousKey || key, ignorePaths, _path);
} }
} }
} }

View File

@ -15,7 +15,7 @@ export default function onMediaLoad(media: HTMLMediaElement, readyState = media.
}; };
const onError = (e: ErrorEvent) => { const onError = (e: ErrorEvent) => {
media.removeEventListener(loadEventName, onLoad); media.removeEventListener(loadEventName, onLoad);
reject(e); reject(media.error);
}; };
media.addEventListener(loadEventName, onLoad, {once: true}); media.addEventListener(loadEventName, onLoad, {once: true});
media.addEventListener(errorEventName, onError, {once: true}); media.addEventListener(errorEventName, onError, {once: true});

View File

@ -29,8 +29,9 @@ export default function preloadAnimatedEmojiSticker(emoji: string, width?: numbe
name: 'doc' + doc.id, name: 'doc' + doc.id,
autoplay: false, autoplay: false,
loop: false, loop: false,
toneIndex toneIndex,
}, 'none'); group: 'none'
});
animation.addEventListener('firstFrame', () => { animation.addEventListener('firstFrame', () => {
saveLottiePreview(doc, animation.canvas[0], toneIndex); saveLottiePreview(doc, animation.canvas[0], toneIndex);

View File

@ -4,7 +4,7 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
import type {Theme} from '../config/state'; import type {AppTheme} from '../config/state';
import IS_TOUCH_SUPPORTED from '../environment/touchSupport'; import IS_TOUCH_SUPPORTED from '../environment/touchSupport';
import rootScope from '../lib/rootScope'; import rootScope from '../lib/rootScope';
import {hslaStringToHex} from './color'; import {hslaStringToHex} from './color';
@ -12,7 +12,7 @@ import {hslaStringToHex} from './color';
export class ThemeController { export class ThemeController {
private themeColor: string; private themeColor: string;
private _themeColorElem: Element; private _themeColorElem: Element;
private systemTheme: Theme['name']; private systemTheme: AppTheme['name'];
constructor() { constructor() {
rootScope.addEventListener('theme_change', () => { rootScope.addEventListener('theme_change', () => {
@ -71,8 +71,8 @@ export class ThemeController {
public applyHighlightningColor() { public applyHighlightningColor() {
let hsla: string; let hsla: string;
const theme = themeController.getTheme(); const theme = themeController.getTheme();
if(theme.background.highlightningColor) { if(theme.settings?.highlightningColor) {
hsla = theme.background.highlightningColor; hsla = theme.settings.highlightningColor;
document.documentElement.style.setProperty('--message-highlightning-color', hsla); document.documentElement.style.setProperty('--message-highlightning-color', hsla);
} else { } else {
document.documentElement.style.removeProperty('--message-highlightning-color'); document.documentElement.style.removeProperty('--message-highlightning-color');
@ -100,7 +100,7 @@ export class ThemeController {
return this.getTheme().name === 'night'; return this.getTheme().name === 'night';
} }
public getTheme(name: Theme['name'] = rootScope.settings.theme === 'system' ? this.systemTheme : rootScope.settings.theme) { public getTheme(name: AppTheme['name'] = rootScope.settings.theme === 'system' ? this.systemTheme : rootScope.settings.theme) {
return rootScope.settings.themes.find((t) => t.name === name); return rootScope.settings.themes.find((t) => t.name === name);
} }
} }

View File

@ -30,6 +30,8 @@ import parseUriParams from './helpers/string/parseUriParams';
import Modes from './config/modes'; import Modes from './config/modes';
import {AuthState} from './types'; import {AuthState} from './types';
import {IS_BETA} from './config/debug'; import {IS_BETA} from './config/debug';
import IS_INSTALL_PROMPT_SUPPORTED from './environment/installPrompt';
import cacheInstallPrompt from './helpers/dom/installPrompt';
// import appNavigationController from './components/appNavigationController'; // import appNavigationController from './components/appNavigationController';
document.addEventListener('DOMContentLoaded', async() => { document.addEventListener('DOMContentLoaded', async() => {
@ -209,6 +211,10 @@ document.addEventListener('DOMContentLoaded', async() => {
}, {capture: true, passive: false}); */ }, {capture: true, passive: false}); */
} }
if(IS_INSTALL_PROMPT_SUPPORTED) {
cacheInstallPrompt();
}
const perf = performance.now(); const perf = performance.now();
// await pause(1000000); // await pause(1000000);

View File

@ -109,6 +109,7 @@ const lang = {
'one_value': '%d exception', 'one_value': '%d exception',
'other_value': '%d exceptions' 'other_value': '%d exceptions'
}, },
'PWA.Install': 'Install App',
'Link.Available': 'Link is available', 'Link.Available': 'Link is available',
'Link.Taken': 'Link is already taken', 'Link.Taken': 'Link is already taken',
'Link.Invalid': 'Link is invalid', 'Link.Invalid': 'Link is invalid',
@ -119,6 +120,11 @@ const lang = {
'Popup.Unpin.HideTitle': 'Hide pinned messages', 'Popup.Unpin.HideTitle': 'Hide pinned messages',
'Popup.Unpin.HideDescription': 'Do you want to hide the pinned message bar? It wil stay hidden until a new message is pinned.', 'Popup.Unpin.HideDescription': 'Do you want to hide the pinned message bar? It wil stay hidden until a new message is pinned.',
'Popup.Unpin.Hide': 'Hide', 'Popup.Unpin.Hide': 'Hide',
'Popup.Attach.GroupMedia': 'Group all media',
'Popup.Attach.UngroupMedia': 'Ungroup all media',
'Popup.Attach.AsMedia': 'Send as media',
'Popup.Attach.EnableSpoilers': 'Hide all with spoilers',
'Popup.Attach.RemoveSpoilers': 'Remove all spoilers',
'TwoStepAuth.EmailCodeChangeEmail': 'Change Email', 'TwoStepAuth.EmailCodeChangeEmail': 'Change Email',
'MarkupTooltip.LinkPlaceholder': 'Enter URL...', 'MarkupTooltip.LinkPlaceholder': 'Enter URL...',
'MediaViewer.Context.Download': 'Download', 'MediaViewer.Context.Download': 'Download',
@ -876,6 +882,9 @@ const lang = {
'LimitReachedFoldersLocked': 'You have reached the limit of **%1$d** folders for this account. We are working to let you increase this limit in the future.', 'LimitReachedFoldersLocked': 'You have reached the limit of **%1$d** folders for this account. We are working to let you increase this limit in the future.',
'FwdMessageToSavedMessages': 'Message forwarded to **Saved Messages**.', 'FwdMessageToSavedMessages': 'Message forwarded to **Saved Messages**.',
'FwdMessagesToSavedMessages': 'Messages forwarded to **Saved Messages**.', 'FwdMessagesToSavedMessages': 'Messages forwarded to **Saved Messages**.',
'ColorTheme': 'Color theme',
'SendAsFile': 'Send as file',
'SendAsFiles': 'Send as files',
// * macos // * macos
'AccountSettings.Filters': 'Chat Folders', 'AccountSettings.Filters': 'Chat Folders',

View File

@ -231,7 +231,7 @@ export class AppDocsManager extends AppManager {
doc.file_name = `${doc.type}_${date}${ext ? '.' + ext : ''}`; doc.file_name = `${doc.type}_${date}${ext ? '.' + ext : ''}`;
} }
if(isServiceWorkerOnline() && (doc.type === 'gif' && doc.size > 8e6) || doc.type === 'audio' || doc.type === 'video'/* || doc.mime_type.indexOf('video/') === 0 */) { if(isServiceWorkerOnline() && ((doc.type === 'gif' && doc.size > 8e6) || doc.type === 'audio' || doc.type === 'video')/* || doc.mime_type.indexOf('video/') === 0 */) {
doc.supportsStreaming = true; doc.supportsStreaming = true;
const cacheContext = this.thumbsStorage.getCacheContext(doc); const cacheContext = this.thumbsStorage.getCacheContext(doc);

View File

@ -27,7 +27,7 @@ import {MOUNT_CLASS_TO} from '../../config/debug';
import appNavigationController from '../../components/appNavigationController'; import appNavigationController from '../../components/appNavigationController';
import AppPrivateSearchTab from '../../components/sidebarRight/tabs/search'; import AppPrivateSearchTab from '../../components/sidebarRight/tabs/search';
import I18n, {i18n, join, LangPackKey} from '../langPack'; import I18n, {i18n, join, LangPackKey} from '../langPack';
import {ChatFull, ChatInvite, ChatParticipant, ChatParticipants, Message, MessageAction, MessageMedia, SendMessageAction, User, Chat as MTChat, UrlAuthResult} from '../../layer'; import {ChatFull, ChatInvite, ChatParticipant, ChatParticipants, Message, MessageAction, MessageMedia, SendMessageAction, User, Chat as MTChat, UrlAuthResult, WallPaper} from '../../layer';
import PeerTitle from '../../components/peerTitle'; import PeerTitle from '../../components/peerTitle';
import PopupPeer, {PopupPeerCheckboxOptions} from '../../components/popups/peer'; import PopupPeer, {PopupPeerCheckboxOptions} from '../../components/popups/peer';
import blurActiveElement from '../../helpers/dom/blurActiveElement'; import blurActiveElement from '../../helpers/dom/blurActiveElement';
@ -100,6 +100,7 @@ import parseUriParams from '../../helpers/string/parseUriParams';
import getMessageThreadId from './utils/messages/getMessageThreadId'; import getMessageThreadId from './utils/messages/getMessageThreadId';
import findUpTag from '../../helpers/dom/findUpTag'; import findUpTag from '../../helpers/dom/findUpTag';
import {MTAppConfig} from '../mtproto/appConfig'; import {MTAppConfig} from '../mtproto/appConfig';
import PopupForward from '../../components/popups/forward';
export type ChatSavedPosition = { export type ChatSavedPosition = {
mids: number[], mids: number[],
@ -186,10 +187,19 @@ export class AppImManager extends EventListenerBase<{
this.backgroundPromises = {}; this.backgroundPromises = {};
STATE_INIT.settings.themes.forEach((theme) => { STATE_INIT.settings.themes.forEach((theme) => {
if(theme.background.slug) { const themeSettings = theme.settings;
const url = 'assets/img/' + theme.background.slug + '.svg' + (IS_FIREFOX ? '?1' : ''); if(!themeSettings) {
this.backgroundPromises[theme.background.slug] = Promise.resolve(url); return;
} }
const {wallpaper} = themeSettings;
const slug = (wallpaper as WallPaper.wallPaper).slug;
if(!slug) {
return;
}
const url = 'assets/img/' + slug + '.svg' + (IS_FIREFOX ? '?1' : '');
this.backgroundPromises[slug] = Promise.resolve(url);
}); });
this.selectTab(APP_TABS.CHATLIST); this.selectTab(APP_TABS.CHATLIST);
@ -793,6 +803,23 @@ export class AppImManager extends EventListenerBase<{
this.onHashChange(true); this.onHashChange(true);
this.attachKeydownListener(); this.attachKeydownListener();
this.handleAutologinDomains(); this.handleAutologinDomains();
this.checkForShare();
}
private checkForShare() {
const share = apiManagerProxy.share;
if(share) {
apiManagerProxy.share = undefined;
new PopupForward(undefined, async(peerId) => {
await this.setPeer({peerId});
if(share.files?.length) {
const foundMedia = share.files.some((file) => MEDIA_MIME_TYPES_SUPPORTED.has(file.type));
new PopupNewMedia(this.chat, share.files, foundMedia ? 'media' : 'document');
} else {
this.managers.appMessagesManager.sendText(peerId, share.text);
}
});
}
} }
public handleUrlAuth(options: { public handleUrlAuth(options: {
@ -1495,16 +1522,17 @@ export class AppImManager extends EventListenerBase<{
public setCurrentBackground(broadcastEvent = false): ReturnType<AppImManager['setBackground']> { public setCurrentBackground(broadcastEvent = false): ReturnType<AppImManager['setBackground']> {
const theme = themeController.getTheme(); const theme = themeController.getTheme();
if(theme.background.slug) { const slug = (theme.settings?.wallpaper as WallPaper.wallPaper)?.slug;
if(slug) {
const defaultTheme = STATE_INIT.settings.themes.find((t) => t.name === theme.name); const defaultTheme = 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; // slug === defaultslug;
// if(!isDefaultBackground) { // if(!isDefaultBackground) {
return this.getBackground(theme.background.slug).then((url) => { return this.getBackground(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.settings = copy(defaultTheme.settings); // * reset background
return this.setCurrentBackground(true); return this.setCurrentBackground(true);
}); });
// } // }

View File

@ -9,6 +9,10 @@
* https://github.com/zhukov/webogram/blob/master/LICENSE * https://github.com/zhukov/webogram/blob/master/LICENSE
*/ */
import type {ApiFileManager} from '../mtproto/apiFileManager';
import type {MediaSize} from '../../helpers/mediaSize';
import type {Progress} from './appDownloadManager';
import type {VIDEO_MIME_TYPE} from '../../environment/videoMimeTypesSupport';
import LazyLoadQueueBase from '../../components/lazyLoadQueueBase'; import LazyLoadQueueBase from '../../components/lazyLoadQueueBase';
import deferredPromise, {CancellablePromise} from '../../helpers/cancellablePromise'; import deferredPromise, {CancellablePromise} from '../../helpers/cancellablePromise';
import tsNow from '../../helpers/tsNow'; import tsNow from '../../helpers/tsNow';
@ -16,7 +20,6 @@ import {randomLong} from '../../helpers/random';
import {Chat, ChatFull, Dialog as MTDialog, DialogPeer, DocumentAttribute, InputMedia, InputMessage, InputPeerNotifySettings, InputSingleMedia, Message, MessageAction, MessageEntity, MessageFwdHeader, MessageMedia, MessageReplies, MessageReplyHeader, MessagesDialogs, MessagesFilter, MessagesMessages, MethodDeclMap, NotifyPeer, PeerNotifySettings, PhotoSize, SendMessageAction, Update, Photo, Updates, ReplyMarkup, InputPeer, InputPhoto, InputDocument, InputGeoPoint, WebPage, GeoPoint, ReportReason, MessagesGetDialogs, InputChannel, InputDialogPeer, ReactionCount, MessagePeerReaction, MessagesSearchCounter, Peer, MessageReactions, Document, InputFile, Reaction, ForumTopic as MTForumTopic, MessagesForumTopics, MessagesGetReplies, MessagesGetHistory, MessagesAffectedHistory, UrlAuthResult} from '../../layer'; import {Chat, ChatFull, Dialog as MTDialog, DialogPeer, DocumentAttribute, InputMedia, InputMessage, InputPeerNotifySettings, InputSingleMedia, Message, MessageAction, MessageEntity, MessageFwdHeader, MessageMedia, MessageReplies, MessageReplyHeader, MessagesDialogs, MessagesFilter, MessagesMessages, MethodDeclMap, NotifyPeer, PeerNotifySettings, PhotoSize, SendMessageAction, Update, Photo, Updates, ReplyMarkup, InputPeer, InputPhoto, InputDocument, InputGeoPoint, WebPage, GeoPoint, ReportReason, MessagesGetDialogs, InputChannel, InputDialogPeer, ReactionCount, MessagePeerReaction, MessagesSearchCounter, Peer, MessageReactions, Document, InputFile, Reaction, ForumTopic as MTForumTopic, MessagesForumTopics, MessagesGetReplies, MessagesGetHistory, MessagesAffectedHistory, UrlAuthResult} from '../../layer';
import {ArgumentTypes, InvokeApiOptions} from '../../types'; import {ArgumentTypes, InvokeApiOptions} from '../../types';
import {logger, LogTypes} from '../logger'; import {logger, LogTypes} from '../logger';
import type {ApiFileManager} from '../mtproto/apiFileManager';
import {ReferenceContext} from '../mtproto/referenceDatabase'; import {ReferenceContext} from '../mtproto/referenceDatabase';
import DialogsStorage, {GLOBAL_FOLDER_ID} from '../storages/dialogs'; import DialogsStorage, {GLOBAL_FOLDER_ID} from '../storages/dialogs';
import {ChatRights} from './appChatsManager'; import {ChatRights} from './appChatsManager';
@ -36,7 +39,6 @@ import deepEqual from '../../helpers/object/deepEqual';
import splitStringByLength from '../../helpers/string/splitStringByLength'; import splitStringByLength from '../../helpers/string/splitStringByLength';
import debounce from '../../helpers/schedulers/debounce'; import debounce from '../../helpers/schedulers/debounce';
import {AppManager} from './manager'; import {AppManager} from './manager';
import type {MediaSize} from '../../helpers/mediaSize';
import getPhotoMediaInput from './utils/photos/getPhotoMediaInput'; import getPhotoMediaInput from './utils/photos/getPhotoMediaInput';
import getPhotoDownloadOptions from './utils/photos/getPhotoDownloadOptions'; import getPhotoDownloadOptions from './utils/photos/getPhotoDownloadOptions';
import fixEmoji from '../richTextProcessor/fixEmoji'; import fixEmoji from '../richTextProcessor/fixEmoji';
@ -53,7 +55,6 @@ import defineNotNumerableProperties from '../../helpers/object/defineNotNumerabl
import getDocumentMediaInput from './utils/docs/getDocumentMediaInput'; import getDocumentMediaInput from './utils/docs/getDocumentMediaInput';
import getDocumentInputFileName from './utils/docs/getDocumentInputFileName'; import getDocumentInputFileName from './utils/docs/getDocumentInputFileName';
import getFileNameForUpload from '../../helpers/getFileNameForUpload'; import getFileNameForUpload from '../../helpers/getFileNameForUpload';
import type {Progress} from './appDownloadManager';
import noop from '../../helpers/noop'; import noop from '../../helpers/noop';
import appTabsManager from './appTabsManager'; import appTabsManager from './appTabsManager';
import MTProtoMessagePort from '../mtproto/mtprotoMessagePort'; import MTProtoMessagePort from '../mtproto/mtprotoMessagePort';
@ -817,7 +818,7 @@ export class AppMessagesManager extends AppManager {
cacheContext.url = options.objectURL || ''; cacheContext.url = options.objectURL || '';
photo = this.appPhotosManager.savePhoto(photo); photo = this.appPhotosManager.savePhoto(photo);
} else if(getEnvironment().VIDEO_MIME_TYPES_SUPPORTED.has(fileType)) { } else if(getEnvironment().VIDEO_MIME_TYPES_SUPPORTED.has(fileType as VIDEO_MIME_TYPE)) {
attachType = 'video'; attachType = 'video';
apiFileName = 'video.mp4'; apiFileName = 'video.mp4';
actionName = 'sendMessageUploadVideoAction'; actionName = 'sendMessageUploadVideoAction';
@ -5624,7 +5625,7 @@ export class AppMessagesManager extends AppManager {
return chatPeerIds[chatPeerIds.length - 1] === peerId; return chatPeerIds[chatPeerIds.length - 1] === peerId;
}); });
if(!tab) { if(!tab && tabs.length) {
tabs.sort((a, b) => a.state.idleStartTime - b.state.idleStartTime); tabs.sort((a, b) => a.state.idleStartTime - b.state.idleStartTime);
tab = !tabs[0].state.idleStartTime ? tabs[0] : tabs[tabs.length - 1]; tab = !tabs[0].state.idleStartTime ? tabs[0] : tabs[tabs.length - 1];
} }
@ -5633,7 +5634,7 @@ export class AppMessagesManager extends AppManager {
port.invokeVoid('notificationBuild', { port.invokeVoid('notificationBuild', {
message, message,
...options ...options
}, tab.source); }, tab?.source);
} }
public getScheduledMessagesStorage(peerId: PeerId) { public getScheduledMessagesStorage(peerId: PeerId) {
@ -6393,7 +6394,7 @@ export class AppMessagesManager extends AppManager {
} }
} }
return unreadCount || +!!(dialog as Dialog).pFlags.unread_mark; return unreadCount || +!!(dialog as Dialog).pFlags?.unread_mark;
} }
public isDialogUnread(dialog: Dialog | ForumTopic) { public isDialogUnread(dialog: Dialog | ForumTopic) {

View File

@ -6,7 +6,7 @@
import App from '../../../../config/app'; import App from '../../../../config/app';
import DEBUG from '../../../../config/debug'; import DEBUG from '../../../../config/debug';
import {AutoDownloadPeerTypeSettings, State, STATE_INIT} from '../../../../config/state'; import {AutoDownloadPeerTypeSettings, State, STATE_INIT, Background, AppTheme} from '../../../../config/state';
import compareVersion from '../../../../helpers/compareVersion'; import compareVersion from '../../../../helpers/compareVersion';
import copy from '../../../../helpers/object/copy'; import copy from '../../../../helpers/object/copy';
import validateInitObject from '../../../../helpers/object/validateInitObject'; import validateInitObject from '../../../../helpers/object/validateInitObject';
@ -18,6 +18,7 @@ import {recordPromiseBound} from '../../../../helpers/recordPromise';
// import RESET_STORAGES_PROMISE from "../storages/resetStoragesPromise"; // import RESET_STORAGES_PROMISE from "../storages/resetStoragesPromise";
import {StoragesResults} from '../storages/loadStorages'; import {StoragesResults} from '../storages/loadStorages';
import {logger} from '../../../logger'; import {logger} from '../../../logger';
import {WallPaper} from '../../../../layer';
const REFRESH_EVERY = 24 * 60 * 60 * 1000; // 1 day const REFRESH_EVERY = 24 * 60 * 60 * 1000; // 1 day
// const REFRESH_EVERY = 1e3; // const REFRESH_EVERY = 1e3;
@ -245,22 +246,6 @@ async function loadStateInner() {
// state = this.state = new Proxy(state, getHandler()); // state = this.state = new Proxy(state, getHandler());
// * support old version
if(!state.settings.hasOwnProperty('theme') && state.settings.hasOwnProperty('nightTheme')) {
state.settings.theme = state.settings.nightTheme ? 'night' : 'day';
pushToState('settings', state.settings);
}
// * support old version
if(!state.settings.hasOwnProperty('themes') && state.settings.background) {
state.settings.themes = copy(STATE_INIT.settings.themes);
const theme = state.settings.themes.find((t) => t.name === state.settings.theme);
if(theme) {
theme.background = state.settings.background;
pushToState('settings', state.settings);
}
}
// * migrate auto download settings // * migrate auto download settings
const autoDownloadSettings = state.settings.autoDownload; const autoDownloadSettings = state.settings.autoDownload;
if(autoDownloadSettings?.private !== undefined) { if(autoDownloadSettings?.private !== undefined) {
@ -291,9 +276,12 @@ async function loadStateInner() {
pushToState('settings', state.settings); pushToState('settings', state.settings);
} }
const SKIP_VALIDATING_PATHS: Set<string> = new Set([
'settings.themes'
]);
validateInitObject(STATE_INIT, state, (missingKey) => { validateInitObject(STATE_INIT, state, (missingKey) => {
pushToState(missingKey as keyof State, state[missingKey as keyof State]); pushToState(missingKey as keyof State, state[missingKey as keyof State]);
}); }, undefined, SKIP_VALIDATING_PATHS);
let newVersion: string, oldVersion: string; let newVersion: string, oldVersion: string;
if(state.version !== STATE_VERSION || state.build !== BUILD/* || true */) { if(state.version !== STATE_VERSION || state.build !== BUILD/* || true */) {
@ -306,26 +294,83 @@ async function loadStateInner() {
resetStorages.add('dialogs'); resetStorages.add('dialogs');
} }
// * migrate backgrounds (March 13, 2022; to version 1.3.0) if(compareVersion(state.version, '1.7.1') === -1) {
if(compareVersion(state.version, '1.3.0') === -1) {
let migrated = false; let migrated = false;
state.settings.themes.forEach((theme, idx, arr) => { // * migrate backgrounds (March 13, 2022; to version 1.3.0)
if(( if(compareVersion(state.version, '1.3.0') === -1) {
theme.name === 'day' && migrated = true;
theme.background.slug === 'ByxGo2lrMFAIAAAAmkJxZabh8eM' && state.settings.theme = copy(STATE_INIT.settings.theme);
theme.background.type === 'image' state.settings.themes = copy(STATE_INIT.settings.themes);
) || ( } else if(compareVersion(state.version, '1.7.1') === -1) { // * migrate backgrounds (January 25th, 2023; to version 1.7.1)
theme.name === 'night' && migrated = true;
theme.background.color === '#0f0f0f' && const oldThemes = state.settings.themes as any as Array<{
theme.background.type === 'color' name: AppTheme['name'],
)) { background: Background
const newTheme = STATE_INIT.settings.themes.find((newTheme) => newTheme.name === theme.name); }>;
if(newTheme) {
arr[idx] = copy(newTheme); state.settings.themes = copy(STATE_INIT.settings.themes);
migrated = true;
} try {
oldThemes.forEach((oldTheme) => {
const oldBackground = oldTheme.background;
if(!oldBackground) {
return;
}
const newTheme = state.settings.themes.find((t) => t.name === oldTheme.name);
newTheme.settings.highlightningColor = oldBackground.highlightningColor;
const getColorFromHex = (hex: string) => hex && parseInt(hex.slice(1), 16);
const colors = (oldBackground.color || '').split(',').map(getColorFromHex);
if(oldBackground.color && !oldBackground.slug) {
newTheme.settings.wallpaper = {
_: 'wallPaperNoFile',
id: 0,
pFlags: {},
settings: {
_: 'wallPaperSettings',
pFlags: {}
}
};
} else {
const wallPaper: WallPaper.wallPaper = {
_: 'wallPaper',
id: 0,
access_hash: 0,
slug: oldBackground.slug,
document: {} as any,
pFlags: {},
settings: {
_: 'wallPaperSettings',
pFlags: {}
}
};
const wallPaperSettings = wallPaper.settings;
newTheme.settings.wallpaper = wallPaper;
if(oldBackground.slug && !oldBackground.color) {
wallPaperSettings.pFlags.blur = oldBackground.blur || undefined;
} else if(oldBackground.intensity) {
wallPaperSettings.intensity = oldBackground.intensity;
wallPaper.pFlags.pattern = true;
wallPaper.pFlags.dark = oldBackground.intensity < 0 || undefined;
}
}
if(colors.length) {
const wallPaperSettings = newTheme.settings.wallpaper.settings;
wallPaperSettings.background_color = colors[0];
wallPaperSettings.second_background_color = colors[1];
wallPaperSettings.third_background_color = colors[2];
wallPaperSettings.fourth_background_color = colors[3];
}
});
} catch(err) {
console.error('migrating themes error', err);
} }
}); }
if(migrated) { if(migrated) {
pushToState('settings', state.settings); pushToState('settings', state.settings);

View File

@ -771,9 +771,9 @@ export class ApiFileManager extends AppManager {
if(isDocument && !thumb) { if(isDocument && !thumb) {
this.rootScope.dispatchEvent('document_downloading', (media as Document.document).id); this.rootScope.dispatchEvent('document_downloading', (media as Document.document).id);
promise.catch(noop).finally(() => { promise.then(() => {
this.rootScope.dispatchEvent('document_downloaded', (media as Document.document).id); this.rootScope.dispatchEvent('document_downloaded', (media as Document.document).id);
}); }).catch(noop);
} }
} }

View File

@ -62,6 +62,8 @@ class ApiManagerProxy extends MTProtoMessagePort {
private tabState: TabState; private tabState: TabState;
public share: ShareData;
public serviceMessagePort: ServiceMessagePort<true>; public serviceMessagePort: ServiceMessagePort<true>;
private lastServiceWorker: ServiceWorker; private lastServiceWorker: ServiceWorker;
@ -308,6 +310,11 @@ class ApiManagerProxy extends MTProtoMessagePort {
hello: (payload, source) => { hello: (payload, source) => {
this.serviceMessagePort.resendLockTask(source); this.serviceMessagePort.resendLockTask(source);
},
share: (payload) => {
this.log('will try to share something');
this.share = payload;
} }
}); });
// #endif // #endif

View File

@ -30,6 +30,8 @@ export type PushSubscriptionNotify = {
tokenValue: string tokenValue: string
}; };
const PING_PUSH_INTERVAL = 10000;
export class WebPushApiManager extends EventListenerBase<{ export class WebPushApiManager extends EventListenerBase<{
push_notification_click: (n: PushNotificationObject) => void, push_notification_click: (n: PushNotificationObject) => void,
push_init: (n: PushSubscriptionNotify) => void, push_init: (n: PushSubscriptionNotify) => void,
@ -187,7 +189,7 @@ export class WebPushApiManager extends EventListenerBase<{
settings: this.settings settings: this.settings
}); });
this.isAliveTO = setTimeout(this.isAliveNotify, 10000); this.isAliveTO = setTimeout(this.isAliveNotify, PING_PUSH_INTERVAL);
} }
public setSettings(newSettings: WebPushApiManager['settings']) { public setSettings(newSettings: WebPushApiManager['settings']) {

View File

@ -151,11 +151,7 @@ export class LottieLoader {
]).then(() => player); ]).then(() => player);
} }
public async loadAnimationWorker( public async loadAnimationWorker(params: RLottieOptions): Promise<RLottiePlayer> {
params: RLottieOptions,
group: AnimationItemGroup = params.group || '',
middleware?: () => boolean
): Promise<RLottiePlayer> {
if(!IS_WEB_ASSEMBLY_SUPPORTED) { if(!IS_WEB_ASSEMBLY_SUPPORTED) {
return this.loadPromise as any; return this.loadPromise as any;
} }
@ -164,6 +160,7 @@ export class LottieLoader {
await this.loadLottieWorkers(); await this.loadLottieWorkers();
} }
const {middleware, group = ''} = params;
if(middleware && !middleware()) { if(middleware && !middleware()) {
throw makeError('MIDDLEWARE'); throw makeError('MIDDLEWARE');
} }
@ -190,7 +187,7 @@ export class LottieLoader {
const player = this.initPlayer(containers, params); const player = this.initPlayer(containers, params);
animationIntersector.addAnimation(player, group); animationIntersector.addAnimation(player, group, undefined, middleware);
return player; return player;
} }

View File

@ -5,6 +5,7 @@
*/ */
import type {AnimationItemGroup, AnimationItemWrapper} from '../../components/animationIntersector'; import type {AnimationItemGroup, AnimationItemWrapper} from '../../components/animationIntersector';
import type {Middleware} from '../../helpers/middleware';
import CAN_USE_TRANSFERABLES from '../../environment/canUseTransferables'; import CAN_USE_TRANSFERABLES from '../../environment/canUseTransferables';
import IS_APPLE_MX from '../../environment/appleMx'; import IS_APPLE_MX from '../../environment/appleMx';
import {IS_ANDROID, IS_APPLE_MOBILE, IS_APPLE, IS_SAFARI} from '../../environment/userAgent'; import {IS_ANDROID, IS_APPLE_MOBILE, IS_APPLE, IS_SAFARI} from '../../environment/userAgent';
@ -17,6 +18,7 @@ import framesCache, {FramesCache, FramesCacheItem} from '../../helpers/framesCac
export type RLottieOptions = { export type RLottieOptions = {
container: HTMLElement | HTMLElement[], container: HTMLElement | HTMLElement[],
middleware?: Middleware,
canvas?: HTMLCanvasElement, canvas?: HTMLCanvasElement,
autoplay?: boolean, autoplay?: boolean,
animationData: Blob, animationData: Blob,

View File

@ -19,6 +19,7 @@ import listenMessagePort from '../../helpers/listenMessagePort';
import {getWindowClients} from '../../helpers/context'; import {getWindowClients} from '../../helpers/context';
import {MessageSendPort} from '../mtproto/superMessagePort'; import {MessageSendPort} from '../mtproto/superMessagePort';
import handleDownload from './download'; import handleDownload from './download';
import onShareFetch, {checkWindowClientForDeferredShare} from './share';
export const log = logger('SW', LogTypes.Error | LogTypes.Debug | LogTypes.Log | LogTypes.Warn, true); export const log = logger('SW', LogTypes.Error | LogTypes.Debug | LogTypes.Log | LogTypes.Warn, true);
const ctx = self as any as ServiceWorkerGlobalScope; const ctx = self as any as ServiceWorkerGlobalScope;
@ -52,6 +53,8 @@ const onWindowConnected = (source: WindowClient) => {
serviceMessagePort.invokeVoid('hello', undefined, source); serviceMessagePort.invokeVoid('hello', undefined, source);
sendMessagePortIfNeeded(source); sendMessagePortIfNeeded(source);
connectedWindows.set(source.id, source); connectedWindows.set(source.id, source);
checkWindowClientForDeferredShare(source);
}; };
export const serviceMessagePort = new ServiceMessagePort<false>(); export const serviceMessagePort = new ServiceMessagePort<false>();
@ -137,6 +140,11 @@ const onFetch = (event: FetchEvent): void => {
break; break;
} }
case 'share': {
onShareFetch(event, params);
break;
}
case 'ping': { case 'ping': {
event.respondWith(new Response('pong')); event.respondWith(new Response('pong'));
break; break;

View File

@ -11,7 +11,7 @@
import {Database} from '../../config/databases'; import {Database} from '../../config/databases';
import DATABASE_STATE from '../../config/databases/state'; import DATABASE_STATE from '../../config/databases/state';
import {IS_FIREFOX} from '../../environment/userAgent'; import {IS_FIREFOX, IS_MOBILE} from '../../environment/userAgent';
import deepEqual from '../../helpers/object/deepEqual'; import deepEqual from '../../helpers/object/deepEqual';
import IDBStorage from '../files/idb'; import IDBStorage from '../files/idb';
import {log, serviceMessagePort} from './index.service'; import {log, serviceMessagePort} from './index.service';
@ -20,6 +20,11 @@ import {ServicePushPingTaskPayload} from './serviceMessagePort';
const ctx = self as any as ServiceWorkerGlobalScope; const ctx = self as any as ServiceWorkerGlobalScope;
const defaultBaseUrl = location.protocol + '//' + location.hostname + location.pathname.split('/').slice(0, -1).join('/') + '/'; const defaultBaseUrl = location.protocol + '//' + location.hostname + location.pathname.split('/').slice(0, -1).join('/') + '/';
// as in webPushApiManager.ts
const PING_PUSH_TIMEOUT = 10000 + 1500;
let lastPingTime = 0;
let localNotificationsAvailable = !IS_MOBILE;
export type PushNotificationObject = { export type PushNotificationObject = {
loc_key: string, loc_key: string,
loc_args: string[], loc_args: string[],
@ -29,7 +34,8 @@ export type PushNotificationObject = {
chat_id?: string, // should be number chat_id?: string, // should be number
from_id?: string, // should be number from_id?: string, // should be number
msg_id: string, msg_id: string,
peerId?: string // should be number peerId?: string, // should be number
silent?: string // can be '1'
}, },
sound?: string, sound?: string,
random_id: number, random_id: number,
@ -37,6 +43,7 @@ export type PushNotificationObject = {
description: string, description: string,
mute: string, // should be number mute: string, // should be number
title: string, title: string,
message?: string,
action?: 'mute1d' | 'push_settings', // will be set before postMessage to main thread action?: 'mute1d' | 'push_settings', // will be set before postMessage to main thread
}; };
@ -55,28 +62,35 @@ class SomethingGetter<T extends Database<any>, Storage extends Record<string, an
this.storage = new IDBStorage<T>(db, storeName); this.storage = new IDBStorage<T>(db, storeName);
} }
public async get<T extends keyof Storage>(key: T) { private getDefault<T extends keyof Storage>(key: T) {
if(this.cache[key] !== undefined) { const callback = this.defaults[key];
return typeof(callback) === 'function' ? callback() : callback;
}
public get<T extends keyof Storage>(key: T) {
if(this.cache.hasOwnProperty(key)) {
return this.cache[key]; return this.cache[key];
} }
let value: Storage[T]; const promise = this.storage.get(key as string) as Promise<Storage[T]>;
try { return promise.then((value) => value, () => undefined as Storage[T]).then((value) => {
value = await this.storage.get(key as string); if(this.cache.hasOwnProperty(key)) {
} catch(err) { return this.cache[key];
}
value ??= this.getDefault(key);
return this.cache[key] = value;
});
}
public getCached<T extends keyof Storage>(key: T) {
const value = this.get(key);
if(value instanceof Promise) {
throw 'no property';
} }
if(this.cache[key] !== undefined) { return value;
return this.cache[key];
}
if(value === undefined) {
const callback = this.defaults[key];
value = typeof(callback) === 'function' ? callback() : callback;
}
return this.cache[key] = value;
} }
public async set<T extends keyof Storage>(key: T, value: Storage[T]) { public async set<T extends keyof Storage>(key: T, value: Storage[T]) {
@ -101,7 +115,7 @@ type PushStorage = {
push_settings: Partial<ServicePushPingTaskPayload['settings']> push_settings: Partial<ServicePushPingTaskPayload['settings']>
}; };
const getter = new SomethingGetter<typeof DATABASE_STATE, PushStorage>(DATABASE_STATE, 'session', { const defaults: PushStorage = {
push_mute_until: 0, push_mute_until: 0,
push_lang: { push_lang: {
push_message_nopreview: 'You have a new message', push_message_nopreview: 'You have a new message',
@ -109,67 +123,49 @@ const getter = new SomethingGetter<typeof DATABASE_STATE, PushStorage>(DATABASE_
push_action_settings: 'Settings' push_action_settings: 'Settings'
}, },
push_settings: {} push_settings: {}
}); };
const getter = new SomethingGetter<typeof DATABASE_STATE, PushStorage>(DATABASE_STATE, 'session', defaults);
// fill cache
for(const i in defaults) {
getter.get(i as keyof PushStorage);
}
ctx.addEventListener('push', (event) => { ctx.addEventListener('push', (event) => {
const obj: PushNotificationObject = event.data.json(); const obj: PushNotificationObject = event.data.json();
log('push', obj); log('push', {...obj});
let hasActiveWindows = false; try {
const checksPromise = Promise.all([ if(!obj.badge) {
getter.get('push_mute_until'), throw 'no badge';
ctx.clients.matchAll({type: 'window'}) }
]).then((result) => {
const [muteUntil, clientList] = result;
log('matched clients', clientList); const [muteUntil, settings, lang] = [
hasActiveWindows = clientList.length > 0; getter.getCached('push_mute_until'),
getter.getCached('push_settings'),
getter.getCached('push_lang')
];
const nowTime = Date.now();
if(
userInvisibleIsSupported() &&
muteUntil &&
nowTime < muteUntil
) {
throw `Supress notification because mute for ${Math.ceil((muteUntil - nowTime) / 60000)} min`;
}
const hasActiveWindows = (Date.now() - lastPingTime) <= PING_PUSH_TIMEOUT && localNotificationsAvailable;
if(hasActiveWindows) { if(hasActiveWindows) {
throw 'Supress notification because some instance is alive'; throw 'Supress notification because some instance is alive';
} }
const nowTime = Date.now(); const notificationPromise = fireNotification(obj, settings, lang);
if(userInvisibleIsSupported() && event.waitUntil(notificationPromise);
muteUntil && } catch(err) {
nowTime < muteUntil) { log(err);
throw `Supress notification because mute for ${Math.ceil((muteUntil - nowTime) / 60000)} min`; }
}
if(!obj.badge) {
throw 'No badge?';
}
});
checksPromise.catch((reason) => {
log(reason);
});
const notificationPromise = checksPromise.then(() => {
return Promise.all([getter.get('push_settings'), getter.get('push_lang')])
}).then((result) => {
return fireNotification(obj, result[0], result[1]);
});
const closePromise = notificationPromise.catch(() => {
log('Closing all notifications on push', hasActiveWindows);
if(userInvisibleIsSupported() || hasActiveWindows) {
return closeAllNotifications();
}
return ctx.registration.showNotification('Telegram', {
tag: 'unknown_peer'
}).then(() => {
if(hasActiveWindows) {
return closeAllNotifications();
}
setTimeout(() => closeAllNotifications(), hasActiveWindows ? 0 : 100);
}).catch((error) => {
log.error('Show notification error', error);
});
});
event.waitUntil(closePromise);
}); });
ctx.addEventListener('notificationclick', (event) => { ctx.addEventListener('notificationclick', (event) => {
@ -205,7 +201,7 @@ ctx.addEventListener('notificationclick', (event) => {
} }
if(ctx.clients.openWindow) { if(ctx.clients.openWindow) {
return getter.get('push_settings').then((settings) => { return Promise.resolve(getter.get('push_settings')).then((settings) => {
return ctx.clients.openWindow(settings.baseUrl || defaultBaseUrl); return ctx.clients.openWindow(settings.baseUrl || defaultBaseUrl);
}); });
} }
@ -236,7 +232,7 @@ function removeFromNotifications(notification: Notification) {
notifications.delete(notification); notifications.delete(notification);
} }
export function closeAllNotifications() { export function closeAllNotifications(tag?: string) {
for(const notification of notifications) { for(const notification of notifications) {
try { try {
notification.close(); notification.close();
@ -245,7 +241,7 @@ export function closeAllNotifications() {
let promise: Promise<void>; let promise: Promise<void>;
if('getNotifications' in ctx.registration) { if('getNotifications' in ctx.registration) {
promise = ctx.registration.getNotifications({}).then((notifications) => { promise = ctx.registration.getNotifications({tag}).then((notifications) => {
for(let i = 0, len = notifications.length; i < len; ++i) { for(let i = 0, len = notifications.length; i < len; ++i) {
try { try {
notifications[i].close(); notifications[i].close();
@ -269,6 +265,7 @@ function userInvisibleIsSupported() {
function fireNotification(obj: PushNotificationObject, settings: PushStorage['push_settings'], lang: PushStorage['push_lang']) { function fireNotification(obj: PushNotificationObject, settings: PushStorage['push_settings'], lang: PushStorage['push_lang']) {
const icon = 'assets/img/logo_filled_rounded.png'; const icon = 'assets/img/logo_filled_rounded.png';
const badge = 'assets/img/masked.svg';
let title = obj.title || 'Telegram'; let title = obj.title || 'Telegram';
let body = obj.description || ''; let body = obj.description || '';
let peerId: string; let peerId: string;
@ -286,7 +283,7 @@ function fireNotification(obj: PushNotificationObject, settings: PushStorage['pu
obj.custom.peerId = '' + peerId; obj.custom.peerId = '' + peerId;
let tag = 'peer' + peerId; let tag = 'peer' + peerId;
if(settings && settings.nopreview) { if(settings?.nopreview) {
title = 'Telegram'; title = 'Telegram';
body = lang.push_message_nopreview; body = lang.push_message_nopreview;
tag = 'unknown_peer'; tag = 'unknown_peer';
@ -307,21 +304,20 @@ function fireNotification(obj: PushNotificationObject, settings: PushStorage['pu
icon, icon,
tag, tag,
data: obj, data: obj,
actions actions,
badge,
silent: obj.custom.silent === '1'
}); });
return notificationPromise.then((event) => { return notificationPromise.catch((error) => {
// @ts-ignore
if(event?.notification) {
// @ts-ignore
pushToNotifications(event.notification);
}
}).catch((error) => {
log.error('Show notification promise', error); log.error('Show notification promise', error);
}); });
} }
export function onPing(payload: ServicePushPingTaskPayload, source?: MessageEventSource) { export function onPing(payload: ServicePushPingTaskPayload, source?: MessageEventSource) {
lastPingTime = Date.now();
localNotificationsAvailable = payload.localNotifications;
if(pendingNotification && source) { if(pendingNotification && source) {
serviceMessagePort.invokeVoid('pushClick', pendingNotification, source); serviceMessagePort.invokeVoid('pushClick', pendingNotification, source);
pendingNotification = undefined; pendingNotification = undefined;

View File

@ -52,6 +52,7 @@ export default class ServiceMessagePort<Master extends boolean = false> extends
// to main thread // to main thread
pushClick: (payload: PushNotificationObject) => void, pushClick: (payload: PushNotificationObject) => void,
hello: (payload: void, source: MessageEventSource) => void, hello: (payload: void, source: MessageEventSource) => void,
share: (payload: ShareData) => void,
// to mtproto worker // to mtproto worker
requestFilePart: (payload: ServiceRequestFilePartTaskPayload) => Promise<MyUploadFile> | MyUploadFile requestFilePart: (payload: ServiceRequestFilePartTaskPayload) => Promise<MyUploadFile> | MyUploadFile

View File

@ -0,0 +1,52 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import {log, serviceMessagePort} from './index.service';
const deferred: {[id: string]: ShareData[]} = {};
function parseFormData(formData: FormData): ShareData {
return {
files: formData.getAll('files') as File[],
title: formData.get('title') as string,
text: formData.get('text') as string,
url: formData.get('url') as string
};
}
async function processShareEvent(formData: FormData, clientId: string) {
try {
log('share data', formData);
const data = parseFormData(formData);
(deferred[clientId] ??= []).push(data);
} catch(err) {
log.warn('something wrong with the data', err);
}
};
export function checkWindowClientForDeferredShare(windowClient: WindowClient) {
const arr = deferred[windowClient.id];
if(!arr) {
return;
}
delete deferred[windowClient.id];
log('releasing share events to client:', windowClient.id, 'length:', arr.length);
arr.forEach((data) => {
serviceMessagePort.invokeVoid('share', data, windowClient);
});
}
export default function onShareFetch(event: FetchEvent, params: string) {
const promise = event.request.formData()
.then((formData) => {
processShareEvent(formData, event.resultingClientId)
return Response.redirect('..');
});
event.respondWith(promise);
}

View File

@ -342,41 +342,37 @@ export default class FiltersStorage extends AppManager {
flags, flags,
id: filter.id, id: filter.id,
filter: remove ? undefined : this.getOutputDialogFilter(filter) filter: remove ? undefined : this.getOutputDialogFilter(filter)
}).then((bool: boolean) => { // возможно нужна проверка и откат, если результат не ТРУ }).then((bool) => { // возможно нужна проверка и откат, если результат не ТРУ
// console.log('updateDialogFilter bool:', bool); // console.log('updateDialogFilter bool:', bool);
if(bool) { /* if(!this.filters[filter.id]) {
/* if(!this.filters[filter.id]) { this.saveDialogFilter(filter);
this.saveDialogFilter(filter);
}
rootScope.$broadcast('filter_update', filter); */
this.onUpdateDialogFilter({
_: 'updateDialogFilter',
id: filter.id,
filter: remove ? undefined : filter as any
});
if(prepend) {
const f: MyDialogFilter[] = [];
for(const filterId in this.filters) {
const filter = this.filters[filterId];
++filter.localId;
f.push(filter);
}
filter.localId = START_LOCAL_ID;
const order = f.sort((a, b) => a.localId - b.localId).map((filter) => filter.id);
this.onUpdateDialogFilterOrder({
_: 'updateDialogFilterOrder',
order
});
}
} }
return bool; rootScope.$broadcast('filter_update', filter); */
this.onUpdateDialogFilter({
_: 'updateDialogFilter',
id: filter.id,
filter: remove ? undefined : filter as any
});
if(prepend) {
const f: MyDialogFilter[] = [];
for(const filterId in this.filters) {
const filter = this.filters[filterId];
++filter.localId;
f.push(filter);
}
filter.localId = START_LOCAL_ID;
const order = f.sort((a, b) => a.localId - b.localId).map((filter) => filter.id);
this.onUpdateDialogFilterOrder({
_: 'updateDialogFilterOrder',
order
});
}
}); });
} }

View File

@ -250,6 +250,10 @@ $btn-menu-z-index: 4;
margin-inline: .3125rem; margin-inline: .3125rem;
font-weight: 500; font-weight: 500;
transform: scale(1); transform: scale(1);
.tgico-char {
width: var(--icon-size);
}
@include animation-level(2) { @include animation-level(2) {
transition: transform var(--btn-menu-transition); transition: transform var(--btn-menu-transition);

View File

@ -177,47 +177,6 @@ $chat-input-border-radius: 1rem;
//right: var(--chat-input-padding); //right: var(--chat-input-padding);
} }
.input-message-input {
--custom-emoji-size: var(--messages-custom-emoji-size);
background: none;
border: none;
width: 100%;
padding: .5rem .5625rem;
/* height: 100%; */
margin-top: -1px;
max-height: calc(30rem - 2.5rem); // 2.5rem - input helper (reply)
//min-height: inherit;
overflow-y: none;
resize: none;
border: none;
outline: none;
font-size: var(--messages-text-size);
line-height: var(--line-height);
pre {
display: inline;
margin: 0;
}
@include animation-level(2) {
transition: height $input-half-transition-time;
}
@media only screen and (max-height: 30rem) {
max-height: unquote('max(36px, calc(100vh - 10rem))');
}
@include respond-to(handhelds) {
max-height: 10rem;
}
&[data-inline-placeholder]:after {
content: attr(data-inline-placeholder);
color: #a2acb4;
pointer-events: none;
}
}
.toggle-emoticons { .toggle-emoticons {
&:before { &:before {
content: $tgico-smile; content: $tgico-smile;
@ -1297,6 +1256,25 @@ $chat-input-border-radius: 1rem;
} }
} */ } */
.input-message-input {
margin-top: -1px;
max-height: calc(30rem - 2.5rem) !important; // 2.5rem - input helper (reply)
@media only screen and (max-height: 30rem) {
max-height: unquote('max(36px, calc(100vh - 10rem))');
}
@include respond-to(handhelds) {
max-height: 10rem;
}
&[data-inline-placeholder]:after {
content: attr(data-inline-placeholder);
color: #a2acb4;
pointer-events: none;
}
}
.new-message-wrapper { .new-message-wrapper {
--send-as-size: 1.875rem; --send-as-size: 1.875rem;
--send-as-margin-left: .25rem; --send-as-margin-left: .25rem;
@ -1502,22 +1480,6 @@ $chat-input-border-radius: 1rem;
} }
} }
.input-message-container {
width: 1%;
max-height: inherit;
flex: 1 1 auto;
position: relative;
overflow: hidden;
align-self: center;
min-height: calc(var(--chat-input-size) - var(--padding-vertical) * 2);
display: flex;
align-items: center;
> .scrollable {
position: relative;
}
}
.btn-icon { .btn-icon {
flex: 0 0 auto; flex: 0 0 auto;
font-size: 1.5rem; font-size: 1.5rem;
@ -1536,6 +1498,47 @@ $chat-input-border-radius: 1rem;
} }
} }
.input-message-container {
width: 1%;
max-height: inherit;
flex: 1 1 auto;
position: relative;
overflow: hidden;
align-self: center;
min-height: calc(var(--chat-input-size) - var(--padding-vertical) * 2);
display: flex;
align-items: center;
.scrollable {
position: relative;
}
}
.input-message-input {
--custom-emoji-size: var(--messages-custom-emoji-size);
background: none;
border: none;
width: 100%;
padding: .5rem .5625rem;
/* height: 100%; */
//min-height: inherit;
overflow-y: none;
resize: none;
border: none;
outline: none;
font-size: var(--messages-text-size);
line-height: var(--line-height);
pre {
display: inline;
margin: 0;
}
@include animation-level(2) {
transition: height $input-half-transition-time;
}
}
.bubbles { .bubbles {
--translateY: 0; --translateY: 0;
width: 100%; width: 100%;

View File

@ -1952,6 +1952,11 @@ $bubble-border-radius-big: 12px;
white-space: nowrap; white-space: nowrap;
height: var(--messages-time-text-size); // * as font-size height: var(--messages-time-text-size); // * as font-size
visibility: visible; visibility: visible;
color: var(--message-time-color);
&:after {
color: var(--message-status-color);
}
} }
.tgico-pinnedchat { .tgico-pinnedchat {
@ -2599,7 +2604,6 @@ $bubble-border-radius-big: 12px;
padding-right: 8px; padding-right: 8px;
.inner { .inner {
color: var(--secondary-text-color);
margin-bottom: 4px; margin-bottom: 4px;
} }
} }
@ -2670,13 +2674,15 @@ $bubble-border-radius-big: 12px;
.bubble.is-out { .bubble.is-out {
flex-direction: row-reverse; flex-direction: row-reverse;
--message-background-color: var(--message-out-background-color); --message-background-color: var(--light-filled-message-out-primary-color);
--light-message-background-color: var(--light-message-out-background-color); --light-message-background-color: var(--light-message-out-primary-color);
--dark-message-background-color: var(--dark-message-out-background-color); --dark-message-background-color: var(--dark-message-out-primary-color);
--link-color: var(--message-out-link-color); --link-color: var(--message-out-link-color);
--message-primary-color: var(--message-out-primary-color); --message-primary-color: var(--message-out-primary-color);
--light-filled-message-primary-color: var(--light-filled-message-out-primary-color); --light-filled-message-primary-color: var(--light-filled-message-out-primary-color);
--selection-background-color: var(--message-out-selection-background-color); --selection-background-color: var(--message-out-selection-background-color);
--message-time-color: var(--message-out-time-color);
--message-status-color: var(--message-out-status-color);
.bubble-content { .bubble-content {
margin-left: auto; margin-left: auto;
@ -2801,17 +2807,15 @@ $bubble-border-radius-big: 12px;
margin-left: -4px; margin-left: -4px;
.inner { .inner {
color: var(--message-out-status-color);
bottom: 4px; bottom: 4px;
} }
&:after, &:after,
.inner:after { .inner:after {
font-size: calc(var(--messages-text-size) + 3px); font-size: calc(var(--messages-text-size) + 3px);
//vertical-align: middle; //vertical-align: middle;
margin-left: 1px; margin-left: 1px;
line-height: var(--messages-time-text-size); // of message line-height: var(--messages-time-text-size); // of message
color: var(--message-out-primary-color);
} }
} }
@ -2973,7 +2977,7 @@ $bubble-border-radius-big: 12px;
&-answer-selected { &-answer-selected {
background-color: var(--message-out-primary-color); background-color: var(--message-out-primary-color);
color: var(--message-out-background-color); color: var(--light-filled-message-out-primary-color);
} }
html.no-touch &-answer:hover { html.no-touch &-answer:hover {

View File

@ -194,10 +194,12 @@
.document, .document,
.audio { .audio {
--padding: 0px;
--icon-size: 3.375rem; --icon-size: 3.375rem;
--icon-margin: .875rem; --icon-margin: .875rem;
--padding-left: calc(var(--icon-size) + var(--icon-margin)); --padding-left: calc(var(--icon-size) + var(--icon-margin) + var(--padding));
padding-left: var(--padding-left); padding: var(--padding);
padding-inline-start: var(--padding-left);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
@ -209,7 +211,7 @@
&-download { &-download {
position: absolute; position: absolute;
// left: 0; // left: 0;
margin-left: calc(var(--padding-left) * -1); margin-inline-start: calc((var(--padding-left) - var(--padding)) * -1);
width: var(--icon-size); width: var(--icon-size);
height: var(--icon-size); height: var(--icon-size);
color: #fff; color: #fff;

View File

@ -1116,7 +1116,7 @@
} }
.background-container { .background-container {
.grid { .background {
&-item { &-item {
&:after { &:after {
content: " "; content: " ";
@ -1144,13 +1144,32 @@
&-media { &-media {
transition: transform .2s ease-in-out; transition: transform .2s ease-in-out;
transform: scale(1); transform: scale(1);
}
}
&.is-pattern { .preloader-container {
background-color: #000; z-index: 1;
}
.media-photo { }
mix-blend-mode: overlay; }
}
.background {
&-item {
cursor: pointer;
&-media {
border-radius: inherit;
&.is-pattern {
background-color: #000;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
.media-photo {
mix-blend-mode: soft-light;
} }
} }
} }
@ -1159,19 +1178,17 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
} border-radius: inherit;
.preloader-container {
z-index: 1;
} }
} }
.background-colors-canvas { &-colors-canvas {
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 100%; height: 100%;
-webkit-mask-position: center; -webkit-mask-position: center;
-webkit-mask-size: contain; -webkit-mask-size: cover;
border-radius: inherit;
} }
} }

View File

@ -166,6 +166,7 @@ $row-border-radius: $border-radius-medium;
transform: translateY(-50%); transform: translateY(-50%);
inset-inline-end: .75rem; inset-inline-end: .75rem;
pointer-events: none; pointer-events: none;
opacity: 0;
} }
&.cant-sort { &.cant-sort {
@ -194,6 +195,12 @@ $row-border-radius: $border-radius-medium;
} }
} }
@include hover() {
.row-sortable-icon {
opacity: 1;
}
}
.is-reordering & { .is-reordering & {
@include animation-level(2) { @include animation-level(2) {
transition: transform var(--transition-standard-in); transition: transform var(--transition-standard-in);

View File

@ -0,0 +1,98 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
.themes {
&-container {
display: flex;
height: 6.5rem;
position: relative;
margin: 0 -.5rem;
width: calc(100% + 1rem);
align-items: center;
&:before,
&:after {
content: " ";
display: block;
width: .5rem;
flex: 0 0 auto;
}
}
}
.theme {
&-container {
height: calc(100% - .5rem);
margin: 0 .25rem;
border-radius: $border-radius-medium;
width: 4.5rem;
flex: 0 0 auto;
position: relative;
transform: scale(1);
@include animation-level(2) {
transition: transform var(--transition-standard-in);
}
&:active {
transform: scale(.9);
}
&:before {
position: absolute;
content: " ";
top: -4px;
right: -4px;
bottom: -4px;
left: -4px;
border-radius: #{$border-radius-medium + 4px};
border: 2px solid var(--primary-color);
transform: scale(.86);
@include animation-level(2) {
transition: transform var(--transition-standard-in);
}
}
&.active {
pointer-events: none;
&:before {
transform: scale(1);
}
}
}
&-emoticon {
position: absolute;
bottom: .5rem;
left: 50%;
transform: translateX(-50%);
width: 1.75rem;
height: 1.75rem;
pointer-events: none;
}
&-bubble {
width: 2.5rem;
height: 1.25rem;
border-radius: 1.75rem;
background-color: #fff;
position: absolute;
&.is-out {
top: .5rem;
right: .375rem;
background-color: var(--light-filled-message-out-primary-color);
}
&.is-in {
background-color: var(--message-background-color);
top: calc(1.25rem + .5rem + .25rem);
left: .375rem;
}
}
}

View File

@ -4,6 +4,8 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
@use "sass:math";
.popup-new-media { .popup-new-media {
$parent: ".popup"; $parent: ".popup";
@ -43,12 +45,11 @@
} }
&-close { &-close {
font-size: 1.5rem;
margin: -1px 0 0 -4px; margin: -1px 0 0 -4px;
} }
&-photo { &-photo {
max-width: 380px; max-width: 100%;
overflow: hidden; overflow: hidden;
// width: fit-content; // width: fit-content;
width: 100%; width: 100%;
@ -93,12 +94,95 @@
} }
.popup-new-media.popup-send-photo { .popup-new-media.popup-send-photo {
.popup-header { .popup-container {
width: 25rem;
max-width: 25rem;
padding: 0; padding: 0;
&.border-top-offset {
.popup-input-container {
overflow: unset;
&:before {
top: -8px;
}
}
}
}
.popup-header {
padding: 0 1rem;
height: 3.5rem;
margin: 0;
}
.popup-title {
padding-inline-start: 1.5rem;
}
.popup-close {
margin: 0;
} }
.popup-body { .popup-body {
position: relative; position: relative;
.scrollable {
padding: 0 .5rem;
}
}
.input-message-container {
min-height: inherit;
max-height: inherit;
// margin-top: -.5rem;
}
.input-message-input {
max-height: inherit !important;
}
.btn-primary {
flex: 0 0 auto;
width: auto;
padding: 0 1rem;
height: 2.5rem;
line-height: 2.5rem;
text-transform: uppercase;
margin-bottom: .5rem;
}
.popup-input-container {
--height: 3.5rem;
--max-height: 8.375rem;
display: flex;
align-items: flex-end;
justify-content: space-between;
padding: 0 .5rem;
min-height: var(--height);
max-height: var(--max-height);
position: relative;
flex: 0 0 auto;
overflow: hidden;
&:before {
content: " ";
position: absolute;
height: 1px;
top: 0;
left: 0;
right: 0;
background-color: var(--border-color);
opacity: 0;
@include animation-level(2) {
transition: opacity var(--transition-standard-in);
}
}
&.has-border-top:before {
opacity: 1;
}
} }
.checkbox-field { .checkbox-field {
@ -146,29 +230,35 @@
} }
.document { .document {
--padding: .25rem;
--icon-size: 4.5rem;
--icon-margin: .5rem;
max-width: 100%; max-width: 100%;
overflow: hidden; overflow: hidden;
cursor: default; cursor: default;
height: 4.5rem; height: 5rem;
margin: 0 .25rem;
border-radius: $border-radius-medium;
&-name { &-name {
font-weight: normal;
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
line-height: 1.5; line-height: 1.5;
margin-bottom: .125rem;
} }
&-ico { &-ico {
height: 48px;
width: 48px;
font-size: 16px; font-size: 16px;
font-weight: normal; font-weight: normal;
line-height: 11px; line-height: 11px;
letter-spacing: 0; letter-spacing: 0;
border-radius: #{math.div($border-radius-medium, 2)};
} }
@include hover-background-effect();
/* &.photo { /* &.photo {
.document-ico { .document-ico {
border-radius: $border-radius; border-radius: $border-radius;

View File

@ -15,7 +15,7 @@
} }
.popup-body { .popup-body {
margin: 1em -.5rem .375rem -.5rem; margin: 1rem 0 .375rem;
overflow: unset; overflow: unset;
} }

View File

@ -181,7 +181,7 @@ $chat-input-inner-padding-handhelds: .25rem;
} }
} }
@mixin splitColor($property, $color, $light: true, $dark: true, $light-rgb: false, $dark-rgb: false, $rgb: false, $alpha: $hover-alpha) { @mixin splitColor($property, $color, $light: true, $dark: true, $light-filled: false, $dark-filled: false, $rgb: false, $alpha: $hover-alpha) {
--#{$property}: #{$color}; --#{$property}: #{$color};
$lightened: hover-color($color); $lightened: hover-color($color);
@ -189,8 +189,8 @@ $chat-input-inner-padding-handhelds: .25rem;
--light-#{$property}: #{$lightened}; --light-#{$property}: #{$lightened};
} }
@if $light-rgb != false { @if $light-filled != false {
--light-filled-#{$property}: #{rgba-to-rgb($lightened, $light-rgb)}; --light-filled-#{$property}: #{rgba-to-rgb($lightened, $light-filled)};
} }
$darkened: darken($color, $alpha * 100); $darkened: darken($color, $alpha * 100);
@ -198,8 +198,8 @@ $chat-input-inner-padding-handhelds: .25rem;
--dark-#{$property}: #{$darkened}; --dark-#{$property}: #{$darkened};
} }
@if $dark-rgb != false { @if $dark-filled != false {
--dark-filled-#{$property}: #{rgba-to-rgb($darkened, $dark-rgb)}; --dark-filled-#{$property}: #{rgba-to-rgb($darkened, $dark-filled)};
} }
@if $rgb != false { @if $rgb != false {
@ -223,7 +223,7 @@ $chat-input-inner-padding-handhelds: .25rem;
--input-search-background-color: #fff; --input-search-background-color: #fff;
--input-search-border-color: #dfe1e5; --input-search-border-color: #dfe1e5;
@include splitColor(primary-color, #3390ec, true, true, #fff, false, true, .04); @include splitColor(primary-color, #3390ec, true, true, #fff, false, true);
@include splitColor(primary-text-color, #000, false, false, false, false, true); @include splitColor(primary-text-color, #000, false, false, false, false, true);
--secondary-color: #c4c9cc; --secondary-color: #c4c9cc;
@ -247,6 +247,7 @@ $chat-input-inner-padding-handhelds: .25rem;
--menu-background-color: rgba(var(--surface-color-rgb), var(--backdrop-opacity)); --menu-background-color: rgba(var(--surface-color-rgb), var(--backdrop-opacity));
--message-background-color: var(--surface-color); --message-background-color: var(--surface-color);
--message-time-color: var(--secondary-text-color);
--message-checkbox-color: #61c642; --message-checkbox-color: #61c642;
--message-checkbox-border-color: #fff; --message-checkbox-border-color: #fff;
--message-primary-color: var(--primary-color); --message-primary-color: var(--primary-color);
@ -259,6 +260,7 @@ $chat-input-inner-padding-handhelds: .25rem;
--message-out-link-color: var(--link-color); --message-out-link-color: var(--link-color);
@include splitColor(message-out-primary-color, #4fae4e, false, false, $message-out-background-color); @include splitColor(message-out-primary-color, #4fae4e, false, false, $message-out-background-color);
--message-out-status-color: var(--message-out-primary-color); --message-out-status-color: var(--message-out-primary-color);
--message-out-time-color: var(--message-out-status-color);
--message-out-audio-play-button-color: #fff; --message-out-audio-play-button-color: #fff;
--message-out-selection-background-color: var(--selection-background-color); --message-out-selection-background-color: var(--selection-background-color);
@ -323,6 +325,7 @@ $chat-input-inner-padding-handhelds: .25rem;
--menu-background-color: rgba(var(--surface-color-rgb), .75); --menu-background-color: rgba(var(--surface-color-rgb), .75);
--message-background-color: var(--surface-color); --message-background-color: var(--surface-color);
--message-time-color: var(--secondary-text-color);
--message-checkbox-color: var(--primary-color); --message-checkbox-color: var(--primary-color);
--message-checkbox-border-color: #fff; --message-checkbox-border-color: #fff;
--message-secondary-color: var(--secondary-color); --message-secondary-color: var(--secondary-color);
@ -333,7 +336,8 @@ $chat-input-inner-padding-handhelds: .25rem;
@include splitColor(message-out-background-color, $message-out-background-color, true, true); @include splitColor(message-out-background-color, $message-out-background-color, true, true);
--message-out-link-color: #fff; --message-out-link-color: #fff;
@include splitColor(message-out-primary-color, #fff, false, false, $message-out-background-color); @include splitColor(message-out-primary-color, #fff, false, false, $message-out-background-color);
--message-out-status-color: rgba(255, 255, 255, .6); --message-out-status-color: #fff;
--message-out-time-color: rgba(255, 255, 255, .6);
--message-out-audio-play-button-color: var(--message-out-background-color); --message-out-audio-play-button-color: var(--message-out-background-color);
--message-out-selection-background-color: rgba(var(--surface-color-rgb), .4); --message-out-selection-background-color: rgba(var(--surface-color-rgb), .4);
// * Night theme end // * Night theme end
@ -395,6 +399,7 @@ $chat-input-inner-padding-handhelds: .25rem;
@import "partials/customEmoji"; @import "partials/customEmoji";
@import "partials/usernames"; @import "partials/usernames";
@import "partials/topics"; @import "partials/topics";
@import "partials/themes";
@import "partials/popups/popup"; @import "partials/popups/popup";
@import "partials/popups/editAvatar"; @import "partials/popups/editAvatar";

View File

@ -186,9 +186,9 @@ module.exports = {
// }, // },
compress: true, compress: true,
http2: useLocalNotLocal ? true : (useLocal ? undefined : true), http2: useLocalNotLocal ? true : (useLocal ? undefined : true),
https: useLocal ? undefined : { https: useLocal ? undefined : { // generated keys using mkcert
key: fs.readFileSync(__dirname + '/certs/server-key.pem', 'utf8'), key: fs.readFileSync(__dirname + '/certs/localhost-key.pem', 'utf8'),
cert: fs.readFileSync(__dirname + '/certs/server-cert.pem', 'utf8') cert: fs.readFileSync(__dirname + '/certs/localhost.pem', 'utf8')
}, },
allowedHosts: useLocal ? undefined : [ allowedHosts: useLocal ? undefined : [
domain domain