From 43cfa9ee123137061dadbf8e71cbd6775d179405 Mon Sep 17 00:00:00 2001 From: Eduard Kuzmenko Date: Sun, 31 Jan 2021 03:52:14 +0200 Subject: [PATCH] Chat background image Fix chat jumping scroll after animation in Chrome Refactor bubble reply rendering Fix reply in discussions Changed subscribers formatting Fix chat resizing again --- src/components/appSearchSuper..ts | 5 +- src/components/chat/bubbles.ts | 347 +++++++++--------- src/components/chat/chat.ts | 33 ++ src/components/chat/input.ts | 8 +- src/components/chat/messageRender.ts | 53 ++- src/components/chat/replies.ts | 2 +- src/components/misc.ts | 7 +- src/components/preloader.ts | 35 +- src/components/sidebarLeft/index.ts | 4 +- src/components/sidebarLeft/tabs/background.ts | 190 ++++++++++ .../sidebarLeft/tabs/generalSettings.ts | 6 + src/components/slider.ts | 4 +- src/components/wrappers.ts | 2 +- src/helpers/blur.ts | 19 +- src/helpers/dom.ts | 6 + src/helpers/heavyQueue.ts | 2 +- src/helpers/number.ts | 4 +- src/lib/appManagers/appChatsManager.ts | 4 +- src/lib/appManagers/appDownloadManager.ts | 20 +- src/lib/appManagers/appImManager.ts | 30 +- src/lib/appManagers/appMessagesManager.ts | 23 +- src/lib/appManagers/appStateManager.ts | 36 +- src/lib/rootScope.ts | 2 + src/scss/partials/_avatar.scss | 14 +- src/scss/partials/_button.scss | 4 +- src/scss/partials/_chat.scss | 30 +- src/scss/partials/_chatBubble.scss | 24 +- src/scss/partials/_leftSidebar.scss | 38 +- src/scss/style.scss | 38 +- 29 files changed, 709 insertions(+), 281 deletions(-) create mode 100644 src/components/sidebarLeft/tabs/background.ts diff --git a/src/components/appSearchSuper..ts b/src/components/appSearchSuper..ts index 93412006..669e26ea 100644 --- a/src/components/appSearchSuper..ts +++ b/src/components/appSearchSuper..ts @@ -386,8 +386,9 @@ export default class AppSearchSuper { }); } - wrapped.images.thumb && wrapped.images.thumb.classList.add('grid-item-media'); - wrapped.images.full && wrapped.images.full.classList.add('grid-item-media'); + [wrapped.images.thumb, wrapped.images.full].filter(Boolean).forEach(image => { + image.classList.add('grid-item-media'); + }); promises.push(wrapped.loadPromises.thumb); diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index 667255f1..cb8d3dd5 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -1,4 +1,4 @@ -import { AppImManager, CHAT_ANIMATION_GROUP } from "../../lib/appManagers/appImManager"; +import { CHAT_ANIMATION_GROUP } from "../../lib/appManagers/appImManager"; import type { AppMessagesManager, HistoryResult, MyMessage } from "../../lib/appManagers/appMessagesManager"; import type { AppStickersManager } from "../../lib/appManagers/appStickersManager"; import type { AppUsersManager } from "../../lib/appManagers/appUsersManager"; @@ -7,7 +7,8 @@ import type { AppPhotosManager } from "../../lib/appManagers/appPhotosManager"; import type { AppDocsManager } from "../../lib/appManagers/appDocsManager"; import type { AppPeersManager } from "../../lib/appManagers/appPeersManager"; import type sessionStorage from '../../lib/sessionStorage'; -import { findUpClassName, cancelEvent, findUpTag, whichChild, getElementByPoint, attachClickEvent, positionElementByIndex } from "../../helpers/dom"; +import type Chat from "./chat"; +import { findUpClassName, cancelEvent, findUpTag, whichChild, getElementByPoint, attachClickEvent, positionElementByIndex, reflowScrollableElement } from "../../helpers/dom"; import { getObjectKeysAndSort } from "../../helpers/object"; import { isTouchSupported } from "../../helpers/touchSupport"; import { logger } from "../../lib/logger"; @@ -33,7 +34,6 @@ import { wrapAlbum, wrapPhoto, wrapVideo, wrapDocument, wrapSticker, wrapPoll, w import { MessageRender } from "./messageRender"; import LazyLoadQueue from "../lazyLoadQueue"; import { AppChatsManager } from "../../lib/appManagers/appChatsManager"; -import Chat from "./chat"; import ListenerSetter from "../../helpers/listenerSetter"; import PollElement from "../poll"; import AudioElement from "../audio"; @@ -87,9 +87,6 @@ export default class ChatBubbles { private preloader: ProgressivePreloader = null; - private scrolledAll: boolean; - public scrolledAllDown: boolean; - private loadedTopTimes = 0; private loadedBottomTimes = 0; @@ -115,6 +112,7 @@ export default class ChatBubbles { public scrollingToNewBubble: HTMLElement; public isFirstLoad = true; + private needReflowScroll: boolean; constructor(private chat: Chat, private appMessagesManager: AppMessagesManager, private appStickersManager: AppStickersManager, private appUsersManager: AppUsersManager, private appInlineBotsManager: AppInlineBotsManager, private appPhotosManager: AppPhotosManager, private appDocsManager: AppDocsManager, private appPeersManager: AppPeersManager, private appChatsManager: AppChatsManager, private storage: typeof sessionStorage) { //this.chat.log.error('Bubbles construction'); @@ -326,7 +324,7 @@ export default class ChatBubbles { }); */ this.needUpdate.forEachReverse((obj, idx) => { - if(obj.replyMid === mid, obj.replyToPeerId === peerId) { + if(obj.replyMid === mid && obj.replyToPeerId === peerId) { const {mid, replyMid} = this.needUpdate.splice(idx, 1)[0]; //this.log('messages_downloaded', mid, replyMid, i, this.needUpdate, this.needUpdate.length, mids, this.bubbles[mid]); @@ -340,8 +338,11 @@ export default class ChatBubbles { delete message.reply_to_mid; // ! WARNING! } - this.renderMessage(message, true, false, bubble, false); - //this.renderMessage(message, true, true, bubble, false); + MessageRender.setReply({ + chat: this.chat, + bubble, + message + }); } }); }); @@ -400,7 +401,7 @@ export default class ChatBubbles { this.listenerSetter.add(rootScope, 'history_append', (e) => { let details = e; - if(!this.scrolledAllDown) { + if(!this.scrollable.loadedAll.bottom) { this.chat.setMessageId(); } else { this.renderNewMessagesByIds([details.messageId], true); @@ -469,7 +470,7 @@ export default class ChatBubbles { if(readed.length) { let maxId = Math.max(...readed); - if(this.scrolledAllDown) { + if(this.scrollable.loadedAll.bottom) { const bubblesMaxId = Math.max(...Object.keys(this.bubbles).map(i => +i)); if(maxId >= bubblesMaxId) { maxId = this.appMessagesManager.getHistoryStorage(this.peerId, this.chat.threadId).maxId || maxId; @@ -505,12 +506,13 @@ export default class ChatBubbles { const onResizeEnd = () => { const height = this.scrollable.container.offsetHeight; - if(height !== wasHeight) { // * fix opening keyboard while ESG is active, offsetHeight will change right between 'start' and this first frame + const isScrolledDown = this.scrollable.isScrolledDown; + if(height !== wasHeight && (!skip || !isScrolledDown)) { // * fix opening keyboard while ESG is active, offsetHeight will change right between 'start' and this first frame part += wasHeight - height; } /* if(DEBUG) { - this.log('resize end', scrolled, this.scrollable.scrollTop, height, this.scrollable.isScrolledDown); + this.log('resize end', scrolled, part, this.scrollable.scrollTop, height, wasHeight, this.scrollable.isScrolledDown); } */ if(part) { @@ -972,8 +974,8 @@ export default class ChatBubbles { /* TEST_SCROLL || */ this.chat.setPeerPromise || this.isHeavyAnimationInProgress || - (top && this.getHistoryTopPromise) || - (!top && this.getHistoryBottomPromise) + (top && (this.getHistoryTopPromise || this.scrollable.loadedAll.top)) || + (!top && (this.getHistoryBottomPromise || this.scrollable.loadedAll.bottom)) ) { return; } @@ -982,28 +984,28 @@ export default class ChatBubbles { const history = Object.keys(this.bubbles).map(id => +id).sort((a, b) => a - b); if(!history.length) return; - if(top && !this.scrolledAll) { - /* if(DEBUG) { - this.log('Will load more (up) history by id:', history[0], 'maxId:', history[history.length - 1], history); - } */ + if(top) { + if(DEBUG) { + this.log('Will load more (up) history by id:', history[0], 'maxId:', history[history.length - 1], justLoad/* , history */); + } /* if(history.length == 75) { this.log('load more', this.scrollable.scrollHeight, this.scrollable.scrollTop, this.scrollable); return; } */ /* false && */this.getHistory(history[0], true, undefined, undefined, justLoad); - } + } else { + //let dialog = this.appMessagesManager.getDialogByPeerId(this.peerId)[0]; + const historyStorage = this.appMessagesManager.getHistoryStorage(this.peerId, this.chat.threadId); + + // if scroll down after search + if(history.indexOf(historyStorage.maxId) !== -1) { + return; + } - if(this.scrolledAllDown) return; - - //let dialog = this.appMessagesManager.getDialogByPeerId(this.peerId)[0]; - const historyStorage = this.appMessagesManager.getHistoryStorage(this.peerId, this.chat.threadId); - - // if scroll down after search - if(!top && history.indexOf(historyStorage.maxId) === -1/* && this.chat.type == 'chat' */) { - /* if(DEBUG) { - this.log('Will load more (down) history by maxId:', history[history.length - 1], history); - } */ + if(DEBUG) { + this.log('Will load more (down) history by id:', history[history.length - 1], justLoad/* , history */); + } /* false && */this.getHistory(history[history.length - 1], false, true, undefined, justLoad); } @@ -1029,7 +1031,7 @@ export default class ChatBubbles { }, 1350); } - if(this.scrollable.getDistanceToEnd() < 300 && this.scrolledAllDown) { + if(this.scrollable.getDistanceToEnd() < 300 && this.scrollable.loadedAll.bottom) { this.bubblesContainer.classList.add('scrolled-down'); this.scrolledDown = true; } else if(this.bubblesContainer.classList.contains('scrolled-down')) { @@ -1044,6 +1046,8 @@ export default class ChatBubbles { public setScroll() { this.scrollable = new Scrollable(this.bubblesContainer/* .firstElementChild */ as HTMLElement, 'IM', /* 10300 */300); + this.scrollable.loadedAll.top = false; + this.scrollable.loadedAll.bottom = false; /* const getScrollOffset = () => { //return Math.round(Math.max(300, appPhotosManager.windowH / 1.5)); @@ -1141,7 +1145,7 @@ export default class ChatBubbles { } public renderNewMessagesByIds(mids: number[], scrolledDown = this.scrolledDown) { - if(!this.scrolledAllDown) { // seems search active or sliced + if(!this.scrollable.loadedAll.bottom) { // seems search active or sliced //this.log('renderNewMessagesByIds: seems search is active, skipping render:', mids); return; } @@ -1295,8 +1299,8 @@ export default class ChatBubbles { public cleanup(bubblesToo = false) { ////console.time('appImManager cleanup'); - this.scrolledAll = false; - this.scrolledAllDown = false; + this.scrollable.loadedAll.top = false; + this.scrollable.loadedAll.bottom = false; if(TEST_SCROLL !== undefined) { TEST_SCROLL = TEST_SCROLL_TIMES; @@ -1520,13 +1524,13 @@ export default class ChatBubbles { // warning if(!lastMsgId || this.bubbles[topMessage] || lastMsgId == topMessage) { - this.scrolledAllDown = true; + this.scrollable.loadedAll.bottom = true; } - this.log('scrolledAllDown:', this.scrolledAllDown); + this.log('scrolledAllDown:', this.scrollable.loadedAll.bottom); //if(!this.unreaded.length && dialog) { // lol - if(this.scrolledAllDown && topMessage) { // lol + if(this.scrollable.loadedAll.bottom && topMessage) { // lol this.onScrolledAllDown(); } @@ -2375,28 +2379,12 @@ export default class ChatBubbles { } if(message.reply_to_mid && message.reply_to_mid !== this.chat.threadId) { - const replyToPeerId = message.reply_to.reply_to_peer_id ? this.appPeersManager.getPeerId(message.reply_to.reply_to_peer_id) : this.peerId; - - let originalMessage = this.appMessagesManager.getMessageByPeer(replyToPeerId, message.reply_to_mid); - let originalPeerTitle: string; - - /////////this.log('message to render reply', originalMessage, originalPeerTitle, bubble, message); - - // need to download separately - if(originalMessage._ == 'messageEmpty') { - //////////this.log('message to render reply empty, need download', message, message.reply_to_mid); - this.appMessagesManager.wrapSingleMessage(replyToPeerId, message.reply_to_mid); - this.needUpdate.push({replyToPeerId, replyMid: message.reply_to_mid, mid: message.mid}); - - originalPeerTitle = 'Loading...'; - } else { - originalPeerTitle = this.appPeersManager.getPeerTitle(originalMessage.fromId || originalMessage.fwdFromId, true) || ''; - } - - const wrapped = wrapReply(originalPeerTitle, originalMessage.message || '', originalMessage); - bubbleContainer.append(wrapped); - //bubbleContainer.insertBefore(, nameContainer); - bubble.classList.add('is-reply'); + MessageRender.setReply({ + chat: this.chat, + bubble, + bubbleContainer, + message + }); } const needAvatar = this.chat.isAnyGroup() && !isOut; @@ -2474,7 +2462,7 @@ export default class ChatBubbles { // commented bot getProfile in getHistory! if(!history/* .filter((id: number) => id > 0) */.length) { if(!isBackLimit) { - this.scrolledAll = true; + this.scrollable.loadedAll.top = true; /* if(this.chat.type === 'discussion') { const serviceStartMessageId = this.appMessagesManager.threadsServiceMessagesIdsStorage[this.peerId + '_' + this.chat.threadId]; @@ -2482,7 +2470,7 @@ export default class ChatBubbles { history.push(this.chat.threadId); } */ } else { - this.scrolledAllDown = true; + this.scrollable.loadedAll.bottom = true; } } @@ -2503,7 +2491,7 @@ export default class ChatBubbles { const historyStorage = this.appMessagesManager.getHistoryStorage(this.peerId, this.chat.threadId); if(history.includes(historyStorage.maxId)) { - this.scrolledAllDown = true; + this.scrollable.loadedAll.bottom = true; } //console.time('appImManager render history'); @@ -2511,7 +2499,9 @@ export default class ChatBubbles { return new Promise((resolve, reject) => { //await new Promise((resolve) => setTimeout(resolve, 1e3)); - //this.log('performHistoryResult: will render some messages:', history.length, this.isHeavyAnimationInProgress); + /* if(DEBUG) { + this.log('performHistoryResult: will render some messages:', history.length, this.isHeavyAnimationInProgress, this.messagesQueuePromise); + } */ const method = (reverse ? history.shift : history.pop).bind(history); @@ -2534,12 +2524,19 @@ export default class ChatBubbles { previousScrollHeightMinusTop = scrollTop; } */ - //this.log('performHistoryResult: messagesQueueOnRender, scrollTop:', scrollTop, scrollHeight, previousScrollHeightMinusTop); + /* if(DEBUG) { + this.log('performHistoryResult: messagesQueueOnRender, scrollTop:', scrollTop, scrollHeight, previousScrollHeightMinusTop); + } */ this.messagesQueueOnRender = undefined; }; //} //} + if(this.needReflowScroll) { + reflowScrollableElement(this.scrollable.container); + this.needReflowScroll = false; + } + while(history.length) { let message = this.chat.getMessage(method()); this.renderMessage(message, reverse, true); @@ -2577,13 +2574,11 @@ export default class ChatBubbles { //isTouchSupported && isApple && (this.scrollable.container.style.overflow = ''); if(isSafari/* && !isAppleMobile */) { // * fix blinking and jumping - this.scrollable.container.style.display = 'none'; - void this.scrollable.container.offsetLeft; // reflow - this.scrollable.container.style.display = ''; + reflowScrollableElement(this.scrollable.container); } /* if(DEBUG) { - this.log('performHistoryResult: have set up scrollTop:', newScrollTop, this.scrollable.scrollTop, this.isHeavyAnimationInProgress); + this.log('performHistoryResult: have set up scrollTop:', newScrollTop, this.scrollable.scrollTop, this.scrollable.scrollHeight, this.isHeavyAnimationInProgress); } */ } @@ -2629,8 +2624,8 @@ export default class ChatBubbles { return promise; } else if(this.chat.type === 'scheduled') { return this.appMessagesManager.getScheduledMessages(this.peerId).then(mids => { - this.scrolledAll = true; - this.scrolledAllDown = true; + this.scrollable.loadedAll.top = true; + this.scrollable.loadedAll.bottom = true; return {history: mids.slice().reverse()}; }); } @@ -2650,8 +2645,8 @@ export default class ChatBubbles { //console.time('appImManager call getHistory'); const pageCount = this.appPhotosManager.windowH / 38/* * 1.25 */ | 0; //const loadCount = Object.keys(this.bubbles).length > 0 ? 50 : pageCount; - //const realLoadCount = Object.keys(this.bubbles).length > 0 || additionMsgId ? Math.max(40, pageCount) : pageCount;//const realLoadCount = 50; - const realLoadCount = pageCount;//const realLoadCount = 50; + const realLoadCount = Object.keys(this.bubbles).length > 0 || additionMsgId ? Math.max(40, pageCount) : pageCount;//const realLoadCount = 50; + //const realLoadCount = pageCount;//const realLoadCount = 50; let loadCount = realLoadCount; /* if(TEST_SCROLL) { @@ -2729,7 +2724,7 @@ export default class ChatBubbles { const serviceStartMessageId = this.appMessagesManager.threadsServiceMessagesIdsStorage[this.peerId + '_' + this.chat.threadId]; if(serviceStartMessageId) historyResult.history.push(serviceStartMessageId); historyResult.history.push(...this.chat.getMidsByMid(this.chat.threadId).reverse()); - this.scrolledAll = true; + this.scrollable.loadedAll.top = true; } } }; @@ -2785,123 +2780,127 @@ export default class ChatBubbles { const waitPromise = isAdditionRender ? processPromise(resultPromise) : promise; if(isFirstMessageRender && rootScope.settings.animationsEnabled/* && false */) { + let times = isAdditionRender ? 2 : 1; this.messagesQueueOnRenderAdditional = () => { - if(Object.keys(this.bubbles).length > 1) { - let sortedMids = getObjectKeysAndSort(this.bubbles, 'desc'); + this.log('ship went past rocks of magnets'); - if(isAdditionRender && additionMsgIds.length) { - sortedMids = sortedMids.filter(mid => !additionMsgIds.includes(mid)); - } + if(--times) return; - let targetMid: number; - if(backLimit) { - targetMid = maxId; - } else { - if(additionMsgId) { - targetMid = additionMsgId; - } else { // * if maxId === 0 - targetMid = Math.max(...sortedMids); - } + this.messagesQueueOnRenderAdditional = undefined; + if(!Object.keys(this.bubbles).length) { + return; + } + + let sortedMids = getObjectKeysAndSort(this.bubbles, 'desc'); + + if(isAdditionRender && additionMsgIds.length) { + sortedMids = sortedMids.filter(mid => !additionMsgIds.includes(mid)); + } + + let targetMid: number; + if(backLimit) { + targetMid = maxId; + } else { + if(additionMsgId) { + targetMid = additionMsgId; + } else { // * if maxId === 0 + targetMid = Math.max(...sortedMids); } + } - const topIds = sortedMids.slice(sortedMids.findIndex(mid => targetMid > mid)); - const middleIds = isAdditionRender ? [] : [targetMid]; - const bottomIds = isAdditionRender ? [] : sortedMids.slice(0, sortedMids.findIndex(mid => targetMid >= mid)).reverse(); - + const topIds = sortedMids.slice(sortedMids.findIndex(mid => targetMid > mid)); + const middleIds = isAdditionRender ? [] : [targetMid]; + const bottomIds = isAdditionRender ? [] : sortedMids.slice(0, sortedMids.findIndex(mid => targetMid >= mid)).reverse(); + + if(DEBUG) { this.log('getHistory: targeting mid:', targetMid, maxId, additionMsgId, topIds.map(m => this.appMessagesManager.getServerMessageId(m)), bottomIds.map(m => this.appMessagesManager.getServerMessageId(m))); + } - const setBubbles: HTMLElement[] = []; - - const delay = isAdditionRender ? 10 : 40; - const offsetIndex = isAdditionRender ? 0 : 1; - const animateAsLadder = (mids: number[], offsetIndex = 0) => { - const animationPromise = deferredPromise(); - let lastMsDelay = 0; - mids.forEach((mid, idx) => { - if(!this.bubbles[mid]) { - this.log.warn('animateAsLadder: no bubble by mid:', mid); - return; - } - - const contentWrapper = this.bubbles[mid].lastElementChild as HTMLElement; - - lastMsDelay = ((idx + offsetIndex) || 0.1) * delay; - //lastMsDelay = (idx + offsetIndex) * delay; - //lastMsDelay = (idx || 0.1) * 1000; - //if(idx || isSafari) { - // ! 0.1 = 1ms задержка для Safari, без этого первое сообщение над самым нижним может появиться позже другого с animation-delay, LOL ! - //contentWrapper.style.animationDelay = lastMsDelay + 'ms'; - //} - - contentWrapper.classList.add('zoom-fade'); - contentWrapper.style.transitionDelay = lastMsDelay + 'ms'; - - if(idx === (mids.length - 1)) { - const onTransitionEnd = (e: TransitionEvent) => { - if(e.target !== contentWrapper) { - return; - } - - //contentWrapper.style.animationDelay = ''; - //contentWrapper.classList.remove('zoom-fade'); - - //this.log('onTransitionEnd', e); - - animationPromise.resolve(); - - contentWrapper.removeEventListener('transitionend', onTransitionEnd); - }; - - contentWrapper.addEventListener('transitionend', onTransitionEnd); - } - - //this.log('supa', bubble); + const setBubbles: HTMLElement[] = []; - setBubbles.push(contentWrapper); + const delay = isAdditionRender ? 10 : 40; + const offsetIndex = isAdditionRender ? 0 : 1; + const animateAsLadder = (mids: number[], offsetIndex = 0) => { + const animationPromise = deferredPromise(); + let lastMsDelay = 0; + mids.forEach((mid, idx) => { + if(!this.bubbles[mid]) { + this.log.warn('animateAsLadder: no bubble by mid:', mid); + return; + } - fastRaf(() => { - contentWrapper.classList.remove('zoom-fade'); - }); - }); + const contentWrapper = this.bubbles[mid].lastElementChild as HTMLElement; - if(!mids.length) { - animationPromise.resolve(); + lastMsDelay = ((idx + offsetIndex) || 0.1) * delay; + //lastMsDelay = (idx + offsetIndex) * delay; + //lastMsDelay = (idx || 0.1) * 1000; + + contentWrapper.classList.add('zoom-fade'); + contentWrapper.style.transitionDelay = lastMsDelay + 'ms'; + + if(idx === (mids.length - 1)) { + const onTransitionEnd = (e: TransitionEvent) => { + if(e.target !== contentWrapper) { + return; + } + + animationPromise.resolve(); + contentWrapper.removeEventListener('transitionend', onTransitionEnd); + }; + + contentWrapper.addEventListener('transitionend', onTransitionEnd); } + + //this.log('supa', bubble); - return {lastMsDelay, animationPromise}; - }; + setBubbles.push(contentWrapper); + }); + + if(!mids.length) { + animationPromise.resolve(); + } + + return {lastMsDelay, animationPromise}; + }; + + const topRes = animateAsLadder(topIds, offsetIndex); + const middleRes = animateAsLadder(middleIds); + const bottomRes = animateAsLadder(bottomIds, offsetIndex); + const promises = [topRes.animationPromise, middleRes.animationPromise, bottomRes.animationPromise]; + const delays: number[] = [topRes.lastMsDelay, middleRes.lastMsDelay, bottomRes.lastMsDelay]; - const topRes = animateAsLadder(topIds, offsetIndex); - const middleRes = animateAsLadder(middleIds); - const bottomRes = animateAsLadder(bottomIds, offsetIndex); - const promises = [topRes.animationPromise, middleRes.animationPromise, bottomRes.animationPromise]; - const delays: number[] = [topRes.lastMsDelay, middleRes.lastMsDelay, bottomRes.lastMsDelay]; - - let promise: Promise; - if(topIds.length || middleIds.length || bottomIds.length) { - promise = Promise.all(promises); - promise.then(() => { - fastRaf(() => { - setBubbles.forEach(contentWrapper => { - contentWrapper.style.transitionDelay = ''; - }); + fastRaf(() => { + setBubbles.forEach(contentWrapper => { + contentWrapper.classList.remove('zoom-fade'); + }); + }); + + let promise: Promise; + if(topIds.length || middleIds.length || bottomIds.length) { + promise = Promise.all(promises); + promise.then(() => { + fastRaf(() => { + setBubbles.forEach(contentWrapper => { + contentWrapper.style.transitionDelay = ''; }); }); - dispatchHeavyAnimationEvent(promise, Math.max(...delays) + 200); // * 200 - transition time - } - (promise || Promise.resolve()).then(() => { - setTimeout(() => { // preload messages - this.loadMoreHistory(reverse, true); - }, 0); + // ! в хроме, каким-то образом из-за zoom-fade класса начинает прыгать скролл при подгрузке сообщений вверх, + // ! т.е. скролл не ставится, так же, как в сафари при translateZ на блок выше scrollable + if(!isSafari) { + this.needReflowScroll = true; + } }); + dispatchHeavyAnimationEvent(promise, Math.max(...delays) + 200); // * 200 - transition time } - if(!isAdditionRender) { - this.messagesQueueOnRenderAdditional = undefined; - } + (promise || Promise.resolve()).then(() => { + setTimeout(() => { // preload messages + this.loadMoreHistory(reverse, true); + }, 0); + }); }; } else { this.messagesQueueOnRenderAdditional = undefined; @@ -2938,7 +2937,7 @@ export default class ChatBubbles { //ids = ids.slice(-removeCount); //ids = ids.slice(removeCount * 2); ids = ids.slice(safeCount); - this.scrolledAllDown = false; + this.scrollable.loadedAll.bottom = false; //this.log('getHistory: slice bottom messages:', ids.length, loadCount); //this.getHistoryBottomPromise = undefined; // !WARNING, это нужно для обратной загрузки истории, если запрос словил флуд @@ -2946,7 +2945,7 @@ export default class ChatBubbles { //ids = ids.slice(0, removeCount); //ids = ids.slice(0, ids.length - (removeCount * 2)); ids = ids.slice(0, ids.length - safeCount); - this.scrolledAll = false; + this.scrollable.loadedAll.top = false; //this.log('getHistory: slice up messages:', ids.length, loadCount); //this.getHistoryTopPromise = undefined; // !WARNING, это нужно для обратной загрузки истории, если запрос словил флуд @@ -2962,9 +2961,9 @@ export default class ChatBubbles { // preload more //if(!isFirstMessageRender) { if(this.chat.type === 'chat'/* || this.chat.type === 'discussion' */) { - const storage = this.appMessagesManager.getHistoryStorage(peerId, this.chat.threadId); + /* const storage = this.appMessagesManager.getHistoryStorage(peerId, this.chat.threadId); const isMaxIdInHistory = storage.history.indexOf(maxId) !== -1; - if(isMaxIdInHistory) { // * otherwise it is a search or jump + if(isMaxIdInHistory || true) { // * otherwise it is a search or jump */ setTimeout(() => { if(reverse) { this.loadMoreHistory(true, true); @@ -2972,7 +2971,7 @@ export default class ChatBubbles { this.loadMoreHistory(false, true); } }, 0); - } + //} } //} }); diff --git a/src/components/chat/chat.ts b/src/components/chat/chat.ts index 0bc334f9..86846d80 100644 --- a/src/components/chat/chat.ts +++ b/src/components/chat/chat.ts @@ -24,6 +24,9 @@ import ChatInput from "./input"; import ChatSelection from "./selection"; import ChatTopbar from "./topbar"; import { REPLIES_PEER_ID } from "../../lib/mtproto/mtproto_config"; +import { renderImageFromUrl } from "../misc"; +import SetTransition from "../singleTransition"; +import { fastRaf } from "../../helpers/schedulers"; export type ChatType = 'chat' | 'pinned' | 'replies' | 'discussion' | 'scheduled'; @@ -68,6 +71,36 @@ export default class Chat extends EventListenerBase<{ this.appImManager.chatsContainer.append(this.container); } + public setBackground(url: string): Promise { + const item = document.createElement('div'); + item.classList.add('chat-background-item'); + + return new Promise((resolve) => { + const cb = () => { + const prev = this.backgroundEl.children[this.backgroundEl.childElementCount - 1] as HTMLElement; + this.backgroundEl.append(item); + + // * одного недостаточно, при обновлении страницы все равно фон появляется неплавно + // ! с requestAnimationFrame лучше, но все равно иногда моргает, так что использую два фаста. + fastRaf(() => { + fastRaf(() => { + SetTransition(item, 'is-visible', true, 200, prev ? () => { + prev.remove(); + } : null); + }); + }); + + resolve(); + }; + + if(url) { + renderImageFromUrl(item, url, cb); + } else { + cb(); + } + }); + } + public setType(type: ChatType) { this.type = type; diff --git a/src/components/chat/input.ts b/src/components/chat/input.ts index 3ac145f4..f5012efb 100644 --- a/src/components/chat/input.ts +++ b/src/components/chat/input.ts @@ -13,13 +13,12 @@ import apiManager from "../../lib/mtproto/mtprotoworker"; //import Recorder from '../opus-recorder/dist/recorder.min'; import opusDecodeController from "../../lib/opusDecodeController"; import RichTextProcessor from "../../lib/richtextprocessor"; -import { attachClickEvent, blurActiveElement, cancelEvent, cancelSelection, findUpClassName, getRichValue, getSelectedNodes, isInputEmpty, markdownTags, MarkdownType, placeCaretAtEnd, isSendShortcutPressed, fixSafariStickyInput } from "../../helpers/dom"; +import { attachClickEvent, blurActiveElement, cancelEvent, cancelSelection, findUpClassName, getRichValue, isInputEmpty, markdownTags, MarkdownType, placeCaretAtEnd, isSendShortcutPressed } from "../../helpers/dom"; import { ButtonMenuItemOptions } from '../buttonMenu'; import emoticonsDropdown from "../emoticonsDropdown"; import PopupCreatePoll from "../popups/createPoll"; import PopupForward from '../popups/forward'; import PopupNewMedia from '../popups/newMedia'; -import Scrollable from "../scrollable"; import { toast } from "../toast"; import { wrapReply } from "../wrappers"; import InputField from '../inputField'; @@ -36,7 +35,6 @@ import rootScope from '../../lib/rootScope'; import PopupPinMessage from '../popups/unpinMessage'; import { debounce } from '../../helpers/schedulers'; import { tsNow } from '../../helpers/date'; -import { isSafari } from '../../helpers/userAgent'; const RECORD_MIN_TIME = 500; const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this group.'; @@ -756,7 +754,7 @@ export default class ChatInput { if(this.chat.type === 'chat' || this.chat.type === 'discussion') { this.listenerSetter.add(this.messageInput, 'focusin', () => { - if(this.chat.bubbles.scrolledAllDown) { + if(this.chat.bubbles.scrollable.loadedAll.bottom) { this.appMessagesManager.readAllHistory(this.chat.peerId, this.chat.threadId); } }); @@ -1448,7 +1446,7 @@ export default class ChatInput { this.willSendWebPage = null; } - this.replyToMsgId = this.chat.threadId; + this.replyToMsgId = undefined; this.forwardingMids.length = 0; this.forwardingFromPeerId = 0; this.editMsgId = undefined; diff --git a/src/components/chat/messageRender.ts b/src/components/chat/messageRender.ts index 920d9891..ce31381a 100644 --- a/src/components/chat/messageRender.ts +++ b/src/components/chat/messageRender.ts @@ -1,6 +1,7 @@ import { getFullDate } from "../../helpers/date"; import { formatNumber } from "../../helpers/number"; import RichTextProcessor from "../../lib/richtextprocessor"; +import { wrapReply } from "../wrappers"; import Chat from "./chat"; import RepliesElement from "./replies"; @@ -34,7 +35,7 @@ export namespace MessageRender { } } - if(message.edit_date) { + if(message.edit_date && chat.type !== 'scheduled') { bubble.classList.add('is-edited'); time = 'edited ' + time; } @@ -74,4 +75,54 @@ export namespace MessageRender { bubbleContainer.prepend(repliesFooter); return isFooter; }; + + export const setReply = ({chat, bubble, bubbleContainer, message}: { + chat: Chat, + bubble: HTMLElement, + bubbleContainer?: HTMLElement, + message: any + }) => { + const isReplacing = !bubbleContainer; + if(isReplacing) { + bubbleContainer = bubble.querySelector('.bubble-content'); + } + + const currentReplyDiv = isReplacing ? bubbleContainer.querySelector('.reply') : null; + if(!message.reply_to_mid) { + if(currentReplyDiv) { + currentReplyDiv.remove(); + } + + bubble.classList.remove('is-reply'); + return; + } + + + const replyToPeerId = message.reply_to.reply_to_peer_id ? chat.appPeersManager.getPeerId(message.reply_to.reply_to_peer_id) : chat.peerId; + + let originalMessage = chat.appMessagesManager.getMessageByPeer(replyToPeerId, message.reply_to_mid); + let originalPeerTitle: string; + + /////////this.log('message to render reply', originalMessage, originalPeerTitle, bubble, message); + + // need to download separately + if(originalMessage._ === 'messageEmpty') { + //////////this.log('message to render reply empty, need download', message, message.reply_to_mid); + chat.appMessagesManager.wrapSingleMessage(replyToPeerId, message.reply_to_mid); + chat.bubbles.needUpdate.push({replyToPeerId, replyMid: message.reply_to_mid, mid: message.mid}); + + originalPeerTitle = 'Loading...'; + } else { + originalPeerTitle = chat.appPeersManager.getPeerTitle(originalMessage.fromId || originalMessage.fwdFromId, true) || ''; + } + + const wrapped = wrapReply(originalPeerTitle, originalMessage.message || '', originalMessage); + if(currentReplyDiv) { + currentReplyDiv.replaceWith(wrapped); + } else { + bubbleContainer.append(wrapped); + } + //bubbleContainer.insertBefore(, nameContainer); + bubble.classList.add('is-reply'); + }; } diff --git a/src/components/chat/replies.ts b/src/components/chat/replies.ts index ad321286..b97aa0df 100644 --- a/src/components/chat/replies.ts +++ b/src/components/chat/replies.ts @@ -62,7 +62,7 @@ export default class RepliesElement extends HTMLElement { if(!avatarElem) { avatarElem = new AvatarElement(); avatarElem.setAttribute('dialog', '0'); - avatarElem.classList.add('avatar-32'); + avatarElem.classList.add('avatar-30'); if(this.loadPromises) { avatarElem.loadPromises = this.loadPromises; diff --git a/src/components/misc.ts b/src/components/misc.ts index 846fbbad..eecc08c7 100644 --- a/src/components/misc.ts +++ b/src/components/misc.ts @@ -17,7 +17,10 @@ const set = (elem: HTMLElement | HTMLImageElement | SVGImageElement | HTMLVideoE // проблема функции в том, что она не подходит для ссылок, пригодна только для blob'ов, потому что обычным ссылкам нужен 'load' каждый раз. export function renderImageFromUrl(elem: HTMLElement | HTMLImageElement | SVGImageElement | HTMLVideoElement, url: string, callback?: (err?: Event) => void, useCache = false): boolean { if(((loadedURLs[url]/* && false */) && useCache) || elem instanceof HTMLVideoElement) { - set(elem, url); + if(elem) { + set(elem, url); + } + callback && callback(); return true; } else { @@ -27,7 +30,7 @@ export function renderImageFromUrl(elem: HTMLElement | HTMLImageElement | SVGIma loader.src = url; //let perf = performance.now(); loader.addEventListener('load', () => { - if(!isImage) { + if(!isImage && elem) { set(elem, url); } diff --git a/src/components/preloader.ts b/src/components/preloader.ts index aafb3edc..e07bf8a4 100644 --- a/src/components/preloader.ts +++ b/src/components/preloader.ts @@ -139,29 +139,42 @@ export default class ProgressivePreloader { this.promise = promise; const tempId = --this.tempId; + const startTime = Date.now(); const onEnd = (err: Error) => { promise.notify = null; - if(tempId === this.tempId) { - if(!err && this.cancelable) { - this.setProgress(100); + if(tempId !== this.tempId) { + return; + } + + const elapsedTime = Date.now() - startTime; + + //console.log('[PP]: end', this.detached, performance.now()); + + if(!err && this.cancelable) { + this.setProgress(100); + const delay = TRANSITION_TIME * 0.75; + + if(elapsedTime < delay) { + this.detach(); + } else { setTimeout(() => { // * wait for transition complete if(tempId === this.tempId) { this.detach(); } - }, TRANSITION_TIME * 0.75); + }, delay); + } + } else { + if(this.tryAgainOnFail) { + this.setManual(); } else { - if(this.tryAgainOnFail) { - this.setManual(); - } else { - this.detach(); - } + this.detach(); } - - this.promise = promise = null; } + + this.promise = promise = null; }; promise diff --git a/src/components/sidebarLeft/index.ts b/src/components/sidebarLeft/index.ts index 83978ccb..fc3cc312 100644 --- a/src/components/sidebarLeft/index.ts +++ b/src/components/sidebarLeft/index.ts @@ -446,7 +446,7 @@ export class SettingSection { public title: HTMLElement; public caption: HTMLElement; - constructor(name: string, caption?: string) { + constructor(name?: string, caption?: string) { this.container = document.createElement('div'); this.container.classList.add('sidebar-left-section'); @@ -473,7 +473,7 @@ export class SettingSection { } } -export const generateSection = (appendTo: Scrollable, name: string, caption?: string) => { +export const generateSection = (appendTo: Scrollable, name?: string, caption?: string) => { const section = new SettingSection(name, caption); appendTo.append(section.container); return section.content; diff --git a/src/components/sidebarLeft/tabs/background.ts b/src/components/sidebarLeft/tabs/background.ts new file mode 100644 index 00000000..892eb692 --- /dev/null +++ b/src/components/sidebarLeft/tabs/background.ts @@ -0,0 +1,190 @@ +import { generateSection } from ".."; +import blur from "../../../helpers/blur"; +import { deferredPromise } from "../../../helpers/cancellablePromise"; +import { attachClickEvent, findUpClassName } from "../../../helpers/dom"; +import { AccountWallPapers, WallPaper } from "../../../layer"; +import appDocsManager, { MyDocument } from "../../../lib/appManagers/appDocsManager"; +import appDownloadManager from "../../../lib/appManagers/appDownloadManager"; +import appImManager from "../../../lib/appManagers/appImManager"; +import appStateManager from "../../../lib/appManagers/appStateManager"; +import apiManager from "../../../lib/mtproto/mtprotoworker"; +import rootScope from "../../../lib/rootScope"; +import Button from "../../button"; +import CheckboxField from "../../checkbox"; +import ProgressivePreloader from "../../preloader"; +import SidebarSlider, { SliderSuperTab } from "../../slider"; +import { wrapPhoto } from "../../wrappers"; + +export default class AppBackgroundTab extends SliderSuperTab { + constructor(slider: SidebarSlider) { + super(slider, true); + } + + init() { + this.container.classList.add('background-container'); + this.title.innerText = 'Chat Background'; + + { + const container = generateSection(this.scrollable); + + const uploadButton = Button('btn-primary btn-transparent', {icon: 'cameraadd', text: 'Upload Wallpaper'}); + const colorButton = Button('btn-primary btn-transparent', {icon: 'colorize', text: 'Set a Color'}); + + const blurCheckboxField = CheckboxField('Blur Wallpaper Image', 'blur', false, 'settings.background.blur'); + blurCheckboxField.input.addEventListener('change', () => { + const active = grid.querySelector('.active') as HTMLElement; + if(!active) return; + + // * wait for animation end + setTimeout(() => { + setBackgroundDocument(active.dataset.slug, appDocsManager.getDoc(active.dataset.docId)); + }, 100); + }); + + container.append(uploadButton, colorButton, blurCheckboxField.label); + } + + const grid = document.createElement('div'); + grid.classList.add('grid'); + + const saveToCache = (url: string) => { + fetch(url).then(response => { + appDownloadManager.cacheStorage.save('background-image', response); + }); + }; + + const setBackgroundDocument = (slug: string, doc: MyDocument) => { + rootScope.settings.background.slug = slug; + rootScope.settings.background.type = 'image'; + appStateManager.pushToState('settings', rootScope.settings); + + const download = appDocsManager.downloadDoc(doc, appImManager.chat.bubbles ? appImManager.chat.bubbles.lazyLoadQueue.queueId : 0); + + const deferred = deferredPromise(); + deferred.addNotifyListener = download.addNotifyListener; + deferred.cancel = download.cancel; + + download.then(() => { + if(rootScope.settings.background.slug !== slug || rootScope.settings.background.type !== 'image') { + return; + } + + if(rootScope.settings.background.blur) { + setTimeout(() => { + blur(doc.url, 12, 4) + .then(url => { + if(rootScope.settings.background.slug !== slug || rootScope.settings.background.type !== 'image') { + return; + } + + saveToCache(url); + return appImManager.setBackground(url); + }) + .then(deferred.resolve); + }, 200); + } else { + saveToCache(doc.url); + appImManager.setBackground(doc.url).then(deferred.resolve); + } + }); + + return deferred; + }; + + const setActive = () => { + const active = grid.querySelector('.active'); + const target = rootScope.settings.background.type === 'image' ? grid.querySelector(`.grid-item[data-slug="${rootScope.settings.background.slug}"]`) : null; + if(active === target) { + return; + } + + if(active) { + active.classList.remove('active'); + } + + if(target) { + target.classList.add('active'); + } + }; + + rootScope.on('background_change', setActive); + + apiManager.invokeApiHashable('account.getWallPapers').then((accountWallpapers) => { + const wallpapers = (accountWallpapers as AccountWallPapers.accountWallPapers).wallpapers as WallPaper.wallPaper[]; + wallpapers.forEach((wallpaper) => { + if(wallpaper.pFlags.pattern || (wallpaper.document as MyDocument).mime_type.indexOf('application/') === 0) { + return; + } + + wallpaper.document = appDocsManager.saveDoc(wallpaper.document); + + const container = document.createElement('div'); + container.classList.add('grid-item'); + + const wrapped = wrapPhoto({ + photo: wallpaper.document, + message: null, + container: container, + boxWidth: 0, + boxHeight: 0, + withoutPreloader: true + }); + + [wrapped.images.thumb, wrapped.images.full].filter(Boolean).forEach(image => { + image.classList.add('grid-item-media'); + }); + + container.dataset.docId = wallpaper.document.id; + container.dataset.slug = wallpaper.slug; + + if(rootScope.settings.background.type === 'image' && rootScope.settings.background.slug === wallpaper.slug) { + container.classList.add('active'); + } + + grid.append(container); + }); + + let clicked: Set = new Set(); + attachClickEvent(grid, (e) => { + const target = findUpClassName(e.target, 'grid-item') as HTMLElement; + if(!target) return; + + const {docId, slug} = target.dataset; + if(clicked.has(docId)) return; + clicked.add(docId); + + const preloader = new ProgressivePreloader({ + cancelable: true, + tryAgainOnFail: false + }); + + const doc = appDocsManager.getDoc(docId); + + const load = () => { + const promise = setBackgroundDocument(slug, doc); + if(!doc.url || rootScope.settings.background.blur) { + preloader.attach(target, true, promise); + } + }; + + preloader.construct(); + + attachClickEvent(target, (e) => { + if(preloader.preloader.parentElement) { + preloader.onClick(e); + } else { + load(); + } + }); + + load(); + + console.log(doc); + }); + + console.log(accountWallpapers); + }); + + this.scrollable.append(grid); + } +} diff --git a/src/components/sidebarLeft/tabs/generalSettings.ts b/src/components/sidebarLeft/tabs/generalSettings.ts index 525f7e39..e65e0947 100644 --- a/src/components/sidebarLeft/tabs/generalSettings.ts +++ b/src/components/sidebarLeft/tabs/generalSettings.ts @@ -9,6 +9,8 @@ import appStateManager from "../../../lib/appManagers/appStateManager"; import rootScope from "../../../lib/rootScope"; import { isApple } from "../../../helpers/userAgent"; import Row from "../../row"; +import { attachClickEvent } from "../../../helpers/dom"; +import AppBackgroundTab from "./background"; export class RangeSettingSelector { public container: HTMLDivElement; @@ -72,6 +74,10 @@ export default class AppGeneralSettingsTab extends SliderSuperTab { const chatBackgroundButton = Button('btn-primary btn-transparent', {icon: 'photo', text: 'Chat Background'}); + attachClickEvent(chatBackgroundButton, () => { + new AppBackgroundTab(this.slider).open(); + }); + const animationsCheckboxField = CheckboxField('Enable Animations', 'animations', false, 'settings.animationsEnabled'); container.append(range.container, chatBackgroundButton, animationsCheckboxField.label); diff --git a/src/components/slider.ts b/src/components/slider.ts index 9484cb88..5d121cf0 100644 --- a/src/components/slider.ts +++ b/src/components/slider.ts @@ -69,12 +69,12 @@ export class SliderSuperTab implements SliderTab { } - /* public onCloseAfterTimeout() { + public onCloseAfterTimeout() { if(this.destroyable) { // ! WARNING, пока что это будет работать только с самой последней внутренней вкладкой ! delete this.slider.tabs[this.id]; this.container.remove(); } - } */ + } } const TRANSITION_TIME = 250; diff --git a/src/components/wrappers.ts b/src/components/wrappers.ts index a9a93f1d..98d1f80d 100644 --- a/src/components/wrappers.ts +++ b/src/components/wrappers.ts @@ -438,7 +438,7 @@ export function wrapDocument({message, withTime, fontWeight, voiceAsMusic, showS const preloader = new ProgressivePreloader(); const load = () => { - const download = appDocsManager.saveDocFile(doc, appImManager.chat.bubbles.lazyLoadQueue.queueId); + const download = appDocsManager.saveDocFile(doc, appImManager.chat.bubbles ? appImManager.chat.bubbles.lazyLoadQueue.queueId : 0); download.then(() => { downloadDiv.classList.add('downloaded'); diff --git a/src/helpers/blur.ts b/src/helpers/blur.ts index 5872304f..d0f8b8c0 100644 --- a/src/helpers/blur.ts +++ b/src/helpers/blur.ts @@ -1,14 +1,18 @@ +import { DEBUG } from '../lib/mtproto/mtproto_config'; import fastBlur from '../vendor/fastBlur'; import pushHeavyTask from './heavyQueue'; const RADIUS = 2; const ITERATIONS = 2; -function processBlur(dataUri: string) { +function processBlur(dataUri: string, radius: number, iterations: number) { return new Promise((resolve) => { const img = new Image(); - console.log('[blur] start'); + const perf = performance.now(); + if(DEBUG) { + console.log('[blur] start'); + } img.onload = () => { const canvas = document.createElement('canvas'); @@ -18,12 +22,15 @@ function processBlur(dataUri: string) { const ctx = canvas.getContext('2d')!; ctx.drawImage(img, 0, 0); - fastBlur(ctx, 0, 0, canvas.width, canvas.height, RADIUS, ITERATIONS); + fastBlur(ctx, 0, 0, canvas.width, canvas.height, radius, iterations); //resolve(canvas.toDataURL()); canvas.toBlob(blob => { resolve(URL.createObjectURL(blob)); - console.log('[blur] end'); + + if(DEBUG) { + console.log(`[blur] end, radius: ${radius}, iterations: ${iterations}, time: ${performance.now() - perf}`); + } }); }; @@ -31,11 +38,11 @@ function processBlur(dataUri: string) { }); } -export default function blur(dataUri: string) { +export default function blur(dataUri: string, radius: number = RADIUS, iterations: number = ITERATIONS) { return new Promise((resolve) => { //return resolve(dataUri); pushHeavyTask({ - items: [dataUri], + items: [[dataUri, radius, iterations]], context: null, process: processBlur }).then(results => { diff --git a/src/helpers/dom.ts b/src/helpers/dom.ts index d213dae2..b8cf5c00 100644 --- a/src/helpers/dom.ts +++ b/src/helpers/dom.ts @@ -756,3 +756,9 @@ export function isSendShortcutPressed(e: KeyboardEvent) { return false; } + +export function reflowScrollableElement(element: HTMLElement) { + element.style.display = 'none'; + void element.offsetLeft; // reflow + element.style.display = ''; +} diff --git a/src/helpers/heavyQueue.ts b/src/helpers/heavyQueue.ts index 64eac93d..0f248249 100644 --- a/src/helpers/heavyQueue.ts +++ b/src/helpers/heavyQueue.ts @@ -50,7 +50,7 @@ function timedChunk(queue: HeavyQueue) { do { await getHeavyAnimationPromise(); - const possiblePromise = queue.process.call(queue.context, todo.shift()); + const possiblePromise = queue.process.apply(queue.context, todo.shift()); let realResult: T; if(possiblePromise instanceof Promise) { try { diff --git a/src/helpers/number.ts b/src/helpers/number.ts index a6419e19..12ebab23 100644 --- a/src/helpers/number.ts +++ b/src/helpers/number.ts @@ -1,6 +1,6 @@ -export function numberWithCommas(x: number) { +export function numberThousandSplitter(x: number, joiner = ',') { const parts = x.toString().split("."); - parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ","); + parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, joiner); return parts.join("."); } diff --git a/src/lib/appManagers/appChatsManager.ts b/src/lib/appManagers/appChatsManager.ts index ac9b30fe..28fea860 100644 --- a/src/lib/appManagers/appChatsManager.ts +++ b/src/lib/appManagers/appChatsManager.ts @@ -1,4 +1,4 @@ -import { numberWithCommas } from "../../helpers/number"; +import { numberThousandSplitter } from "../../helpers/number"; import { isObject, safeReplaceObject, copy } from "../../helpers/object"; import { ChatAdminRights, ChatBannedRights, ChatFull, ChatParticipants, InputChannel, InputChatPhoto, InputFile, InputPeer, SendMessageAction, Updates } from "../../layer"; import apiManager from '../mtproto/mtprotoworker'; @@ -398,7 +398,7 @@ export class AppChatsManager { } const isChannel = this.isBroadcast(id); - return numberWithCommas(count || 1) + ' ' + (isChannel ? 'followers' : 'members'); + return numberThousandSplitter(count || 1, ' ') + ' ' + (isChannel ? 'subscribers' : 'members'); } public wrapForFull(id: number, fullChat: any) { diff --git a/src/lib/appManagers/appDownloadManager.ts b/src/lib/appManagers/appDownloadManager.ts index 10090688..881c2c65 100644 --- a/src/lib/appManagers/appDownloadManager.ts +++ b/src/lib/appManagers/appDownloadManager.ts @@ -23,7 +23,7 @@ export type Progress = {done: number, fileName: string, total: number, offset: n export type ProgressCallback = (details: Progress) => void; export class AppDownloadManager { - private cacheStorage = new CacheStorageController('cachedFiles'); + public cacheStorage = new CacheStorageController('cachedFiles'); private downloads: {[fileName: string]: Download} = {}; private progress: {[fileName: string]: Progress} = {}; private progressCallbacks: {[fileName: string]: Array} = {}; @@ -51,14 +51,18 @@ export class AppDownloadManager { const deferred = deferredPromise(); deferred.cancel = () => { - const error = new Error('Download canceled'); - error.name = 'AbortError'; - - apiManager.cancelDownload(fileName); - this.clearDownload(fileName); + //try { + const error = new Error('Download canceled'); + error.name = 'AbortError'; + + apiManager.cancelDownload(fileName); + this.clearDownload(fileName); + + deferred.reject(error); + deferred.cancel = () => {}; + /* } catch(err) { - deferred.reject(error); - deferred.cancel = () => {}; + } */ }; deferred.finally(() => { diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index 06d1c984..03e4c013 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -21,7 +21,7 @@ import appStickersManager from './appStickersManager'; import appWebPagesManager from './appWebPagesManager'; import { cancelEvent, getFilesFromEvent, placeCaretAtEnd } from '../../helpers/dom'; import PopupNewMedia from '../../components/popups/newMedia'; -import { numberWithCommas } from '../../helpers/number'; +import { numberThousandSplitter } from '../../helpers/number'; import MarkupTooltip from '../../components/chat/markupTooltip'; import { isTouchSupported } from '../../helpers/touchSupport'; import appPollsManager from './appPollsManager'; @@ -33,6 +33,9 @@ import useHeavyAnimationCheck, { dispatchHeavyAnimationEvent } from '../../hooks import appDraftsManager from './appDraftsManager'; import serverTimeManager from '../mtproto/serverTimeManager'; import sessionStorage from '../sessionStorage'; +import { renderImageFromUrl } from '../../components/misc'; +import appDownloadManager from './appDownloadManager'; +import appStateManager, { AppStateManager } from './appStateManager'; //console.log('appImManager included33!'); @@ -154,6 +157,16 @@ export class AppImManager { animationIntersector.checkAnimations(false); }); + const isDefaultBackground = rootScope.settings.background.blur === AppStateManager.STATE_INIT.settings.background.blur && + rootScope.settings.background.slug === AppStateManager.STATE_INIT.settings.background.slug; + if(!isDefaultBackground) { + appDownloadManager.cacheStorage.getFile('background-image').then(blob => { + this.setBackground(URL.createObjectURL(blob), false); + }); + } else { + this.setBackground(''); + } + /* rootScope.on('peer_changing', (chat) => { this.saveChatPosition(chat); }); @@ -163,6 +176,15 @@ export class AppImManager { }); */ } + public setBackground(url: string, broadcastEvent = true): Promise { + const promises = this.chats.map(chat => chat.setBackground(url)); + return promises[promises.length - 1].then(() => { + if(broadcastEvent) { + rootScope.broadcast('background_change'); + } + }); + } + /* public saveChatPosition(chat: Chat) { const bubble = chat.bubbles.getBubbleByPoint('top'); if(bubble) { @@ -503,6 +525,10 @@ export class AppImManager { private createNewChat() { const chat = new Chat(this, appChatsManager, appDocsManager, appInlineBotsManager, appMessagesManager, appPeersManager, appPhotosManager, appProfileManager, appStickersManager, appUsersManager, appWebPagesManager, appPollsManager, apiManager, appDraftsManager, serverTimeManager, sessionStorage); + if(this.chats.length) { + chat.backgroundEl.append(this.chat.backgroundEl.lastElementChild.cloneNode(true)); + } + this.chats.push(chat); } @@ -661,7 +687,7 @@ export class AppImManager { if(participants_count < 2) return subtitle; const onlines = await appChatsManager.getOnlines(chat.id); if(onlines > 1) { - subtitle += ', ' + numberWithCommas(onlines) + ' online'; + subtitle += ', ' + numberThousandSplitter(onlines, ' ') + ' online'; } return subtitle; diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index ca351474..938d74de 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -264,7 +264,7 @@ export class AppMessagesManager { const folder = this.dialogsStorage.getFolder(+folderId); for(let dialog of folder) { - items.push(dialog); + items.push([dialog]); } } @@ -421,6 +421,10 @@ export class AppMessagesManager { //this.checkSendOptions(options); + if(options.threadId && !options.replyToMsgId) { + options.replyToMsgId = options.threadId; + } + const MAX_LENGTH = 4096; if(text.length > MAX_LENGTH) { const splitted = splitStringByLength(text, MAX_LENGTH); @@ -603,6 +607,11 @@ export class AppMessagesManager { waveform: Uint8Array }> = {}) { peerId = appPeersManager.getPeerMigratedTo(peerId) || peerId; + + if(options.threadId && !options.replyToMsgId) { + options.replyToMsgId = options.threadId; + } + //this.checkSendOptions(options); const messageId = this.generateTempMessageId(peerId); const randomIdS = randomLong(); @@ -999,6 +1008,10 @@ export class AppMessagesManager { }> = {}) { //this.checkSendOptions(options); + if(options.threadId && !options.replyToMsgId) { + options.replyToMsgId = options.threadId; + } + if(files.length === 1) { return this.sendFile(peerId, files[0], {...options, ...options.sendFileDetails[0]}); } @@ -1152,6 +1165,10 @@ export class AppMessagesManager { }> = {}) { peerId = appPeersManager.getPeerMigratedTo(peerId) || peerId; + if(options.threadId && !options.replyToMsgId) { + options.replyToMsgId = options.threadId; + } + //this.checkSendOptions(options); const messageId = this.generateTempMessageId(peerId); const randomIdS = randomLong(); @@ -1392,10 +1409,10 @@ export class AppMessagesManager { private generateReplyHeader(replyToMsgId: number, replyToTopId?: number) { const header = { _: 'messageReplyHeader', - reply_to_msg_id: replyToMsgId, + reply_to_msg_id: replyToMsgId || replyToTopId, } as MessageReplyHeader; - if(replyToTopId && replyToTopId !== replyToMsgId) { + if(replyToTopId && header.reply_to_msg_id !== replyToTopId) { header.reply_to_top_id = replyToTopId; } diff --git a/src/lib/appManagers/appStateManager.ts b/src/lib/appManagers/appStateManager.ts index 4fb9576f..fd779ebb 100644 --- a/src/lib/appManagers/appStateManager.ts +++ b/src/lib/appManagers/appStateManager.ts @@ -1,5 +1,5 @@ import type { Dialog } from './appMessagesManager'; -import { App, MOUNT_CLASS_TO, UserAuth } from '../mtproto/mtproto_config'; +import { App, DEBUG, MOUNT_CLASS_TO, UserAuth } from '../mtproto/mtproto_config'; import EventListenerBase from '../../helpers/eventListenerBase'; import rootScope from '../rootScope'; import sessionStorage from '../sessionStorage'; @@ -54,12 +54,18 @@ export type State = Partial<{ stickers: { suggest: boolean, loop: boolean + }, + background: { + type: 'color' | 'image' | 'default', + blur: boolean, + color?: string, + slug?: string, } }, drafts: AppDraftsManager['drafts'] }>; -const STATE_INIT: State = { +export const STATE_INIT: State = { dialogs: [], allDialogsLoaded: {}, chats: {}, @@ -95,6 +101,11 @@ const STATE_INIT: State = { stickers: { suggest: true, loop: true + }, + background: { + type: 'image', + blur: false, + slug: 'ByxGo2lrMFAIAAAAmkJxZabh8eM', // * new blurred camomile } }, drafts: {} @@ -108,6 +119,7 @@ const REFRESH_KEYS = ['dialogs', 'allDialogsLoaded', 'messages', 'contactsList', export class AppStateManager extends EventListenerBase<{ save: (state: State) => Promise }> { + public static STATE_INIT = STATE_INIT; public loaded: Promise; private log = logger('STATE'/* , LogLevels.error */); @@ -144,11 +156,25 @@ export class AppStateManager extends EventListenerBase<{ if(state.version !== STATE_VERSION) { state = copy(STATE_INIT); } else if((state.stateCreatedTime + REFRESH_EVERY) < time/* && false */) { - this.log('will refresh state', state.stateCreatedTime, time); + if(DEBUG) { + this.log('will refresh state', state.stateCreatedTime, time); + } + REFRESH_KEYS.forEach(key => { // @ts-ignore state[key] = copy(STATE_INIT[key]); }); + + const users: typeof state['users'] = {}, chats: typeof state['chats'] = {}; + if(state.recentSearch?.length) { + state.recentSearch.forEach(peerId => { + if(peerId < 0) chats[peerId] = state.chats[peerId]; + else users[peerId] = state.users[peerId]; + }); + } + + state.users = users; + state.chats = chats; } } @@ -160,7 +186,9 @@ export class AppStateManager extends EventListenerBase<{ // ! probably there is better place for it rootScope.settings = this.state.settings; - this.log('state res', state); + if(DEBUG) { + this.log('state res', state); + } //return resolve(); diff --git a/src/lib/rootScope.ts b/src/lib/rootScope.ts index 8056274b..2995ad4c 100644 --- a/src/lib/rootScope.ts +++ b/src/lib/rootScope.ts @@ -88,6 +88,8 @@ type BroadcastEvents = { 'im_tab_change': number, 'overlay_toggle': boolean, + + 'background_change': void, }; class RootScope extends EventListenerBase { diff --git a/src/scss/partials/_avatar.scss b/src/scss/partials/_avatar.scss index 64d9dbd1..31950875 100644 --- a/src/scss/partials/_avatar.scss +++ b/src/scss/partials/_avatar.scss @@ -67,13 +67,21 @@ avatar-element { } img { - width: 100%; - height: 100%; - border-radius: inherit; + //width: 100% !important; + //height: 100% !important; + width: var(--size) !important; + height: var(--size) !important; + border-radius: inherit !important; &.fade-in { animation: fade-in-opacity .2s ease forwards; } + + &.emoji { + width: calc(1.125rem / var(--multiplier)); + height: calc(1.125rem / var(--multiplier)); + vertical-align: middle !important; + } } path { diff --git a/src/scss/partials/_button.scss b/src/scss/partials/_button.scss index c653c3a8..9ef63d43 100644 --- a/src/scss/partials/_button.scss +++ b/src/scss/partials/_button.scss @@ -248,7 +248,7 @@ } } -// ! example: multiselect input, button in pinned messages chat +// ! example: multiselect input, button in pinned messages chat, settings, chat background tab .btn-transparent { color: #000; background-color: transparent; @@ -256,7 +256,7 @@ align-items: center; padding: 0 .875rem; //width: auto; - text-transform: capitalize; + //text-transform: capitalize; font-weight: normal; html.no-touch &:hover { diff --git a/src/scss/partials/_chat.scss b/src/scss/partials/_chat.scss index 169d7837..50f4b1fd 100644 --- a/src/scss/partials/_chat.scss +++ b/src/scss/partials/_chat.scss @@ -116,7 +116,7 @@ $chat-helper-size: 39px; background: none; border: none; width: 100%; - padding: 0 .5625rem; + padding: .5rem .5625rem; /* height: 100%; */ margin-top: -1px; max-height: calc(30rem - 2.5rem); // 2.5rem - input helper (reply) @@ -563,12 +563,13 @@ $chat-helper-size: 39px; &-background { overflow: hidden; + background-color: #e6ebee; &.no-transition:before { transition: none !important; } - &, &:before { + &, &-item { position: absolute !important; top: 0; left: 0; @@ -576,24 +577,32 @@ $chat-helper-size: 39px; right: 0; } - &:before { - content: ""; - display: block; + &-item { background-image: url('assets/img/bg.jpeg'); background-size: cover; background-position: center center; + background-color: inherit; + + body.animation-level-2 & { + transition: opacity var(--layer-transition); + opacity: 0; + + &.is-visible:not(.backwards) { + opacity: 1; + } + } @include respond-to(medium-screens) { body.animation-level-2 & { // !WARNING, МАГИЧЕСКОЕ ЧИСЛО - margin: -16rem -5rem -20rem 0; + margin: -16.5rem 0 -20rem 0; transform: scale(1); transform-origin: left center; - transition: transform var(--layer-transition); + transition: transform var(--layer-transition), opacity var(--layer-transition); } body.animation-level-2.is-right-column-shown & { - transform: scale(.67); + transform: scale(.666666667); } } } @@ -1036,6 +1045,8 @@ $chat-helper-size: 39px; cursor: pointer; //--translateY: 0; opacity: 1; + transition: opacity var(--layer-transition), visibility 0s 0s !important; + visibility: visible; /* &.is-broadcast { --translateY: 79px !important; @@ -1151,12 +1162,13 @@ $chat-helper-size: 39px; bottom: calc(var(--chat-input-size) + var(--bottom) + 10px); cursor: default; opacity: 0; + visibility: hidden; z-index: 2; //transition: transform var(--layer-transition), opacity var(--layer-transition) !important; overflow: visible; //--translateY: calc(var(--chat-input-size) + 10px); //--translateY: calc(100% + 10px); - transition: opacity var(--layer-transition) !important; + transition: opacity var(--layer-transition), visibility 0s .2s !important; transform: none !important; body.animation-level-0 & { diff --git a/src/scss/partials/_chatBubble.scss b/src/scss/partials/_chatBubble.scss index 16074248..912f8778 100644 --- a/src/scss/partials/_chatBubble.scss +++ b/src/scss/partials/_chatBubble.scss @@ -14,24 +14,6 @@ $bubble-margin: .25rem; } } -/* - * zoom-fade-opacity - */ - @keyframes zoom-opacity-fade-in { - 0% { - //transform: scale(.8) translateZ(0); - transform: scale3d(.8, .8, 1); - //transform: scale(.8); - opacity: 0; - } - 100% { - //transform: scale(1) translateZ(0); - transform: scale3d(1, 1, 1); - //transform: scale(1); - opacity: 1; - } -} - .bubbles-date-group { position: relative; @@ -1522,11 +1504,9 @@ $bubble-margin: .25rem; &.zoom-fade /* .bubble-content */ { //transform: scale(.8) translateZ(0); - transform: scale3d(.8, .8, 1); - //transform: scale(.8); + transform: scale3d(.8, .8, 1) translateX(0); + //transform: scale(.8) translateX(0); opacity: 0; - //animation: zoom-opacity-fade-in .2s ease-in-out forwards; - //animation-delay: 0s; } @include respond-to(not-handhelds) { diff --git a/src/scss/partials/_leftSidebar.scss b/src/scss/partials/_leftSidebar.scss index 4b67d82c..f97a8931 100644 --- a/src/scss/partials/_leftSidebar.scss +++ b/src/scss/partials/_leftSidebar.scss @@ -931,4 +931,40 @@ --thumb-size: 12px; } } -} \ No newline at end of file +} + +.background-container { + .grid { + padding: 0 .5rem; + + &-item { + &:after { + content: " "; + display: block; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + border: 3px solid $color-blue; + opacity: 0; + transition: opacity .2s ease-in-out; + } + + &.active { + &:after { + opacity: 1; + } + + .grid-item-media { + transform: scale(.91); + } + } + + &-media { + transition: transform .2s ease-in-out; + transform: scale(1); + } + } + } +} diff --git a/src/scss/style.scss b/src/scss/style.scss index 93384e98..fe1aaf4e 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -843,21 +843,29 @@ img.emoji { } } -.grid-item { - height: 0; - padding-bottom: 100%; - //overflow: hidden; - position: relative; - cursor: pointer; - user-select: none; - - &-media { - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - object-fit: cover; +.grid { + width: 100%; + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-auto-rows: 1fr; + grid-gap: .25rem; + + &-item { + height: 0; + padding-bottom: 100%; + //overflow: hidden; + position: relative; + cursor: pointer; + user-select: none; + + &-media { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + object-fit: cover; + } } }