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_HASH=452b0359b988148995f22ff0f4229750
VERSION=1.7.0
VERSION_FULL=1.7.0 (289)
BUILD=289
VERSION=1.7.1
VERSION_FULL=1.7.1 (291)
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"
}
],
"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",
"background_color": "#ffffff",
"display": "standalone",

View File

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

View File

@ -132,6 +132,7 @@ import confirmationPopup from '../confirmationPopup';
import wrapPeerTitle from '../wrappers/peerTitle';
import {PopupPeerCheckboxOptions} from '../popups/peer';
import toggleDisability from '../../helpers/dom/toggleDisability';
import {copyTextToClipboard} from '../../helpers/clipboard';
export const USER_REACTIONS_INLINE = false;
const USE_MEDIA_TAILS = false;
@ -1671,9 +1672,18 @@ export default class ChatBubbles {
const contactDiv: HTMLElement = findUpClassName(target, 'contact');
if(contactDiv) {
this.chat.appImManager.setInnerPeer({
peerId: contactDiv.dataset.peerId.toPeerId()
});
const 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;
}
@ -3940,11 +3950,9 @@ export default class ChatBubbles {
}
return new Promise<PeerId>((resolve, reject) => {
const popup = new PopupForward({
[this.peerId]: []
}, (peerId) => {
const popup = new PopupForward(undefined, (peerId) => {
resolve(peerId);
}, true);
});
popup.addEventListener('close', () => {
reject();
@ -4568,11 +4576,12 @@ export default class ChatBubbles {
contactDetails.className = 'contact-details';
const contactNameDiv = document.createElement('div');
contactNameDiv.className = 'contact-name';
const fullName = [
contact.first_name,
contact.last_name
].filter(Boolean).join(' ');
contactNameDiv.append(
wrapEmojiText([
contact.first_name,
contact.last_name
].filter(Boolean).join(' '))
fullName.trim() ? wrapEmojiText(fullName) : i18n('AttachContact')
);
const contactNumberDiv = document.createElement('div');
@ -4585,7 +4594,8 @@ export default class ChatBubbles {
const avatarElem = new AvatarElement();
avatarElem.updateWithOptions({
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');

View File

@ -20,7 +20,7 @@ import {NULL_PEER_ID, REPLIES_PEER_ID} from '../../lib/mtproto/mtproto_config';
import SetTransition from '../singleTransition';
import AppPrivateSearchTab from '../sidebarRight/tabs/search';
import renderImageFromUrl from '../../helpers/dom/renderImageFromUrl';
import mediaSizes from '../../helpers/mediaSizes';
import mediaSizes, {ScreenSize} from '../../helpers/mediaSizes';
import ChatSearch from './search';
import IS_TOUCH_SUPPORTED from '../../environment/touchSupport';
import getAutoDownloadSettingsByPeerId, {ChatAutoDownloadSettings} from '../../helpers/autoDownload';
@ -34,8 +34,9 @@ import AppSharedMediaTab from '../sidebarRight/tabs/sharedMedia';
import noop from '../../helpers/noop';
import middlewarePromise from '../../helpers/middlewarePromise';
import indexOfAndSplice from '../../helpers/array/indexOfAndSplice';
import {Message} from '../../layer';
import {Message, WallPaper} from '../../layer';
import animationIntersector, {AnimationItemGroup} from '../animationIntersector';
import {getColorsFromWallPaper} from '../sidebarLeft/tabs/background';
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> {
const theme = themeController.getTheme();
const themeSettings = theme.settings;
const wallPaper = themeSettings.wallpaper;
const colors = getColorsFromWallPaper(wallPaper);
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(
isColorBackground &&
document.documentElement.style.cursor === 'grabbing' &&
this.gradientRenderer &&
!this.patternRenderer
) {
this.gradientCanvas.dataset.colors = theme.background.color;
this.gradientCanvas.dataset.colors = colors;
this.gradientRenderer.init(this.gradientCanvas);
return Promise.resolve();
}
@ -150,7 +154,7 @@ export default class Chat extends EventListenerBase<{
// this.renderDarkPattern =
undefined;
const intensity = theme.background.intensity && theme.background.intensity / 100;
const intensity = wallPaper.settings?.intensity && wallPaper.settings.intensity / 100;
const isDarkPattern = !!intensity && intensity < 0;
let patternRenderer: ChatBackgroundPatternRenderer;
@ -190,19 +194,18 @@ export default class Chat extends EventListenerBase<{
// });
// };
// }
} else if(theme.background.slug) {
} else {
item.classList.add('is-image');
}
} else if(theme.background.color) {
} else {
item.classList.add('is-color');
}
}
let gradientRenderer: ChatBackgroundGradientRenderer;
const color = theme.background.color;
if(color) {
if(colors) {
// if(color.includes(',')) {
const {canvas, gradientRenderer: _gradientRenderer} = ChatBackgroundGradientRenderer.create(color);
const {canvas, gradientRenderer: _gradientRenderer} = ChatBackgroundGradientRenderer.create(colors);
gradientRenderer = this.gradientRenderer = _gradientRenderer;
gradientCanvas = this.gradientCanvas = 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) => {
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.inputFake.classList.replace('input-field-input', 'input-message-input');
this.messageInput = this.messageInputField.input;
this.messageInput.classList.add('no-scrollbar');
this.attachMessageInputListeners();
if(IS_STICKY_INPUT_BUGGED) {

View File

@ -165,14 +165,10 @@ export default class DotRenderer implements AnimationItemWrapper {
animationGroup: AnimationItemGroup,
multiply?: number
}) {
middleware.onClean(() => {
animationIntersector.removeAnimationByPlayer(dotRenderer);
});
const dotRenderer = new DotRenderer(width, height, multiply);
dotRenderer.renderFirstFrame();
animationIntersector.addAnimation(dotRenderer, animationGroup, dotRenderer.canvas, true);
animationIntersector.addAnimation(dotRenderer, animationGroup, dotRenderer.canvas, middleware);
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 {
public inputFake: HTMLElement;
public onChangeHeight: (height: number) => void;
// public onLengthChange: (length: number, isOverflow: boolean) => void;
// protected wasInputFakeClientHeight: number;
@ -31,7 +32,7 @@ export default class InputFieldAnimated extends InputField {
_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.showScrollDebounced = debounce(() => this.input.classList.remove('no-scrollbar'), 150, false, true);
this.inputFake = document.createElement('div');
@ -62,6 +63,7 @@ export default class InputFieldAnimated extends InputField {
this.input.style.transitionDuration = `${transitionDuration}ms`;
if(setHeight) {
this.onChangeHeight?.(newHeight);
this.input.style.height = newHeight ? newHeight + 'px' : '';
}

View File

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

View File

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

View File

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

View File

@ -153,8 +153,8 @@ export class ScrollableBase {
this.onScrolledBottom = undefined;
}
public append(element: HTMLElement) {
this.container.append(element);
public append(...args: Parameters<HTMLElement['append']>) {
this.container.append(...args);
}
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 mediaSizes from '../../helpers/mediaSizes';
import {fastRaf} from '../../helpers/schedulers';
import {getInstallPrompt} from '../../helpers/dom/installPrompt';
export const LEFT_COLUMN_ACTIVE_CLASSNAME = 'is-left-column-shown';
@ -218,6 +219,14 @@ export class AppSidebarLeft extends SidebarSlider {
});
},
verify: () => App.isMainDomain
}, {
icon: 'download',
text: 'PWA.Install',
onClick: () => {
const installPrompt = getInstallPrompt();
installPrompt?.();
},
verify: () => !!getInstallPrompt()
}];
const filteredButtons = menuButtons.filter(Boolean);

View File

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

View File

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

View File

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

View File

@ -11,11 +11,11 @@ import RadioField from '../../radioField';
import rootScope from '../../../lib/rootScope';
import {IS_APPLE} from '../../../environment/userAgent';
import Row, {CreateRowFromCheckboxField} from '../../row';
import AppBackgroundTab from './background';
import AppBackgroundTab, {getHexColorFromTelegramColor, getRgbColorFromTelegramColor} from './background';
import {LangPackKey, _i18n} from '../../../lib/langPack';
import {attachClickEvent} from '../../../helpers/dom/clickEvent';
import assumeType from '../../../helpers/assumeType';
import {AvailableReaction, MessagesAllStickers, StickerSet} from '../../../layer';
import {AvailableReaction, BaseTheme, MessagesAllStickers, StickerSet} from '../../../layer';
import LazyLoadQueue from '../../lazyLoadQueue';
import PopupStickers from '../../popups/stickers';
import eachMinute from '../../../helpers/eachMinute';
@ -27,6 +27,14 @@ import {State} from '../../../config/state';
import wrapStickerSetThumb from '../../wrappers/stickerSetThumb';
import wrapStickerToRow from '../../wrappers/stickerToRow';
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 {
public container: HTMLDivElement;
@ -87,11 +95,20 @@ export class RangeSettingSelector {
}
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.setTitle('General');
const section = generateSection.bind(null, this.scrollable);
const promises: Promise<any>[] = [];
{
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');
@ -264,7 +595,7 @@ export default class AppGeneralSettingsTab extends SliderSuperTabEventable {
});
const renderQuickReaction = () => {
this.managers.appReactionsManager.getQuickReaction().then((reaction) => {
p.quickReaction.then((reaction) => {
if(reaction._ === 'availableReaction') {
return reaction.static_icon;
} else {
@ -325,7 +656,8 @@ export default class AppGeneralSettingsTab extends SliderSuperTabEventable {
lazyLoadQueue,
width: 36,
height: 36,
autoplay: true
autoplay: true,
middleware: this.middlewareHelper.get()
});
row.container.append(div);
@ -333,13 +665,14 @@ export default class AppGeneralSettingsTab extends SliderSuperTabEventable {
stickersContent[method](row.container);
};
this.managers.appStickersManager.getAllStickers().then((allStickers) => {
const promise = p.allStickers.then((allStickers) => {
assumeType<MessagesAllStickers.messagesAllStickers>(allStickers);
for(const stickerSet of allStickers.sets) {
renderStickerSet(stickerSet);
}
const promises = allStickers.sets.map((stickerSet) => renderStickerSet(stickerSet));
return Promise.all(promises);
});
promises.push(promise);
this.listenerSetter.add(rootScope)('stickers_installed', (set) => {
if(!stickerSets[set.id]) {
renderStickerSet(set, 'prepend');
@ -360,12 +693,7 @@ export default class AppGeneralSettingsTab extends SliderSuperTabEventable {
);
this.scrollable.append(section.container);
}
}
onOpen() {
if(this.init) {
this.init();
this.init = null;
}
return Promise.all(promises);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -8,16 +8,17 @@ import {AppMediaPlaybackController} from '../components/appMediaPlaybackControll
import {IS_MOBILE} from '../environment/userAgent';
import getTimeFormat from '../helpers/getTimeFormat';
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 DialogsStorage from '../lib/storages/dialogs';
import FiltersStorage from '../lib/storages/filters';
import {AuthState} from '../types';
import {AuthState, Modify} from '../types';
import App from './app';
const STATE_VERSION = App.version;
const BUILD = App.build;
// ! DEPRECATED
export type Background = {
type?: 'color' | 'image' | 'default', // ! DEPRECATED
blur: boolean,
@ -28,10 +29,12 @@ export type Background = {
id: string | number, // wallpaper id
};
export type Theme = {
export type AppTheme = Modify<Theme, {
name: 'day' | 'night' | 'system',
background: Background
};
settings?: Modify<ThemeSettings, {
highlightningColor: string
}>
}>;
export type AutoDownloadPeerTypeSettings = {
contacts: boolean,
@ -95,8 +98,8 @@ export type State = {
big: boolean
},
background?: Background, // ! DEPRECATED
themes: Theme[],
theme: Theme['name'],
themes: AppTheme[],
theme: AppTheme['name'],
notifications: {
sound: boolean
},
@ -110,41 +113,101 @@ export type State = {
notifySettings: {[k in Exclude<NotifyPeer['_'], 'notifyPeer'>]?: PeerNotifySettings.peerNotifySettings}
};
const BACKGROUND_DAY_DESKTOP: Background = {
blur: false,
slug: 'pattern',
color: '#dbddbb,#6ba587,#d5d88d,#88b884',
highlightningColor: 'hsla(86.4, 43.846153%, 45.117647%, .4)',
intensity: 50,
id: '1'
};
// const BACKGROUND_DAY_MOBILE: Background = {
// blur: false,
// slug: '',
// color: '#dbddbb,#6ba587,#d5d88d,#88b884',
// highlightningColor: 'hsla(86.4, 43.846153%, 45.117647%, .4)',
// intensity: 0,
// id: '1'
// };
const BACKGROUND_DAY_MOBILE: Background = {
blur: false,
// const BACKGROUND_NIGHT_MOBILE: Background = {
// 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: '',
color: '#dbddbb,#6ba587,#d5d88d,#88b884',
highlightningColor: 'hsla(86.4, 43.846153%, 45.117647%, .4)',
intensity: 0,
id: '1'
title: '',
emoticon: '🏠',
pFlags: {default: true}
};
const BACKGROUND_NIGHT_DESKTOP: Background = {
blur: false,
slug: 'pattern',
// color: '#dbddbb,#6ba587,#d5d88d,#88b884',
color: '#fec496,#dd6cb9,#962fbf,#4f5bd5',
highlightningColor: 'hsla(299.142857, 44.166666%, 37.470588%, .4)',
intensity: -50,
id: '-1'
};
const BACKGROUND_NIGHT_MOBILE: Background = {
blur: false,
slug: '',
color: '#0f0f0f',
highlightningColor: 'hsla(0, 0%, 3.82353%, 0.4)',
intensity: 0,
id: '-1'
const makeDefaultAppTheme = (
name: AppTheme['name'],
baseTheme: BaseTheme['_'],
highlightningColor: string
): AppTheme => {
return {
...DEFAULT_THEME,
name,
settings: {
...DEFAULT_THEME.settings.find((s) => s.base_theme._ === baseTheme),
highlightningColor
}
};
};
export const STATE_INIT: State = {
@ -214,13 +277,10 @@ export const STATE_INIT: State = {
suggest: true,
big: true
},
themes: [{
name: 'day',
background: IS_MOBILE ? BACKGROUND_DAY_MOBILE : BACKGROUND_DAY_DESKTOP
}, {
name: 'night',
background: IS_MOBILE ? BACKGROUND_NIGHT_MOBILE : BACKGROUND_NIGHT_DESKTOP
}],
themes: [
makeDefaultAppTheme('day', 'baseThemeClassic', 'hsla(86.4, 43.846153%, 45.117647%, .4)'),
makeDefaultAppTheme('night', 'baseThemeNight', 'hsla(299.142857, 44.166666%, 37.470588%, .4)')
],
theme: 'system',
notifications: {
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 = /* 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';
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
'video/mp4',
'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 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]
*/
@ -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
* 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} s The saturation
* @param {number} l The lightness
* @return {Array} The RGB representation
* @param {number} h The hue [0, 360]
* @param {number} s The saturation [0, 1]
* @param {number} l The lightness [0, 1]
* @return {Array} The RGB representation [0, 255]
*/
export function hslaToRgba(h: number, s: number, l: number, a: number): ColorRgba {
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);
}
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) {

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 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) {
const _path = path ? `${path}.${key}` : key;
if(ignorePaths?.has(_path)) {
continue;
}
if(typeof(currentObject[key]) !== typeof(initObject[key])) {
currentObject[key] = copy(initObject[key]);
onReplace && onReplace(previousKey || key);
onReplace?.(previousKey || 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) => {
media.removeEventListener(loadEventName, onLoad);
reject(e);
reject(media.error);
};
media.addEventListener(loadEventName, onLoad, {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,
autoplay: false,
loop: false,
toneIndex
}, 'none');
toneIndex,
group: 'none'
});
animation.addEventListener('firstFrame', () => {
saveLottiePreview(doc, animation.canvas[0], toneIndex);

View File

@ -4,7 +4,7 @@
* 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 rootScope from '../lib/rootScope';
import {hslaStringToHex} from './color';
@ -12,7 +12,7 @@ import {hslaStringToHex} from './color';
export class ThemeController {
private themeColor: string;
private _themeColorElem: Element;
private systemTheme: Theme['name'];
private systemTheme: AppTheme['name'];
constructor() {
rootScope.addEventListener('theme_change', () => {
@ -71,8 +71,8 @@ export class ThemeController {
public applyHighlightningColor() {
let hsla: string;
const theme = themeController.getTheme();
if(theme.background.highlightningColor) {
hsla = theme.background.highlightningColor;
if(theme.settings?.highlightningColor) {
hsla = theme.settings.highlightningColor;
document.documentElement.style.setProperty('--message-highlightning-color', hsla);
} else {
document.documentElement.style.removeProperty('--message-highlightning-color');
@ -100,7 +100,7 @@ export class ThemeController {
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);
}
}

View File

@ -30,6 +30,8 @@ import parseUriParams from './helpers/string/parseUriParams';
import Modes from './config/modes';
import {AuthState} from './types';
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';
document.addEventListener('DOMContentLoaded', async() => {
@ -209,6 +211,10 @@ document.addEventListener('DOMContentLoaded', async() => {
}, {capture: true, passive: false}); */
}
if(IS_INSTALL_PROMPT_SUPPORTED) {
cacheInstallPrompt();
}
const perf = performance.now();
// await pause(1000000);

View File

@ -109,6 +109,7 @@ const lang = {
'one_value': '%d exception',
'other_value': '%d exceptions'
},
'PWA.Install': 'Install App',
'Link.Available': 'Link is available',
'Link.Taken': 'Link is already taken',
'Link.Invalid': 'Link is invalid',
@ -119,6 +120,11 @@ const lang = {
'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.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',
'MarkupTooltip.LinkPlaceholder': 'Enter URL...',
'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.',
'FwdMessageToSavedMessages': 'Message forwarded to **Saved Messages**.',
'FwdMessagesToSavedMessages': 'Messages forwarded to **Saved Messages**.',
'ColorTheme': 'Color theme',
'SendAsFile': 'Send as file',
'SendAsFiles': 'Send as files',
// * macos
'AccountSettings.Filters': 'Chat Folders',

View File

@ -231,7 +231,7 @@ export class AppDocsManager extends AppManager {
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;
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 AppPrivateSearchTab from '../../components/sidebarRight/tabs/search';
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 PopupPeer, {PopupPeerCheckboxOptions} from '../../components/popups/peer';
import blurActiveElement from '../../helpers/dom/blurActiveElement';
@ -100,6 +100,7 @@ import parseUriParams from '../../helpers/string/parseUriParams';
import getMessageThreadId from './utils/messages/getMessageThreadId';
import findUpTag from '../../helpers/dom/findUpTag';
import {MTAppConfig} from '../mtproto/appConfig';
import PopupForward from '../../components/popups/forward';
export type ChatSavedPosition = {
mids: number[],
@ -186,10 +187,19 @@ export class AppImManager extends EventListenerBase<{
this.backgroundPromises = {};
STATE_INIT.settings.themes.forEach((theme) => {
if(theme.background.slug) {
const url = 'assets/img/' + theme.background.slug + '.svg' + (IS_FIREFOX ? '?1' : '');
this.backgroundPromises[theme.background.slug] = Promise.resolve(url);
const themeSettings = theme.settings;
if(!themeSettings) {
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);
@ -793,6 +803,23 @@ export class AppImManager extends EventListenerBase<{
this.onHashChange(true);
this.attachKeydownListener();
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: {
@ -1495,16 +1522,17 @@ export class AppImManager extends EventListenerBase<{
public setCurrentBackground(broadcastEvent = false): ReturnType<AppImManager['setBackground']> {
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 isDefaultBackground = theme.background.blur === defaultTheme.background.blur &&
// theme.background.slug === defaultTheme.background.slug;
// slug === defaultslug;
// if(!isDefaultBackground) {
return this.getBackground(theme.background.slug).then((url) => {
return this.getBackground(slug).then((url) => {
return this.setBackground(url, broadcastEvent);
}, () => { // * if NO_ENTRY_FOUND
theme.background = copy(defaultTheme.background); // * reset background
theme.settings = copy(defaultTheme.settings); // * reset background
return this.setCurrentBackground(true);
});
// }

View File

@ -9,6 +9,10 @@
* 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 deferredPromise, {CancellablePromise} from '../../helpers/cancellablePromise';
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 {ArgumentTypes, InvokeApiOptions} from '../../types';
import {logger, LogTypes} from '../logger';
import type {ApiFileManager} from '../mtproto/apiFileManager';
import {ReferenceContext} from '../mtproto/referenceDatabase';
import DialogsStorage, {GLOBAL_FOLDER_ID} from '../storages/dialogs';
import {ChatRights} from './appChatsManager';
@ -36,7 +39,6 @@ import deepEqual from '../../helpers/object/deepEqual';
import splitStringByLength from '../../helpers/string/splitStringByLength';
import debounce from '../../helpers/schedulers/debounce';
import {AppManager} from './manager';
import type {MediaSize} from '../../helpers/mediaSize';
import getPhotoMediaInput from './utils/photos/getPhotoMediaInput';
import getPhotoDownloadOptions from './utils/photos/getPhotoDownloadOptions';
import fixEmoji from '../richTextProcessor/fixEmoji';
@ -53,7 +55,6 @@ import defineNotNumerableProperties from '../../helpers/object/defineNotNumerabl
import getDocumentMediaInput from './utils/docs/getDocumentMediaInput';
import getDocumentInputFileName from './utils/docs/getDocumentInputFileName';
import getFileNameForUpload from '../../helpers/getFileNameForUpload';
import type {Progress} from './appDownloadManager';
import noop from '../../helpers/noop';
import appTabsManager from './appTabsManager';
import MTProtoMessagePort from '../mtproto/mtprotoMessagePort';
@ -817,7 +818,7 @@ export class AppMessagesManager extends AppManager {
cacheContext.url = options.objectURL || '';
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';
apiFileName = 'video.mp4';
actionName = 'sendMessageUploadVideoAction';
@ -5624,7 +5625,7 @@ export class AppMessagesManager extends AppManager {
return chatPeerIds[chatPeerIds.length - 1] === peerId;
});
if(!tab) {
if(!tab && tabs.length) {
tabs.sort((a, b) => a.state.idleStartTime - b.state.idleStartTime);
tab = !tabs[0].state.idleStartTime ? tabs[0] : tabs[tabs.length - 1];
}
@ -5633,7 +5634,7 @@ export class AppMessagesManager extends AppManager {
port.invokeVoid('notificationBuild', {
message,
...options
}, tab.source);
}, tab?.source);
}
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) {

View File

@ -6,7 +6,7 @@
import App from '../../../../config/app';
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 copy from '../../../../helpers/object/copy';
import validateInitObject from '../../../../helpers/object/validateInitObject';
@ -18,6 +18,7 @@ import {recordPromiseBound} from '../../../../helpers/recordPromise';
// import RESET_STORAGES_PROMISE from "../storages/resetStoragesPromise";
import {StoragesResults} from '../storages/loadStorages';
import {logger} from '../../../logger';
import {WallPaper} from '../../../../layer';
const REFRESH_EVERY = 24 * 60 * 60 * 1000; // 1 day
// const REFRESH_EVERY = 1e3;
@ -245,22 +246,6 @@ async function loadStateInner() {
// 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
const autoDownloadSettings = state.settings.autoDownload;
if(autoDownloadSettings?.private !== undefined) {
@ -291,9 +276,12 @@ async function loadStateInner() {
pushToState('settings', state.settings);
}
const SKIP_VALIDATING_PATHS: Set<string> = new Set([
'settings.themes'
]);
validateInitObject(STATE_INIT, state, (missingKey) => {
pushToState(missingKey as keyof State, state[missingKey as keyof State]);
});
}, undefined, SKIP_VALIDATING_PATHS);
let newVersion: string, oldVersion: string;
if(state.version !== STATE_VERSION || state.build !== BUILD/* || true */) {
@ -306,26 +294,83 @@ async function loadStateInner() {
resetStorages.add('dialogs');
}
// * migrate backgrounds (March 13, 2022; to version 1.3.0)
if(compareVersion(state.version, '1.3.0') === -1) {
if(compareVersion(state.version, '1.7.1') === -1) {
let migrated = false;
state.settings.themes.forEach((theme, idx, arr) => {
if((
theme.name === 'day' &&
theme.background.slug === 'ByxGo2lrMFAIAAAAmkJxZabh8eM' &&
theme.background.type === 'image'
) || (
theme.name === 'night' &&
theme.background.color === '#0f0f0f' &&
theme.background.type === 'color'
)) {
const newTheme = STATE_INIT.settings.themes.find((newTheme) => newTheme.name === theme.name);
if(newTheme) {
arr[idx] = copy(newTheme);
migrated = true;
}
// * migrate backgrounds (March 13, 2022; to version 1.3.0)
if(compareVersion(state.version, '1.3.0') === -1) {
migrated = true;
state.settings.theme = copy(STATE_INIT.settings.theme);
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)
migrated = true;
const oldThemes = state.settings.themes as any as Array<{
name: AppTheme['name'],
background: Background
}>;
state.settings.themes = copy(STATE_INIT.settings.themes);
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) {
pushToState('settings', state.settings);

View File

@ -771,9 +771,9 @@ export class ApiFileManager extends AppManager {
if(isDocument && !thumb) {
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);
});
}).catch(noop);
}
}

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@
*/
import type {AnimationItemGroup, AnimationItemWrapper} from '../../components/animationIntersector';
import type {Middleware} from '../../helpers/middleware';
import CAN_USE_TRANSFERABLES from '../../environment/canUseTransferables';
import IS_APPLE_MX from '../../environment/appleMx';
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 = {
container: HTMLElement | HTMLElement[],
middleware?: Middleware,
canvas?: HTMLCanvasElement,
autoplay?: boolean,
animationData: Blob,

View File

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

View File

@ -11,7 +11,7 @@
import {Database} from '../../config/databases';
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 IDBStorage from '../files/idb';
import {log, serviceMessagePort} from './index.service';
@ -20,6 +20,11 @@ import {ServicePushPingTaskPayload} from './serviceMessagePort';
const ctx = self as any as ServiceWorkerGlobalScope;
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 = {
loc_key: string,
loc_args: string[],
@ -29,7 +34,8 @@ export type PushNotificationObject = {
chat_id?: string, // should be number
from_id?: string, // should be number
msg_id: string,
peerId?: string // should be number
peerId?: string, // should be number
silent?: string // can be '1'
},
sound?: string,
random_id: number,
@ -37,6 +43,7 @@ export type PushNotificationObject = {
description: string,
mute: string, // should be number
title: string,
message?: string,
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);
}
public async get<T extends keyof Storage>(key: T) {
if(this.cache[key] !== undefined) {
private getDefault<T extends keyof Storage>(key: T) {
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];
}
let value: Storage[T];
try {
value = await this.storage.get(key as string);
} catch(err) {
const promise = this.storage.get(key as string) as Promise<Storage[T]>;
return promise.then((value) => value, () => undefined as Storage[T]).then((value) => {
if(this.cache.hasOwnProperty(key)) {
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 this.cache[key];
}
if(value === undefined) {
const callback = this.defaults[key];
value = typeof(callback) === 'function' ? callback() : callback;
}
return this.cache[key] = value;
return value;
}
public async set<T extends keyof Storage>(key: T, value: Storage[T]) {
@ -101,7 +115,7 @@ type PushStorage = {
push_settings: Partial<ServicePushPingTaskPayload['settings']>
};
const getter = new SomethingGetter<typeof DATABASE_STATE, PushStorage>(DATABASE_STATE, 'session', {
const defaults: PushStorage = {
push_mute_until: 0,
push_lang: {
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_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) => {
const obj: PushNotificationObject = event.data.json();
log('push', obj);
log('push', {...obj});
let hasActiveWindows = false;
const checksPromise = Promise.all([
getter.get('push_mute_until'),
ctx.clients.matchAll({type: 'window'})
]).then((result) => {
const [muteUntil, clientList] = result;
try {
if(!obj.badge) {
throw 'no badge';
}
log('matched clients', clientList);
hasActiveWindows = clientList.length > 0;
const [muteUntil, settings, lang] = [
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) {
throw 'Supress notification because some instance is alive';
}
const nowTime = Date.now();
if(userInvisibleIsSupported() &&
muteUntil &&
nowTime < muteUntil) {
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);
const notificationPromise = fireNotification(obj, settings, lang);
event.waitUntil(notificationPromise);
} catch(err) {
log(err);
}
});
ctx.addEventListener('notificationclick', (event) => {
@ -205,7 +201,7 @@ ctx.addEventListener('notificationclick', (event) => {
}
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);
});
}
@ -236,7 +232,7 @@ function removeFromNotifications(notification: Notification) {
notifications.delete(notification);
}
export function closeAllNotifications() {
export function closeAllNotifications(tag?: string) {
for(const notification of notifications) {
try {
notification.close();
@ -245,7 +241,7 @@ export function closeAllNotifications() {
let promise: Promise<void>;
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) {
try {
notifications[i].close();
@ -269,6 +265,7 @@ function userInvisibleIsSupported() {
function fireNotification(obj: PushNotificationObject, settings: PushStorage['push_settings'], lang: PushStorage['push_lang']) {
const icon = 'assets/img/logo_filled_rounded.png';
const badge = 'assets/img/masked.svg';
let title = obj.title || 'Telegram';
let body = obj.description || '';
let peerId: string;
@ -286,7 +283,7 @@ function fireNotification(obj: PushNotificationObject, settings: PushStorage['pu
obj.custom.peerId = '' + peerId;
let tag = 'peer' + peerId;
if(settings && settings.nopreview) {
if(settings?.nopreview) {
title = 'Telegram';
body = lang.push_message_nopreview;
tag = 'unknown_peer';
@ -307,21 +304,20 @@ function fireNotification(obj: PushNotificationObject, settings: PushStorage['pu
icon,
tag,
data: obj,
actions
actions,
badge,
silent: obj.custom.silent === '1'
});
return notificationPromise.then((event) => {
// @ts-ignore
if(event?.notification) {
// @ts-ignore
pushToNotifications(event.notification);
}
}).catch((error) => {
return notificationPromise.catch((error) => {
log.error('Show notification promise', error);
});
}
export function onPing(payload: ServicePushPingTaskPayload, source?: MessageEventSource) {
lastPingTime = Date.now();
localNotificationsAvailable = payload.localNotifications;
if(pendingNotification && source) {
serviceMessagePort.invokeVoid('pushClick', pendingNotification, source);
pendingNotification = undefined;

View File

@ -52,6 +52,7 @@ export default class ServiceMessagePort<Master extends boolean = false> extends
// to main thread
pushClick: (payload: PushNotificationObject) => void,
hello: (payload: void, source: MessageEventSource) => void,
share: (payload: ShareData) => void,
// to mtproto worker
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,
id: filter.id,
filter: remove ? undefined : this.getOutputDialogFilter(filter)
}).then((bool: boolean) => { // возможно нужна проверка и откат, если результат не ТРУ
}).then((bool) => { // возможно нужна проверка и откат, если результат не ТРУ
// console.log('updateDialogFilter bool:', bool);
if(bool) {
/* if(!this.filters[filter.id]) {
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
});
}
/* if(!this.filters[filter.id]) {
this.saveDialogFilter(filter);
}
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;
font-weight: 500;
transform: scale(1);
.tgico-char {
width: var(--icon-size);
}
@include animation-level(2) {
transition: transform var(--btn-menu-transition);

View File

@ -177,47 +177,6 @@ $chat-input-border-radius: 1rem;
//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 {
&:before {
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 {
--send-as-size: 1.875rem;
--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 {
flex: 0 0 auto;
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 {
--translateY: 0;
width: 100%;

View File

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

View File

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

View File

@ -1116,7 +1116,7 @@
}
.background-container {
.grid {
.background {
&-item {
&:after {
content: " ";
@ -1144,13 +1144,32 @@
&-media {
transition: transform .2s ease-in-out;
transform: scale(1);
}
}
&.is-pattern {
background-color: #000;
.media-photo {
mix-blend-mode: overlay;
}
.preloader-container {
z-index: 1;
}
}
}
.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%;
height: 100%;
object-fit: cover;
}
.preloader-container {
z-index: 1;
border-radius: inherit;
}
}
.background-colors-canvas {
&-colors-canvas {
position: absolute;
width: 100%;
height: 100%;
-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%);
inset-inline-end: .75rem;
pointer-events: none;
opacity: 0;
}
&.cant-sort {
@ -194,6 +195,12 @@ $row-border-radius: $border-radius-medium;
}
}
@include hover() {
.row-sortable-icon {
opacity: 1;
}
}
.is-reordering & {
@include animation-level(2) {
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
*/
@use "sass:math";
.popup-new-media {
$parent: ".popup";
@ -43,12 +45,11 @@
}
&-close {
font-size: 1.5rem;
margin: -1px 0 0 -4px;
}
&-photo {
max-width: 380px;
max-width: 100%;
overflow: hidden;
// width: fit-content;
width: 100%;
@ -93,12 +94,95 @@
}
.popup-new-media.popup-send-photo {
.popup-header {
.popup-container {
width: 25rem;
max-width: 25rem;
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 {
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 {
@ -146,29 +230,35 @@
}
.document {
--padding: .25rem;
--icon-size: 4.5rem;
--icon-margin: .5rem;
max-width: 100%;
overflow: hidden;
cursor: default;
height: 4.5rem;
height: 5rem;
margin: 0 .25rem;
border-radius: $border-radius-medium;
&-name {
font-weight: normal;
width: 100%;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.5;
margin-bottom: .125rem;
}
&-ico {
height: 48px;
width: 48px;
font-size: 16px;
font-weight: normal;
line-height: 11px;
letter-spacing: 0;
border-radius: #{math.div($border-radius-medium, 2)};
}
@include hover-background-effect();
/* &.photo {
.document-ico {
border-radius: $border-radius;

View File

@ -15,7 +15,7 @@
}
.popup-body {
margin: 1em -.5rem .375rem -.5rem;
margin: 1rem 0 .375rem;
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};
$lightened: hover-color($color);
@ -189,8 +189,8 @@ $chat-input-inner-padding-handhelds: .25rem;
--light-#{$property}: #{$lightened};
}
@if $light-rgb != false {
--light-filled-#{$property}: #{rgba-to-rgb($lightened, $light-rgb)};
@if $light-filled != false {
--light-filled-#{$property}: #{rgba-to-rgb($lightened, $light-filled)};
}
$darkened: darken($color, $alpha * 100);
@ -198,8 +198,8 @@ $chat-input-inner-padding-handhelds: .25rem;
--dark-#{$property}: #{$darkened};
}
@if $dark-rgb != false {
--dark-filled-#{$property}: #{rgba-to-rgb($darkened, $dark-rgb)};
@if $dark-filled != false {
--dark-filled-#{$property}: #{rgba-to-rgb($darkened, $dark-filled)};
}
@if $rgb != false {
@ -223,7 +223,7 @@ $chat-input-inner-padding-handhelds: .25rem;
--input-search-background-color: #fff;
--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);
--secondary-color: #c4c9cc;
@ -247,6 +247,7 @@ $chat-input-inner-padding-handhelds: .25rem;
--menu-background-color: rgba(var(--surface-color-rgb), var(--backdrop-opacity));
--message-background-color: var(--surface-color);
--message-time-color: var(--secondary-text-color);
--message-checkbox-color: #61c642;
--message-checkbox-border-color: #fff;
--message-primary-color: var(--primary-color);
@ -259,6 +260,7 @@ $chat-input-inner-padding-handhelds: .25rem;
--message-out-link-color: var(--link-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-time-color: var(--message-out-status-color);
--message-out-audio-play-button-color: #fff;
--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);
--message-background-color: var(--surface-color);
--message-time-color: var(--secondary-text-color);
--message-checkbox-color: var(--primary-color);
--message-checkbox-border-color: #fff;
--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);
--message-out-link-color: #fff;
@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-selection-background-color: rgba(var(--surface-color-rgb), .4);
// * Night theme end
@ -395,6 +399,7 @@ $chat-input-inner-padding-handhelds: .25rem;
@import "partials/customEmoji";
@import "partials/usernames";
@import "partials/topics";
@import "partials/themes";
@import "partials/popups/popup";
@import "partials/popups/editAvatar";

View File

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