diff --git a/.env b/.env index 7aaf0ddf..deb1d6b4 100644 --- a/.env +++ b/.env @@ -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 diff --git a/public/assets/img/masked.svg b/public/assets/img/masked.svg new file mode 100644 index 00000000..e80baa89 --- /dev/null +++ b/public/assets/img/masked.svg @@ -0,0 +1,45 @@ + + + + +Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + + + diff --git a/public/assets/img/screenshot.jpg b/public/assets/img/screenshot.jpg new file mode 100644 index 00000000..51db4997 Binary files /dev/null and b/public/assets/img/screenshot.jpg differ diff --git a/public/site.webmanifest b/public/site.webmanifest index d5b85195..72518218 100644 --- a/public/site.webmanifest +++ b/public/site.webmanifest @@ -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", diff --git a/public/site_apple.webmanifest b/public/site_apple.webmanifest index f0b1e145..e922fca3 100644 --- a/public/site_apple.webmanifest +++ b/public/site_apple.webmanifest @@ -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", diff --git a/public/version b/public/version index dcba8116..f543415a 100644 --- a/public/version +++ b/public/version @@ -1 +1 @@ -1.7.0 (289) \ No newline at end of file +1.7.1 (291) \ No newline at end of file diff --git a/src/components/animationIntersector.ts b/src/components/animationIntersector.ts index 45e2ffb9..905d8d7b 100644 --- a/src/components/animationIntersector.ts +++ b/src/components/animationIntersector.ts @@ -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; diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index 24d9abad..93411214 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -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('.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((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'); diff --git a/src/components/chat/chat.ts b/src/components/chat/chat.ts index 97e04604..d17d5a98 100644 --- a/src/components/chat/chat.ts +++ b/src/components/chat/chat.ts @@ -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 { 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)); }); } diff --git a/src/components/chat/input.ts b/src/components/chat/input.ts index 9de458cf..4c42bb3c 100644 --- a/src/components/chat/input.ts +++ b/src/components/chat/input.ts @@ -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) { diff --git a/src/components/dotRenderer.ts b/src/components/dotRenderer.ts index c2aa6fd6..0c12d37e 100644 --- a/src/components/dotRenderer.ts +++ b/src/components/dotRenderer.ts @@ -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; } diff --git a/src/components/inputFieldAnimated.ts b/src/components/inputFieldAnimated.ts index 7bd9f34f..98e43b11 100644 --- a/src/components/inputFieldAnimated.ts +++ b/src/components/inputFieldAnimated.ts @@ -14,6 +14,7 @@ const USELESS_REG_EXP = new RegExp(`(${BOM})|()`, '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' : ''; } diff --git a/src/components/popups/forward.ts b/src/components/popups/forward.ts index 9c919221..b3e6d68a 100644 --- a/src/components/popups/forward.ts +++ b/src/components/popups/forward.ts @@ -11,13 +11,12 @@ import PopupPickUser from './pickUser'; export default class PopupForward extends PopupPickUser { constructor( - peerIdMids: {[fromPeerId: PeerId]: number[]}, - onSelect?: (peerId: PeerId) => Promise | void, - overrideOnSelect = false + peerIdMids?: {[fromPeerId: PeerId]: number[]}, + onSelect?: (peerId: PeerId) => Promise | 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) { diff --git a/src/components/popups/newMedia.ts b/src/components/popups/newMedia.ts index c8f5b336..20978712 100644 --- a/src/components/popups/newMedia.ts +++ b/src/components/popups/newMedia.ts @@ -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; 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 { @@ -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(); }); } } diff --git a/src/components/popups/reportMessagesConfirm.ts b/src/components/popups/reportMessagesConfirm.ts index 5e5240b5..e8d886a7 100644 --- a/src/components/popups/reportMessagesConfirm.ts +++ b/src/components/popups/reportMessagesConfirm.ts @@ -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', diff --git a/src/components/scrollable.ts b/src/components/scrollable.ts index e8f4109f..b0bf78aa 100644 --- a/src/components/scrollable.ts +++ b/src/components/scrollable.ts @@ -153,8 +153,8 @@ export class ScrollableBase { this.onScrolledBottom = undefined; } - public append(element: HTMLElement) { - this.container.append(element); + public append(...args: Parameters) { + this.container.append(...args); } public scrollIntoViewNew(options: Omit) { diff --git a/src/components/sidebarLeft/index.ts b/src/components/sidebarLeft/index.ts index fe80fc88..1e203d9e 100644 --- a/src/components/sidebarLeft/index.ts +++ b/src/components/sidebarLeft/index.ts @@ -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); diff --git a/src/components/sidebarLeft/tabs/background.ts b/src/components/sidebarLeft/tabs/background.ts index c35a3b58..3ec904b2 100644 --- a/src/components/sidebarLeft/tabs/background.ts +++ b/src/components/sidebarLeft/tabs/background.ts @@ -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 = 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[] = []; let wrapped: ReturnType, size: ReturnType; @@ -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(); let download: Promise | ReturnType; 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; - 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(); diff --git a/src/components/sidebarLeft/tabs/backgroundColor.ts b/src/components/sidebarLeft/tabs/backgroundColor.ts index bf590530..5587a749 100644 --- a/src/components/sidebarLeft/tabs/backgroundColor.ts +++ b/src/components/sidebarLeft/tabs/backgroundColor.ts @@ -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; diff --git a/src/components/sidebarLeft/tabs/editFolder.ts b/src/components/sidebarLeft/tabs/editFolder.ts index 97924dc8..26e7af7a 100644 --- a/src/components/sidebarLeft/tabs/editFolder.ts +++ b/src/components/sidebarLeft/tabs/editFolder.ts @@ -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; + let promise: Promise; 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 diff --git a/src/components/sidebarLeft/tabs/generalSettings.ts b/src/components/sidebarLeft/tabs/generalSettings.ts index 0909feec..7ef699b7 100644 --- a/src/components/sidebarLeft/tabs/generalSettings.ts +++ b/src/components/sidebarLeft/tabs/generalSettings.ts @@ -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) { this.container.classList.add('general-settings-container'); this.setTitle('General'); const section = generateSection.bind(null, this.scrollable); + const promises: Promise[] = []; { 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(); + + 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(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[] = []; + 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(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); } } diff --git a/src/components/wrappers/sticker.ts b/src/components/wrappers/sticker.ts index dbb6f677..e056b514 100644 --- a/src/components/wrappers/sticker.ts +++ b/src/components/wrappers/sticker.ts @@ -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(); @@ -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) { diff --git a/src/components/wrappers/stickerEmoji.ts b/src/components/wrappers/stickerEmoji.ts index abe2b7a2..c63220de 100644 --- a/src/components/wrappers/stickerEmoji.ts +++ b/src/components/wrappers/stickerEmoji.ts @@ -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[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 }); diff --git a/src/components/wrappers/stickerSetThumb.ts b/src/components/wrappers/stickerSetThumb.ts index b5745200..86b635f0 100644 --- a/src/components/wrappers/stickerSetThumb.ts +++ b/src/components/wrappers/stickerSetThumb.ts @@ -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 } } diff --git a/src/config/app.ts b/src/config/app.ts index 23a5a391..c465dd0c 100644 --- a/src/config/app.ts +++ b/src/config/app.ts @@ -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, diff --git a/src/config/state.ts b/src/config/state.ts index 401e369c..7a462bab 100644 --- a/src/config/state.ts +++ b/src/config/state.ts @@ -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 +}>; 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]?: 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 diff --git a/src/environment/installPrompt.ts b/src/environment/installPrompt.ts new file mode 100644 index 00000000..25f1096c --- /dev/null +++ b/src/environment/installPrompt.ts @@ -0,0 +1,2 @@ +const IS_INSTALL_PROMPT_SUPPORTED = 'onbeforeinstallprompt' in window; +export default IS_INSTALL_PROMPT_SUPPORTED; diff --git a/src/environment/standalone.ts b/src/environment/standalone.ts new file mode 100644 index 00000000..fb4cb142 --- /dev/null +++ b/src/environment/standalone.ts @@ -0,0 +1,2 @@ +const IS_STANDALONE = window.matchMedia('(display-mode: standalone)').matches; +export default IS_STANDALONE; diff --git a/src/environment/userAgent.ts b/src/environment/userAgent.ts index d42a9e92..cf05f867 100644 --- a/src/environment/userAgent.ts +++ b/src/environment/userAgent.ts @@ -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; diff --git a/src/environment/videoMimeTypesSupport.ts b/src/environment/videoMimeTypesSupport.ts index 1ef3c9b7..cabcee2c 100644 --- a/src/environment/videoMimeTypesSupport.ts +++ b/src/environment/videoMimeTypesSupport.ts @@ -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 = new Set([ 'image/gif', // have to display it as video 'video/mp4', 'video/webm' diff --git a/src/helpers/canvas/canvasToVideo.ts b/src/helpers/canvas/canvasToVideo.ts new file mode 100644 index 00000000..1b07762b --- /dev/null +++ b/src/helpers/canvas/canvasToVideo.ts @@ -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((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; diff --git a/src/helpers/color.ts b/src/helpers/color.ts index 04658460..43b40c56 100644 --- a/src/helpers/color.ts +++ b/src/helpers/color.ts @@ -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) { diff --git a/src/helpers/dom/installPrompt.ts b/src/helpers/dom/installPrompt.ts new file mode 100644 index 00000000..35a086cf --- /dev/null +++ b/src/helpers/dom/installPrompt.ts @@ -0,0 +1,17 @@ +let callback: () => Promise; +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; +} diff --git a/src/helpers/object/validateInitObject.ts b/src/helpers/object/validateInitObject.ts index 622ac341..3349560a 100644 --- a/src/helpers/object/validateInitObject.ts +++ b/src/helpers/object/validateInitObject.ts @@ -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, + 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); } } } diff --git a/src/helpers/onMediaLoad.ts b/src/helpers/onMediaLoad.ts index 891f1a1b..b0001acc 100644 --- a/src/helpers/onMediaLoad.ts +++ b/src/helpers/onMediaLoad.ts @@ -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}); diff --git a/src/helpers/preloadAnimatedEmojiSticker.ts b/src/helpers/preloadAnimatedEmojiSticker.ts index b4e627d4..ca158e69 100644 --- a/src/helpers/preloadAnimatedEmojiSticker.ts +++ b/src/helpers/preloadAnimatedEmojiSticker.ts @@ -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); diff --git a/src/helpers/themeController.ts b/src/helpers/themeController.ts index 1ab88c30..dd6a0ecf 100644 --- a/src/helpers/themeController.ts +++ b/src/helpers/themeController.ts @@ -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); } } diff --git a/src/index.ts b/src/index.ts index b4a549a6..b1b14dd9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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); diff --git a/src/lang.ts b/src/lang.ts index 5f56ac4a..495acbb0 100644 --- a/src/lang.ts +++ b/src/lang.ts @@ -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', diff --git a/src/lib/appManagers/appDocsManager.ts b/src/lib/appManagers/appDocsManager.ts index 67cb4ffd..80018359 100644 --- a/src/lib/appManagers/appDocsManager.ts +++ b/src/lib/appManagers/appDocsManager.ts @@ -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); diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index 845c5525..ce0adc8c 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -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 { 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); }); // } diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index 436c461e..72873c67 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -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) { diff --git a/src/lib/appManagers/utils/state/loadState.ts b/src/lib/appManagers/utils/state/loadState.ts index 564c0561..e4301ed7 100644 --- a/src/lib/appManagers/utils/state/loadState.ts +++ b/src/lib/appManagers/utils/state/loadState.ts @@ -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 = 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); diff --git a/src/lib/mtproto/apiFileManager.ts b/src/lib/mtproto/apiFileManager.ts index 095de769..77743775 100644 --- a/src/lib/mtproto/apiFileManager.ts +++ b/src/lib/mtproto/apiFileManager.ts @@ -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); } } diff --git a/src/lib/mtproto/mtprotoworker.ts b/src/lib/mtproto/mtprotoworker.ts index 645b4ebe..2f5f56ef 100644 --- a/src/lib/mtproto/mtprotoworker.ts +++ b/src/lib/mtproto/mtprotoworker.ts @@ -62,6 +62,8 @@ class ApiManagerProxy extends MTProtoMessagePort { private tabState: TabState; + public share: ShareData; + public serviceMessagePort: ServiceMessagePort; 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 diff --git a/src/lib/mtproto/webPushApiManager.ts b/src/lib/mtproto/webPushApiManager.ts index b2e8baa4..15271544 100644 --- a/src/lib/mtproto/webPushApiManager.ts +++ b/src/lib/mtproto/webPushApiManager.ts @@ -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']) { diff --git a/src/lib/rlottie/lottieLoader.ts b/src/lib/rlottie/lottieLoader.ts index 28e53193..9a6302ba 100644 --- a/src/lib/rlottie/lottieLoader.ts +++ b/src/lib/rlottie/lottieLoader.ts @@ -151,11 +151,7 @@ export class LottieLoader { ]).then(() => player); } - public async loadAnimationWorker( - params: RLottieOptions, - group: AnimationItemGroup = params.group || '', - middleware?: () => boolean - ): Promise { + public async loadAnimationWorker(params: RLottieOptions): Promise { 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; } diff --git a/src/lib/rlottie/rlottiePlayer.ts b/src/lib/rlottie/rlottiePlayer.ts index 4ca987ba..a314eab7 100644 --- a/src/lib/rlottie/rlottiePlayer.ts +++ b/src/lib/rlottie/rlottiePlayer.ts @@ -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, diff --git a/src/lib/serviceWorker/index.service.ts b/src/lib/serviceWorker/index.service.ts index 506cd393..c80da001 100644 --- a/src/lib/serviceWorker/index.service.ts +++ b/src/lib/serviceWorker/index.service.ts @@ -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(); @@ -137,6 +140,11 @@ const onFetch = (event: FetchEvent): void => { break; } + case 'share': { + onShareFetch(event, params); + break; + } + case 'ping': { event.respondWith(new Response('pong')); break; diff --git a/src/lib/serviceWorker/push.ts b/src/lib/serviceWorker/push.ts index c90b9275..51a830f6 100644 --- a/src/lib/serviceWorker/push.ts +++ b/src/lib/serviceWorker/push.ts @@ -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, Storage extends Record(db, storeName); } - public async get(key: T) { - if(this.cache[key] !== undefined) { + private getDefault(key: T) { + const callback = this.defaults[key]; + return typeof(callback) === 'function' ? callback() : callback; + } + + public get(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; + 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(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(key: T, value: Storage[T]) { @@ -101,7 +115,7 @@ type PushStorage = { push_settings: Partial }; -const getter = new SomethingGetter(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(DATABASE_ push_action_settings: 'Settings' }, push_settings: {} -}); +}; + +const getter = new SomethingGetter(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; 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; diff --git a/src/lib/serviceWorker/serviceMessagePort.ts b/src/lib/serviceWorker/serviceMessagePort.ts index 22eb41bf..9d1c1e42 100644 --- a/src/lib/serviceWorker/serviceMessagePort.ts +++ b/src/lib/serviceWorker/serviceMessagePort.ts @@ -52,6 +52,7 @@ export default class ServiceMessagePort 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 diff --git a/src/lib/serviceWorker/share.ts b/src/lib/serviceWorker/share.ts new file mode 100644 index 00000000..da96f991 --- /dev/null +++ b/src/lib/serviceWorker/share.ts @@ -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); +} diff --git a/src/lib/storages/filters.ts b/src/lib/storages/filters.ts index 4291d3e2..8267b9ea 100644 --- a/src/lib/storages/filters.ts +++ b/src/lib/storages/filters.ts @@ -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 + }); + } }); } diff --git a/src/scss/partials/_button.scss b/src/scss/partials/_button.scss index 81f993a7..c33710c6 100644 --- a/src/scss/partials/_button.scss +++ b/src/scss/partials/_button.scss @@ -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); diff --git a/src/scss/partials/_chat.scss b/src/scss/partials/_chat.scss index f10c21be..db91d798 100644 --- a/src/scss/partials/_chat.scss +++ b/src/scss/partials/_chat.scss @@ -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%; diff --git a/src/scss/partials/_chatBubble.scss b/src/scss/partials/_chatBubble.scss index 6a2a9905..70931888 100644 --- a/src/scss/partials/_chatBubble.scss +++ b/src/scss/partials/_chatBubble.scss @@ -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 { diff --git a/src/scss/partials/_document.scss b/src/scss/partials/_document.scss index 490c087c..a9c754cb 100644 --- a/src/scss/partials/_document.scss +++ b/src/scss/partials/_document.scss @@ -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; diff --git a/src/scss/partials/_leftSidebar.scss b/src/scss/partials/_leftSidebar.scss index c4a11d26..b24ffde5 100644 --- a/src/scss/partials/_leftSidebar.scss +++ b/src/scss/partials/_leftSidebar.scss @@ -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; } } diff --git a/src/scss/partials/_row.scss b/src/scss/partials/_row.scss index 553dc702..0337d19d 100644 --- a/src/scss/partials/_row.scss +++ b/src/scss/partials/_row.scss @@ -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); diff --git a/src/scss/partials/_themes.scss b/src/scss/partials/_themes.scss new file mode 100644 index 00000000..fbd7e274 --- /dev/null +++ b/src/scss/partials/_themes.scss @@ -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; + } + } +} diff --git a/src/scss/partials/popups/_mediaAttacher.scss b/src/scss/partials/popups/_mediaAttacher.scss index f72384cd..227ce950 100644 --- a/src/scss/partials/popups/_mediaAttacher.scss +++ b/src/scss/partials/popups/_mediaAttacher.scss @@ -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; diff --git a/src/scss/partials/popups/_reportMessages.scss b/src/scss/partials/popups/_reportMessages.scss index e65b5463..faf3bbf6 100644 --- a/src/scss/partials/popups/_reportMessages.scss +++ b/src/scss/partials/popups/_reportMessages.scss @@ -15,7 +15,7 @@ } .popup-body { - margin: 1em -.5rem .375rem -.5rem; + margin: 1rem 0 .375rem; overflow: unset; } diff --git a/src/scss/style.scss b/src/scss/style.scss index f515fe00..9cb65998 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -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"; diff --git a/webpack.common.js b/webpack.common.js index d3b052ee..7fe5a457 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -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