From 409f30d8062cd1171dc83dd406ca0176c1bad04f Mon Sep 17 00:00:00 2001 From: morethanwords Date: Fri, 22 Oct 2021 22:31:54 +0400 Subject: [PATCH] FIx unneeded webPage refresh Fix locking scroll due to tab swipe on iOS --- src/components/chat/bubbles.ts | 26 ++++++++++- src/components/chat/input.ts | 21 +++++++-- src/components/inputField.ts | 17 +++++--- src/components/scrollable.ts | 7 +-- src/helpers/dom/handleHorizontalSwipe.ts | 3 +- src/helpers/fastSmoothScroll.ts | 36 +++++++++++----- src/layer.d.ts | 4 +- src/lib/appManagers/appDraftsManager.ts | 2 +- src/lib/appManagers/appImManager.ts | 12 ++++-- src/lib/appManagers/appMessagesManager.ts | 45 +++++++++++++------- src/lib/appManagers/appWebPagesManager.ts | 11 ++++- src/scripts/in/schema_additional_params.json | 4 +- src/scss/partials/_chat.scss | 3 +- 13 files changed, 141 insertions(+), 50 deletions(-) diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index e8e02222..dd085bc7 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -133,7 +133,7 @@ export default class ChatBubbles { private loadedTopTimes = 0; private loadedBottomTimes = 0; - private messagesQueuePromise: Promise = null; + public messagesQueuePromise: Promise = null; private messagesQueue: {message: any, bubble: HTMLElement, reverse: boolean, promises: Promise[]}[] = []; private messagesQueueOnRender: () => void = null; private messagesQueueOnRenderAdditional: () => void = null; @@ -1409,6 +1409,10 @@ export default class ChatBubbles { if(msgId > 0 && msgId <= maxId) { const bubble = this.bubbles[msgId]; if(bubble) { + if(bubble.classList.contains('is-sending')) { + continue; + } + bubble.classList.remove('is-sent', 'is-sending'); // is-sending can be when there are bulk of updates (e.g. sending command to Stickers bot) bubble.classList.add('is-read'); } @@ -1533,7 +1537,25 @@ export default class ChatBubbles { } } - return this.scrollable.scrollIntoViewNew(element, position, 4, undefined, forceDirection, forceDuration); + return this.scrollable.scrollIntoViewNew( + element, + position, + 4, + undefined, + forceDirection, + forceDuration, + 'y', + ({rect}) => { + let height = windowSize.windowH; + height -= this.chat.topbar.container.getBoundingClientRect().height; + height -= 78; + return height; + + const rowsWrapperHeight = this.chat.input.rowsWrapper.getBoundingClientRect().height; + const diff = rowsWrapperHeight - 54; + return rect.height + diff; + } + ); } public scrollToBubbleEnd(bubble = this.getLastBubble()) { diff --git a/src/components/chat/input.ts b/src/components/chat/input.ts index eea78391..e8326da8 100644 --- a/src/components/chat/input.ts +++ b/src/components/chat/input.ts @@ -82,7 +82,6 @@ import PopupPeer from '../popups/peer'; import MEDIA_MIME_TYPES_SUPPORTED from '../../environment/mediaMimeTypesSupport'; import appMediaPlaybackController from '../appMediaPlaybackController'; import { NULL_PEER_ID } from '../../lib/mtproto/mtproto_config'; -import replaceContent from '../../helpers/dom/replaceContent'; const RECORD_MIN_TIME = 500; const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this group.'; @@ -776,6 +775,14 @@ export default class ChatInput { draft = this.appDraftsManager.getDraft(this.chat.peerId, this.chat.threadId); if(!draft) { + if(force) { // this situation can only happen when sending message with clearDraft + ((this.chat.bubbles.messagesQueuePromise || Promise.resolve()) as Promise).then(() => { + fastRaf(() => { + this.onMessageSent(); + }); + }); + } + return false; } } @@ -921,6 +928,7 @@ export default class ChatInput { private attachMessageInputListeners() { this.listenerSetter.add(this.messageInput)('keydown', (e: KeyboardEvent) => { if(isSendShortcutPressed(e)) { + cancelEvent(e); this.sendMessage(); } else if(e.ctrlKey || e.metaKey) { this.handleMarkdownShortcut(e); @@ -1758,6 +1766,8 @@ export default class ChatInput { entities, noWebPage: this.noWebPage }); + + this.onMessageSent(); } else { new PopupDeleteMessages(this.chat.peerId, [this.editMsgId], this.chat.type); @@ -1774,6 +1784,9 @@ export default class ChatInput { silent: this.sendSilent, clearDraft: true }); + + this.onMessageSent(false, false); + // this.onMessageSent(); } // * wait for sendText set messageId for invokeAfterMsg @@ -1792,7 +1805,7 @@ export default class ChatInput { }, 0); } - this.onMessageSent(); + // this.onMessageSent(); } public sendMessageWithDocument(document: MyDocument | string, force = false, clearDraft = false) { @@ -1934,12 +1947,12 @@ export default class ChatInput { if(message._ === 'messageEmpty') { // load missing replying message peerTitleEl = i18n('Loading'); - this.chat.appMessagesManager.wrapSingleMessage(this.chat.peerId, mid).then(() => { + this.chat.appMessagesManager.wrapSingleMessage(this.chat.peerId, mid).then((_message) => { if(this.replyToMsgId !== mid) { return; } - message = this.chat.getMessage(mid); + message = _message; if(message._ === 'messageEmpty') { this.clearHelper('reply'); } else { diff --git a/src/components/inputField.ts b/src/components/inputField.ts index a0c89105..ed20d2c4 100644 --- a/src/components/inputField.ts +++ b/src/components/inputField.ts @@ -93,7 +93,7 @@ class InputField { public validate: () => boolean; //public onLengthChange: (length: number, isOverflow: boolean) => void; - protected wasInputFakeClientHeight: number; + // protected wasInputFakeClientHeight: number; // protected showScrollDebounced: () => void; constructor(public options: InputFieldOptions = {}) { @@ -147,7 +147,7 @@ class InputField { if(options.animate) { input.classList.add('scrollable', 'scrollable-y'); - this.wasInputFakeClientHeight = 0; + // this.wasInputFakeClientHeight = 0; // this.showScrollDebounced = debounce(() => this.input.classList.remove('no-scrollbar'), 150, false, true); this.inputFake = document.createElement('div'); this.inputFake.setAttribute('contenteditable', 'true'); @@ -237,14 +237,21 @@ class InputField { } public onFakeInput() { - const {scrollHeight, clientHeight} = this.inputFake; + const {scrollHeight: newHeight/* , clientHeight */} = this.inputFake; /* if(this.wasInputFakeClientHeight && this.wasInputFakeClientHeight !== clientHeight) { this.input.classList.add('no-scrollbar'); // ! в сафари может вообще не появиться скролл после анимации, так как ему нужен полный reflow блока с overflow. this.showScrollDebounced(); } */ - this.wasInputFakeClientHeight = clientHeight; - this.input.style.height = scrollHeight ? scrollHeight + 'px' : ''; + const TRANSITION_DURATION_FACTOR = 50; + const currentHeight = +this.input.style.height.replace('px', ''); + const transitionDuration = Math.round( + TRANSITION_DURATION_FACTOR * Math.log(Math.abs(newHeight - currentHeight)), + ); + + // this.wasInputFakeClientHeight = clientHeight; + this.input.style.transitionDuration = `${transitionDuration}ms`; + this.input.style.height = newHeight ? newHeight + 'px' : ''; } get value() { diff --git a/src/components/scrollable.ts b/src/components/scrollable.ts index 1c95a3b0..fe75d298 100644 --- a/src/components/scrollable.ts +++ b/src/components/scrollable.ts @@ -6,7 +6,7 @@ import { IS_TOUCH_SUPPORTED } from "../environment/touchSupport"; import { logger, LogTypes } from "../lib/logger"; -import fastSmoothScroll, { FocusDirection } from "../helpers/fastSmoothScroll"; +import fastSmoothScroll, { FocusDirection, ScrollGetNormalSizeCallback } from "../helpers/fastSmoothScroll"; import useHeavyAnimationCheck from "../hooks/useHeavyAnimationCheck"; import { cancelEvent } from "../helpers/dom/cancelEvent"; /* @@ -106,10 +106,11 @@ export class ScrollableBase { maxDistance?: number, forceDirection?: FocusDirection, forceDuration?: number, - axis?: 'x' | 'y' + axis?: 'x' | 'y', + getNormalSize?: ScrollGetNormalSizeCallback ) { //return Promise.resolve(); - return fastSmoothScroll(this.container, element, position, margin, maxDistance, forceDirection, forceDuration, axis); + return fastSmoothScroll(this.container, element, position, margin, maxDistance, forceDirection, forceDuration, axis, getNormalSize); } } diff --git a/src/helpers/dom/handleHorizontalSwipe.ts b/src/helpers/dom/handleHorizontalSwipe.ts index a90faefa..a3077850 100644 --- a/src/helpers/dom/handleHorizontalSwipe.ts +++ b/src/helpers/dom/handleHorizontalSwipe.ts @@ -5,7 +5,6 @@ */ import SwipeHandler, { SwipeHandlerOptions } from "../../components/swipeHandler"; -import { IS_APPLE_MOBILE, IS_SAFARI } from "../../environment/userAgent"; import { cancelEvent } from "./cancelEvent"; import findUpClassName from "./findUpClassName"; import isSwipingBackSafari from "./isSwipingBackSafari"; @@ -45,6 +44,6 @@ export default function handleHorizontalSwipe(options: SwipeHandlerHorizontalOpt cancelY = false; options.onReset && options.onReset(); }, - cancelEvent: true + cancelEvent: false // cannot use cancelEvent on Safari iOS because scroll will be canceled too }); } diff --git a/src/helpers/fastSmoothScroll.ts b/src/helpers/fastSmoothScroll.ts index 70d4ce17..5b1e7e36 100644 --- a/src/helpers/fastSmoothScroll.ts +++ b/src/helpers/fastSmoothScroll.ts @@ -12,9 +12,10 @@ import { animateSingle, cancelAnimationByKey } from './animation'; import rootScope from '../lib/rootScope'; import isInDOM from './dom/isInDOM'; -const MAX_DISTANCE = 1500; const MIN_JS_DURATION = 250; const MAX_JS_DURATION = 600; +const LONG_TRANSITION_MAX_DISTANCE = 1500; +const SHORT_TRANSITION_MAX_DISTANCE = 500; export enum FocusDirection { Up, @@ -22,15 +23,18 @@ export enum FocusDirection { Static, }; +export type ScrollGetNormalSizeCallback = (options: {rect: DOMRect}) => number; + export default function fastSmoothScroll( container: HTMLElement, element: HTMLElement, position: ScrollLogicalPosition, margin = 0, - maxDistance = MAX_DISTANCE, + maxDistance = LONG_TRANSITION_MAX_DISTANCE, forceDirection?: FocusDirection, forceDuration?: number, - axis: 'x' | 'y' = 'y' + axis: 'x' | 'y' = 'y', + getNormalSize?: ScrollGetNormalSizeCallback ) { //return; @@ -40,7 +44,7 @@ export default function fastSmoothScroll( if(forceDirection === FocusDirection.Static) { forceDuration = 0; - return scrollWithJs(container, element, position, margin, forceDuration, axis); + return scrollWithJs(container, element, position, margin, forceDuration, axis, getNormalSize); /* return Promise.resolve(); element.scrollIntoView({ block: position }); @@ -82,9 +86,9 @@ export default function fastSmoothScroll( } */ } - const promise = new Promise((resolve) => { + const promise = new Promise((resolve) => { fastRaf(() => { - scrollWithJs(container, element, position, margin, forceDuration, axis) + scrollWithJs(container, element, position, margin, forceDuration, axis, getNormalSize) .then(resolve); }); }); @@ -93,7 +97,13 @@ export default function fastSmoothScroll( } function scrollWithJs( - container: HTMLElement, element: HTMLElement, position: ScrollLogicalPosition, margin = 0, forceDuration?: number, axis: 'x' | 'y' = 'y' + container: HTMLElement, + element: HTMLElement, + position: ScrollLogicalPosition, + margin = 0, + forceDuration?: number, + axis: 'x' | 'y' = 'y', + getNormalSize?: ScrollGetNormalSizeCallback ) { if(!isInDOM(element)) { cancelAnimationByKey(container); @@ -115,7 +125,7 @@ function scrollWithJs( const elementPosition = elementRect[rectStartKey] - containerRect[rectStartKey]; const elementSize = element[scrollSizeKey]; // margin is exclusive in DOMRect - const containerSize = containerRect[sizeKey]; + const containerSize = getNormalSize ? getNormalSize({rect: containerRect}) : containerRect[sizeKey]; const scrollPosition = container[scrollPositionKey]; const scrollSize = container[scrollSizeKey]; @@ -177,8 +187,9 @@ function scrollWithJs( } const target = container[scrollPositionKey] + path; + const absPath = Math.abs(path); const duration = forceDuration ?? ( - MIN_JS_DURATION + (Math.abs(path) / MAX_DISTANCE) * (MAX_JS_DURATION - MIN_JS_DURATION) + MIN_JS_DURATION + (absPath / LONG_TRANSITION_MAX_DISTANCE) * (MAX_JS_DURATION - MIN_JS_DURATION) ); const startAt = Date.now(); @@ -222,6 +233,7 @@ function scrollWithJs( //transformable.style.minHeight = `${transformableHeight}px`; */ + const transition = absPath < SHORT_TRANSITION_MAX_DISTANCE ? shortTransition : longTransition; const tick = () => { const t = duration ? Math.min((Date.now() - startAt) / duration, 1) : 1; @@ -258,6 +270,10 @@ function scrollWithJs( return animateSingle(tick, container); } -function transition(t: number) { +function longTransition(t: number) { + return 1 - ((1 - t) ** 5); +} + +function shortTransition(t: number) { return 1 - ((1 - t) ** 3.5); } diff --git a/src/layer.d.ts b/src/layer.d.ts index 6a06f182..12823790 100644 --- a/src/layer.d.ts +++ b/src/layer.d.ts @@ -816,7 +816,9 @@ export namespace Message { flags?: number, id: number, peer_id?: Peer, - deleted?: boolean + deleted?: boolean, + mid?: number, + pFlags?: {} }; export type message = { diff --git a/src/lib/appManagers/appDraftsManager.ts b/src/lib/appManagers/appDraftsManager.ts index bb03a703..8940bf4d 100644 --- a/src/lib/appManagers/appDraftsManager.ts +++ b/src/lib/appManagers/appDraftsManager.ts @@ -251,7 +251,7 @@ export class AppDraftsManager { if(threadId) { this.syncDraft(peerId, threadId); } else { - this.saveDraft(peerId, threadId, null, {notify: true/* , force: true */}); + this.saveDraft(peerId, threadId, null, {notify: true, force: true}); } } diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index 54a4a9d8..119a3797 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -501,16 +501,22 @@ export class AppImManager { return; } - if(chat.input.messageInput && + if( + chat?.input?.messageInput && e.target !== chat.input.messageInput && target.tagName !== 'INPUT' && !target.hasAttribute('contenteditable') && !IS_TOUCH_SUPPORTED && (!mediaSizes.isMobile || this.tabId === 1) && - !this.chat.selection.isSelecting && - !this.chat.input.recording) { + !chat.selection.isSelecting && + !chat.input.recording + ) { chat.input.messageInput.focus(); placeCaretAtEnd(chat.input.messageInput); + + // clone and dispatch same event to new input. it is needed for sending message if input was blurred + const newEvent = new KeyboardEvent(e.type, e); + chat.input.messageInput.dispatchEvent(newEvent); } }; diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index 409a6eff..d1685bd4 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -1073,7 +1073,7 @@ export class AppMessagesManager { const messages = files.map((file, idx) => { const details = options.sendFileDetails[idx]; - const o: any = { + const o: Parameters[2] = { isGroupedItem: true, isMedia: options.isMedia, scheduleDate: options.scheduleDate, @@ -1094,7 +1094,9 @@ export class AppMessagesManager { }); if(options.clearDraft) { - appDraftsManager.clearDraft(peerId, options.threadId); + setTimeout(() => { + appDraftsManager.clearDraft(peerId, options.threadId); + }, 0); } // * test pending @@ -1407,10 +1409,6 @@ export class AppMessagesManager { }, 0); } - if(!options.isGroupedItem && options.clearDraft) { - appDraftsManager.clearDraft(peerId, options.threadId); - } - this.pendingByRandomId[message.random_id] = { peerId, tempId: messageId, @@ -1419,9 +1417,13 @@ export class AppMessagesManager { }; if(!options.isGroupedItem && message.send) { - setTimeout(message.send, 0); - //setTimeout(message.send, 4000); - //setTimeout(message.send, 7000); + setTimeout(() => { + if(options.clearDraft) { + appDraftsManager.clearDraft(peerId, options.threadId); + } + + message.send(); + }, 0); } } @@ -1966,15 +1968,20 @@ export class AppMessagesManager { return promise; } - public getMessageFromStorage(storage: MessagesStorage, mid: number) { - return storage && storage.get(mid) || { + public generateEmptyMessage(mid: number): Message.messageEmpty { + return { _: 'messageEmpty', - id: mid, + id: appMessagesIdsManager.getServerMessageId(mid), + mid, deleted: true, pFlags: {} }; } + public getMessageFromStorage(storage: MessagesStorage, mid: number) { + return storage && storage.get(mid) || this.generateEmptyMessage(mid); + } + private createMessageStorage() { const storage: MessagesStorage = new Map(); @@ -5455,7 +5462,6 @@ export class AppMessagesManager { for(const [peerId, map] of this.needSingleMessages) { const mids = [...map.keys()]; - const promises = [...map.values()]; const msgIds: InputMessage[] = mids.map((mid) => { return { _: 'inputMessageID', @@ -5483,8 +5489,17 @@ export class AppMessagesManager { this.saveMessages(getMessagesResult.messages); for(let i = 0; i < getMessagesResult.messages.length; ++i) { - const promise = promises[i]; + const message = getMessagesResult.messages[i]; + const mid = appMessagesIdsManager.generateMessageId(message.id); + const promise = map.get(mid); promise.resolve(getMessagesResult.messages[i]); + map.delete(mid); + } + + if(map.size) { + for(const [mid, promise] of map) { + promise.resolve(this.generateEmptyMessage(mid)); + } } }).finally(() => { rootScope.dispatchEvent('messages_downloaded', {peerId, mids}); @@ -5497,7 +5512,7 @@ export class AppMessagesManager { Promise.all(requestPromises).finally(() => { this.fetchSingleMessagesPromise = null; - if(Object.keys(this.needSingleMessages).length) this.fetchSingleMessages(); + if(this.needSingleMessages.size) this.fetchSingleMessages(); resolve(); }); }, 0); diff --git a/src/lib/appManagers/appWebPagesManager.ts b/src/lib/appManagers/appWebPagesManager.ts index dc18c10d..bc783741 100644 --- a/src/lib/appManagers/appWebPagesManager.ts +++ b/src/lib/appManagers/appWebPagesManager.ts @@ -43,6 +43,13 @@ export class AppWebPagesManager { if(apiWebPage._ === 'webPageNotModified') return; const {id} = apiWebPage; + const oldWebPage = this.webpages[id]; + if(oldWebPage && + oldWebPage._ === apiWebPage._ && + (oldWebPage as WebPage.webPage).hash === (oldWebPage as WebPage.webPage).hash) { + return oldWebPage; + } + if(apiWebPage._ === 'webPage') { if(apiWebPage.photo?._ === 'photo') { apiWebPage.photo = appPhotosManager.savePhoto(apiWebPage.photo, mediaContext); @@ -97,10 +104,10 @@ export class AppWebPagesManager { pendingSet.add(messageKey); } - if(this.webpages[id] === undefined) { + if(oldWebPage === undefined) { this.webpages[id] = apiWebPage; } else { - safeReplaceObject(this.webpages[id], apiWebPage); + safeReplaceObject(oldWebPage, apiWebPage); } if(!messageKey && pendingSet !== undefined) { diff --git a/src/scripts/in/schema_additional_params.json b/src/scripts/in/schema_additional_params.json index 9a6d59e5..87dc5b53 100644 --- a/src/scripts/in/schema_additional_params.json +++ b/src/scripts/in/schema_additional_params.json @@ -99,7 +99,9 @@ }, { "predicate": "messageEmpty", "params": [ - {"name": "deleted", "type": "boolean"} + {"name": "deleted", "type": "boolean"}, + {"name": "mid", "type": "number"}, + {"name": "pFlags", "type": "{}"} ] }, { "predicate": "userFull", diff --git a/src/scss/partials/_chat.scss b/src/scss/partials/_chat.scss index 2a833af5..a6188885 100644 --- a/src/scss/partials/_chat.scss +++ b/src/scss/partials/_chat.scss @@ -917,7 +917,8 @@ $chat-helper-size: 36px; pointer-events: none; @include animation-level(2) { - transition: height var(--layer-transition), opacity var(--layer-transition); + // transition: height var(--layer-transition), opacity var(--layer-transition); + transition: height .15s ease-out, opacity .15s ease-out; } @include respond-to(esg-bottom-new) {