diff --git a/src/components/appMediaViewer.ts b/src/components/appMediaViewer.ts index f5450746..f8282670 100644 --- a/src/components/appMediaViewer.ts +++ b/src/components/appMediaViewer.ts @@ -959,7 +959,7 @@ class AppMediaViewerBase; private getHistoryBottomPromise: Promise; @@ -140,10 +140,10 @@ export default class ChatBubbles { this.chatInner = document.createElement('div'); this.chatInner.classList.add('bubbles-inner'); - this.bubblesContainer.append(this.chatInner); - this.setScroll(); + this.bubblesContainer.append(this.scrollable.container); + // * constructor end this.log = this.chat.log; @@ -1067,10 +1067,12 @@ export default class ChatBubbles { }; public setScroll() { - this.scrollable = new Scrollable(this.bubblesContainer/* .firstElementChild */ as HTMLElement, 'IM', /* 10300 */300); + this.scrollable = new Scrollable(null, 'IM', /* 10300 */300); this.scrollable.loadedAll.top = false; this.scrollable.loadedAll.bottom = false; + this.scrollable.container.append(this.chatInner); + /* const getScrollOffset = () => { //return Math.round(Math.max(300, appPhotosManager.windowH / 1.5)); return 300; @@ -1081,17 +1083,14 @@ export default class ChatBubbles { }); this.scrollable = new Scrollable(this.bubblesContainer, 'y', 'IM', this.chatInner, getScrollOffset()); */ - this.scroll = this.scrollable.container; this.scrollable.onAdditionalScroll = this.onScroll; this.scrollable.onScrolledTop = () => this.loadMoreHistory(true); this.scrollable.onScrolledBottom = () => this.loadMoreHistory(false); //this.scrollable.attachSentinels(undefined, 300); - this.bubblesContainer.classList.add('scrolled-down'); - if(isTouchSupported) { - this.scroll.addEventListener('touchmove', () => { + this.scrollable.container.addEventListener('touchmove', () => { if(this.isScrollingTimeout) { clearTimeout(this.isScrollingTimeout); } else if(!this.chatInner.classList.contains('is-scrolling')) { @@ -1099,7 +1098,7 @@ export default class ChatBubbles { } }, {passive: true}); - this.scroll.addEventListener('touchend', () => { + this.scrollable.container.addEventListener('touchend', () => { if(!this.chatInner.classList.contains('is-scrolling')) { return; } @@ -1447,7 +1446,7 @@ export default class ChatBubbles { this.chat.dispatchEvent('setPeer', lastMsgId, false); } else if(topMessage && !isJump) { //this.log('will scroll down', this.scroll.scrollTop, this.scroll.scrollHeight); - this.scroll.scrollTop = this.scroll.scrollHeight; + this.scrollable.scrollTop = this.scrollable.scrollHeight; this.chat.dispatchEvent('setPeer', lastMsgId, true); } @@ -1493,9 +1492,8 @@ export default class ChatBubbles { const oldChatInner = this.chatInner; this.cleanup(); - this.chatInner = document.createElement('div'); - this.chatInner.className = oldChatInner.className; - this.chatInner.classList.add('disable-hover', 'is-scrolling'); + this.chatInner = oldChatInner.cloneNode() as HTMLDivElement; + this.chatInner.classList.remove('disable-hover', 'is-scrolling'); this.lazyLoadQueue.lock(); @@ -1516,7 +1514,7 @@ export default class ChatBubbles { // clear if(!cached) { if(!samePeer) { - this.scrollable.container.innerHTML = ''; + this.scrollable.container.textContent = ''; //oldChatInner.remove(); this.chat.finishPeerChange(isTarget, isJump, lastMsgId); this.preloader.attach(this.bubblesContainer); @@ -1529,9 +1527,6 @@ export default class ChatBubbles { const setPeerPromise = promise.then(() => { ////this.log('setPeer removing preloader'); - this.scrollable.container.innerHTML = ''; - //oldChatInner.remove(); - if(cached) { if(!samePeer) { this.chat.finishPeerChange(isTarget, isJump, lastMsgId); // * костыль @@ -1540,11 +1535,14 @@ export default class ChatBubbles { this.preloader.detach(); } - this.scrollable.container.append(this.chatInner); + replaceContent(this.scrollable.container, this.chatInner); + animationIntersector.unlockGroup(CHAT_ANIMATION_GROUP); animationIntersector.checkAnimations(false, CHAT_ANIMATION_GROUP/* , true */); - this.lazyLoadQueue.unlock(); + fastRaf(() => { + this.lazyLoadQueue.unlock(); + }); //if(dialog && lastMsgID && lastMsgID !== topMessage && (this.bubbles[lastMsgID] || this.firstUnreadBubble)) { if(savedPosition) { @@ -1564,7 +1562,7 @@ export default class ChatBubbles { const fromUp = maxBubbleId > 0 && (maxBubbleId < lastMsgId || lastMsgId < 0); const followingUnread = readMaxId === lastMsgId && !isTarget; if(!fromUp && samePeer) { - this.scrollable.scrollTop = this.scrollable.scrollHeight; + this.scrollable.scrollTop = 99999; } else if(fromUp/* && (samePeer || forwardingUnread) */) { this.scrollable.scrollTop = 0; } @@ -1583,7 +1581,7 @@ export default class ChatBubbles { } } } else { - this.scrollable.scrollTop = this.scrollable.scrollHeight; + this.scrollable.scrollTop = 99999; } this.chat.dispatchEvent('setPeer', lastMsgId, !isJump); @@ -1613,8 +1611,11 @@ export default class ChatBubbles { } } - this.chatInner.classList.remove('disable-hover', 'is-scrolling'); // warning, performance! + //this.chatInner.classList.remove('disable-hover', 'is-scrolling'); // warning, performance! + /* if(!document.body.classList.contains(RIGHT_COLUMN_ACTIVE_CLASSNAME)) { + return new Promise((resolve) => fastRaf(resolve)); + } */ //console.timeEnd('appImManager setPeer'); }).catch(err => { this.log.error('getHistory promise error:', err); @@ -2729,9 +2730,9 @@ export default class ChatBubbles { const peerId = this.peerId; //console.time('appImManager call getHistory'); - const pageCount = this.appPhotosManager.windowH / 38/* * 1.25 */ | 0; + const pageCount = Math.min(30, 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 = Object.keys(this.bubbles).length > 0/* || additionMsgId */ ? Math.max(40, pageCount) : pageCount;//const realLoadCount = 50; //const realLoadCount = pageCount;//const realLoadCount = 50; let loadCount = realLoadCount; diff --git a/src/components/chat/replies.ts b/src/components/chat/replies.ts index 27085f66..9b20f42a 100644 --- a/src/components/chat/replies.ts +++ b/src/components/chat/replies.ts @@ -148,4 +148,4 @@ export default class RepliesElement extends HTMLElement { } } -customElements.define(TAG_NAME, RepliesElement); \ No newline at end of file +customElements.define(TAG_NAME, RepliesElement); diff --git a/src/components/lazyLoadQueue.ts b/src/components/lazyLoadQueue.ts index b33946aa..8f471c14 100644 --- a/src/components/lazyLoadQueue.ts +++ b/src/components/lazyLoadQueue.ts @@ -4,7 +4,7 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ -import { debounce } from "../helpers/schedulers"; +import { throttle } from "../helpers/schedulers"; import { logger, LogLevels } from "../lib/logger"; import VisibilityIntersector, { OnVisibilityChange } from "./visibilityIntersector"; import { findAndSpliceAll } from "../helpers/array"; @@ -33,7 +33,7 @@ export class LazyLoadQueueBase { protected processQueue: () => void; constructor(protected parallelLimit = PARALLEL_LIMIT) { - this.processQueue = debounce(() => this._processQueue(), 20, false, true); + this.processQueue = throttle(() => this._processQueue(), 20, false); } public clear() { @@ -117,6 +117,8 @@ export class LazyLoadQueueBase { protected _processQueue(item?: LazyLoadElementBase) { if(!this.queue.length || this.lockPromise || (this.parallelLimit > 0 && this.inProcess.size >= this.parallelLimit)) return; + //console.log('_processQueue start'); + let added = 0; do { if(item) { this.queue.findAndSplice(i => i === item); @@ -131,7 +133,9 @@ export class LazyLoadQueueBase { } item = null; + ++added; } while(this.inProcess.size < this.parallelLimit && this.queue.length); + //console.log('_processQueue end, added', added, this.queue.length); } public push(el: LazyLoadElementBase) { diff --git a/src/components/poll.ts b/src/components/poll.ts index d70bb5d7..5eadb387 100644 --- a/src/components/poll.ts +++ b/src/components/poll.ts @@ -21,7 +21,7 @@ import SetTransition from "./singleTransition"; import findUpClassName from "../helpers/dom/findUpClassName"; let lineTotalLength = 0; -const tailLength = 9; +//const tailLength = 9; const times = 10; const fullTime = 340; const oneTime = fullTime / times; @@ -163,8 +163,9 @@ export default class PollElement extends HTMLElement { private votersCountDiv: HTMLDivElement; private maxOffset = -46.5; - private maxLength: number; - private maxLengths: number[]; + //private maxLength: number; + //private maxLengths: number[]; + private maxPercents: number[]; public isClosed = false; private isQuiz = false; @@ -393,21 +394,17 @@ export default class PollElement extends HTMLElement { footerDiv.append(this.sendVoteBtn); } - const width = this.getBoundingClientRect().width; - this.maxLength = width + tailLength + this.maxOffset + -13.7; // 13 - position left + //const width = this.getBoundingClientRect().width; + //this.maxLength = width + tailLength + this.maxOffset + -13.7; // 13 - position left if(poll.chosenIndexes.length || this.isClosed) { - this.performResults(results, poll.chosenIndexes); + this.performResults(results, poll.chosenIndexes, false); } else if(!this.isClosed) { this.setVotersCount(results); attachClickEvent(this, this.clickHandler); } } - connectedCallback() { - this.render(); - } - initQuizHint(results: PollResults) { if(results.solution && results.solution_entities) { const toggleHint = document.createElement('div'); @@ -486,7 +483,11 @@ export default class PollElement extends HTMLElement { }); } - performResults(results: PollResults, chosenIndexes: number[]) { + performResults(results: PollResults, chosenIndexes: number[], animate = true) { + if(!rootScope.settings.animationsEnabled) { + animate = false; + } + if(this.isQuiz && (results.results?.length || this.isClosed)) { this.answerDivs.forEach((el, idx) => { el.classList.toggle('is-correct', !!results.results[idx].pFlags.correct); @@ -533,9 +534,13 @@ export default class PollElement extends HTMLElement { if(this.chosenIndexes.length || this.isRetracted || this.isClosed) { const percents = results.results.map(v => results.total_voters ? v.voters / results.total_voters * 100 : 0); - SetTransition(this, '', !this.isRetracted, 340); + this.classList.toggle('no-transition', !animate); + if(animate) { + SetTransition(this, '', !this.isRetracted, 340); + } + fastRaf(() => { - this.setResults(this.isRetracted ? this.percents : percents, this.chosenIndexes); + this.setResults(this.isRetracted ? this.percents : percents, this.chosenIndexes, animate); this.percents = percents; this.isRetracted = false; }); @@ -576,7 +581,7 @@ export default class PollElement extends HTMLElement { } } - setResults(percents: number[], chosenIndexes: number[]) { + setResults(percents: number[], chosenIndexes: number[], animate: boolean) { this.svgLines.forEach(svg => svg.style.display = ''); this.answerDivs.forEach((el, idx) => { @@ -584,7 +589,8 @@ export default class PollElement extends HTMLElement { }); const maxValue = Math.max(...percents); - this.maxLengths = percents.map(p => p / maxValue * this.maxLength); + //this.maxLengths = percents.map(p => p / maxValue * this.maxLength); + this.maxPercents = percents.map(p => p / maxValue); // line if(this.isRetracted) { @@ -592,42 +598,70 @@ export default class PollElement extends HTMLElement { this.setLineProgress(idx, -1); }); } else { - this.svgLines.forEach((svg, idx) => { - void svg.getBoundingClientRect(); // reflow - this.setLineProgress(idx, 1); - }); + const cb = () => { + this.svgLines.forEach((svg, idx) => { + //void svg.getBoundingClientRect(); // reflow + this.setLineProgress(idx, 1); + }); + }; + + animate ? fastRaf(cb) : cb(); } percents = percents.slice(); roundPercents(percents); + let getPercentValue: (percents: number, index: number) => number; + const iterate = (i: number) => { + percents.forEach((percents, idx) => { + const value = getPercentValue(percents, i); + this.numberDivs[idx].innerText = value + '%'; + }); + }; // numbers if(this.isRetracted) { - for(let i = (times - 1), k = 0; i >= 0; --i, ++k) { - setTimeout(() => { - percents.forEach((percents, idx) => { - const value = Math.round(percents / times * i); - this.numberDivs[idx].innerText = value + '%'; - }); - }, oneTime * k); + getPercentValue = (percents, index) => Math.round(percents / times * index); + + if(animate) { + for(let i = (times - 1), k = 0; i >= 0; --i, ++k) { + setTimeout(() => { + iterate(i); + }, oneTime * k); + } + } else { + iterate(0); } } else { - for(let i = 0; i < times; ++i) { - setTimeout(() => { - percents.forEach((percents, idx) => { - const value = Math.round(percents / times * (i + 1)); - this.numberDivs[idx].innerText = value + '%'; - }); - }, oneTime * i); + getPercentValue = (percents, index) => Math.round(percents / times * (index + 1)); + + if(animate) { + for(let i = 0; i < times; ++i) { + setTimeout(() => { + iterate(i); + }, oneTime * i); + } + } else { + iterate(times - 1); } } if(this.isRetracted) { - this.classList.add('is-retracting'); + if(animate) { + this.classList.add('is-retracting'); + } + this.classList.remove('is-voted'); - setTimeout(() => { - this.classList.remove('is-retracting'); + const cb = () => { this.svgLines.forEach(svg => svg.style.display = 'none'); - }, fullTime); + }; + + if(animate) { + setTimeout(() => { + this.classList.remove('is-retracting'); + cb(); + }, fullTime); + } else { + cb(); + } } else { this.classList.add('is-voted'); } @@ -647,15 +681,16 @@ export default class PollElement extends HTMLElement { replaceContent(this.votersCountDiv, i18n(key, args)); } - setLineProgress(index: number, percents: number) { + setLineProgress(index: number, multiplier: number) { const svg = this.svgLines[index]; - if(percents === -1) { + if(multiplier === -1) { svg.style.strokeDasharray = ''; svg.style.strokeDashoffset = ''; } else { - svg.style.strokeDasharray = (percents * this.maxLengths[index]) + ', 485.9'; - svg.style.strokeDashoffset = '' + percents * this.maxOffset; + //svg.style.strokeDasharray = (percents * this.maxLengths[index]) + ', 485.9'; + svg.style.strokeDasharray = (multiplier * this.maxPercents[index] * 100) + '%, 485.9'; + svg.style.strokeDashoffset = '' + multiplier * this.maxOffset; } } diff --git a/src/components/ripple.ts b/src/components/ripple.ts index 3b7f6589..ed2450a7 100644 --- a/src/components/ripple.ts +++ b/src/components/ripple.ts @@ -5,6 +5,7 @@ */ import findUpClassName from "../helpers/dom/findUpClassName"; +import sequentialDom from "../helpers/sequentialDom"; import {isTouchSupported} from "../helpers/touchSupport"; import rootScope from "../lib/rootScope"; @@ -44,22 +45,22 @@ export function ripple(elem: HTMLElement, callback: (id: number) => Promise { + //console.log('ripple elapsedTime total pre-remove:', Date.now() - startTime); + sequentialDom.mutate(() => { + elem.remove(); + }); + + if(onEnd) onEnd(clickId); + }; if(elapsedTime < duration) { let delay = Math.max(duration - elapsedTime, duration / 2); setTimeout(() => elem.classList.add('hiding'), Math.max(delay - duration / 2, 0)); - setTimeout(() => { - //console.log('ripple elapsedTime total pre-remove:', Date.now() - startTime); - elem.remove(); - if(onEnd) onEnd(clickId); - }, delay); + setTimeout(cb, delay); } else { elem.classList.add('hiding'); - setTimeout(() => { - //console.log('ripple elapsedTime total pre-remove:', Date.now() - startTime); - elem.remove(); - if(onEnd) onEnd(clickId); - }, duration / 2); + setTimeout(cb, duration / 2); } if(!isTouchSupported) { diff --git a/src/components/scrollable.ts b/src/components/scrollable.ts index 3a482ca7..921079ba 100644 --- a/src/components/scrollable.ts +++ b/src/components/scrollable.ts @@ -108,6 +108,7 @@ export class ScrollableBase { forceDuration?: number, axis?: 'x' | 'y' ) { + //return Promise.resolve(); return fastSmoothScroll(this.container, element, position, margin, maxDistance, forceDirection, forceDuration, axis); } } diff --git a/src/components/visibilityIntersector.ts b/src/components/visibilityIntersector.ts index 0ff90ef6..99543d9e 100644 --- a/src/components/visibilityIntersector.ts +++ b/src/components/visibilityIntersector.ts @@ -120,4 +120,4 @@ export default class VisibilityIntersector { public lock() { this.locked = true; } -} \ No newline at end of file +} diff --git a/src/components/wrappers.ts b/src/components/wrappers.ts index 9ac0ef6c..fbff1a67 100644 --- a/src/components/wrappers.ts +++ b/src/components/wrappers.ts @@ -579,7 +579,7 @@ function wrapMediaWithTail(photo: MyPhoto | MyDocument, message: {mid: number, m const foreignObject = document.createElementNS("http://www.w3.org/2000/svg", 'foreignObject'); - const gotThumb = appPhotosManager.getStrippedThumbIfNeeded(photo); + const gotThumb = appPhotosManager.getStrippedThumbIfNeeded(photo, true); if(gotThumb) { foreignObject.append(gotThumb.image); } @@ -632,7 +632,7 @@ function wrapMediaWithTail(photo: MyPhoto | MyDocument, message: {mid: number, m return img; } -export function wrapPhoto({photo, message, container, boxWidth, boxHeight, withTail, isOut, lazyLoadQueue, middleware, size, withoutPreloader, loadPromises, noAutoDownload}: { +export function wrapPhoto({photo, message, container, boxWidth, boxHeight, withTail, isOut, lazyLoadQueue, middleware, size, withoutPreloader, loadPromises, noAutoDownload, noBlur}: { photo: MyPhoto | MyDocument, message: any, container: HTMLElement, @@ -646,6 +646,7 @@ export function wrapPhoto({photo, message, container, boxWidth, boxHeight, withT withoutPreloader?: boolean, loadPromises?: Promise[], noAutoDownload?: boolean, + noBlur?: boolean, }) { if(!((photo as MyPhoto).sizes || (photo as MyDocument).thumbs)) { if(boxWidth && boxHeight && photo._ === 'document') { @@ -680,7 +681,7 @@ export function wrapPhoto({photo, message, container, boxWidth, boxHeight, withT size = appPhotosManager.setAttachmentSize(photo, container, boxWidth, boxHeight, undefined, message && message.message); } - const gotThumb = appPhotosManager.getStrippedThumbIfNeeded(photo); + const gotThumb = appPhotosManager.getStrippedThumbIfNeeded(photo, !noBlur); if(gotThumb) { loadThumbPromise = gotThumb.loadPromise; thumbImage = gotThumb.image; @@ -1307,6 +1308,6 @@ export function wrapPoll(message: any) { elem.setAttribute('peer-id', '' + message.peerId); elem.setAttribute('poll-id', message.media.poll.id); elem.setAttribute('message-id', '' + message.mid); - //elem.render(); + elem.render(); return elem; } diff --git a/src/config/app.ts b/src/config/app.ts index aaf6c731..f7b6356e 100644 --- a/src/config/app.ts +++ b/src/config/app.ts @@ -12,7 +12,7 @@ const App = { id: 1025907, hash: '452b0359b988148995f22ff0f4229750', - version: '0.4.0', + version: '0.4.1', langPackVersion: '0.1.3', langPack: 'macos', langPackCode: 'en', diff --git a/src/helpers/blur.ts b/src/helpers/blur.ts index 88fb5b73..d2a5fd44 100644 --- a/src/helpers/blur.ts +++ b/src/helpers/blur.ts @@ -46,8 +46,11 @@ function processBlur(dataUri: string, radius: number, iterations: number) { }); } +const blurPromises: {[dataUri: string]: Promise} = {}; + export default function blur(dataUri: string, radius: number = RADIUS, iterations: number = ITERATIONS) { - return new Promise((resolve) => { + if(blurPromises[dataUri]) return blurPromises[dataUri]; + return blurPromises[dataUri] = new Promise((resolve) => { //return resolve(dataUri); pushHeavyTask({ items: [[dataUri, radius, iterations]], diff --git a/src/helpers/dom.ts b/src/helpers/dom.ts index 71811afa..e0eec949 100644 --- a/src/helpers/dom.ts +++ b/src/helpers/dom.ts @@ -529,7 +529,7 @@ export const getSelectedNodes = () => { return nodes.filter(node => !!node); }; -export const isSelectionSingle = (input: Element = document.activeElement) => { +/* export const isSelectionSingle = (input: Element = document.activeElement) => { const nodes = getSelectedNodes(); const parents = [...new Set(nodes.map(node => node.parentNode))]; const differentParents = parents.length > 1; @@ -545,7 +545,7 @@ export const isSelectionSingle = (input: Element = document.activeElement) => { } return single; -}; +}; */ export const handleScrollSideEvent = (elem: HTMLElement, side: 'top' | 'bottom', callback: () => void, listenerSetter: ListenerSetter) => { if(isTouchSupported) { @@ -587,81 +587,6 @@ export const handleScrollSideEvent = (elem: HTMLElement, side: 'top' | 'bottom', } }; -export const getElementByPoint = (container: HTMLElement, verticalSide: 'top' | 'bottom', horizontalSide: 'center' | 'left'): HTMLElement => { - const rect = container.getBoundingClientRect(); - const x = horizontalSide === 'center' ? Math.ceil(rect.left + ((rect.right - rect.left) / 2) + 1) : Math.ceil(rect.left + 1); - const y = verticalSide === 'bottom' ? Math.floor(rect.top + rect.height - 1) : Math.ceil(rect.top + 1); - return document.elementFromPoint(x, y) as any; -}; - -MOUNT_CLASS_TO.getElementByPoint = getElementByPoint; - -export async function getFilesFromEvent(e: ClipboardEvent | DragEvent, onlyTypes = false): Promise { - const files: any[] = []; - - const scanFiles = async(entry: any, item: DataTransferItem) => { - if(entry.isDirectory) { - const directoryReader = entry.createReader(); - await new Promise((resolve, reject) => { - directoryReader.readEntries(async(entries: any) => { - for(const entry of entries) { - await scanFiles(entry, item); - } - - resolve(); - }); - }); - } else if(entry) { - if(onlyTypes) { - files.push(entry.type); - } else { - const itemFile = item.getAsFile(); // * Safari can't handle entry.file with pasting - const file = entry instanceof File ? - entry : - ( - entry instanceof DataTransferItem ? - entry.getAsFile() : - await new Promise((resolve, reject) => entry.file(resolve, (err: any) => resolve(itemFile))) - ); - - /* if(!onlyTypes) { - console.log('getFilesFromEvent: got file', item, file); - } */ - - if(!file) return; - files.push(file); - } - } - }; - - if(e instanceof DragEvent && e.dataTransfer.files && !e.dataTransfer.items) { - for(let i = 0; i < e.dataTransfer.files.length; i++) { - const file = e.dataTransfer.files[i]; - files.push(onlyTypes ? file.type : file); - } - } else { - // @ts-ignore - const items = (e.dataTransfer || e.clipboardData || e.originalEvent.clipboardData).items; - - const promises: Promise[] = []; - for(let i = 0; i < items.length; ++i) { - const item: DataTransferItem = items[i]; - if(item.kind === 'file') { - const entry = (onlyTypes ? item : item.webkitGetAsEntry()) || item.getAsFile(); - promises.push(scanFiles(entry, item)); - } - } - - await Promise.all(promises); - } - - /* if(!onlyTypes) { - console.log('getFilesFromEvent: got files:', e, files); - } */ - - return files; -} - /* export function radiosHandleChange(inputs: HTMLInputElement[], onChange: (value: string) => void) { inputs.forEach(input => { input.addEventListener('change', () => { @@ -756,11 +681,13 @@ export function htmlToSpan(html: string) { } export function replaceContent(elem: HTMLElement, node: string | Node) { - if(elem.children.length === 1) { - elem.firstChild.remove(); + // * children.length doesn't count text nodes + if(elem.children.length) { + elem.firstChild.replaceWith(node); + } else if(!elem.firstChild) { + elem.append(node); } else { elem.textContent = ''; + elem.append(node); } - - elem.append(node); } diff --git a/src/helpers/dom/getElementByPoint.ts b/src/helpers/dom/getElementByPoint.ts new file mode 100644 index 00000000..372d7638 --- /dev/null +++ b/src/helpers/dom/getElementByPoint.ts @@ -0,0 +1,17 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + */ + +import { MOUNT_CLASS_TO } from "../../config/debug"; + +export function getElementByPoint(container: HTMLElement, verticalSide: 'top' | 'bottom', horizontalSide: 'center' | 'left'): HTMLElement { + //return null; + const rect = container.getBoundingClientRect(); + const x = horizontalSide === 'center' ? Math.ceil(rect.left + ((rect.right - rect.left) / 2) + 1) : Math.ceil(rect.left + 1); + const y = verticalSide === 'bottom' ? Math.floor(rect.top + rect.height - 1) : Math.ceil(rect.top + 1); + return document.elementFromPoint(x, y) as any; +}; + +MOUNT_CLASS_TO.getElementByPoint = getElementByPoint; diff --git a/src/helpers/files.ts b/src/helpers/files.ts index a111cc6d..5ab4da40 100644 --- a/src/helpers/files.ts +++ b/src/helpers/files.ts @@ -53,4 +53,70 @@ export function onVideoLoad(video: HTMLVideoElement) { video.addEventListener(isAppleMobile ? 'loadeddata' : 'canplay', () => resolve(), {once: true}); }); -} \ No newline at end of file +} + +export async function getFilesFromEvent(e: ClipboardEvent | DragEvent, onlyTypes = false): Promise { + const files: any[] = []; + + const scanFiles = async(entry: any, item: DataTransferItem) => { + if(entry.isDirectory) { + const directoryReader = entry.createReader(); + await new Promise((resolve, reject) => { + directoryReader.readEntries(async(entries: any) => { + for(const entry of entries) { + await scanFiles(entry, item); + } + + resolve(); + }); + }); + } else if(entry) { + if(onlyTypes) { + files.push(entry.type); + } else { + const itemFile = item.getAsFile(); // * Safari can't handle entry.file with pasting + const file = entry instanceof File ? + entry : + ( + entry instanceof DataTransferItem ? + entry.getAsFile() : + await new Promise((resolve, reject) => entry.file(resolve, (err: any) => resolve(itemFile))) + ); + + /* if(!onlyTypes) { + console.log('getFilesFromEvent: got file', item, file); + } */ + + if(!file) return; + files.push(file); + } + } + }; + + if(e instanceof DragEvent && e.dataTransfer.files && !e.dataTransfer.items) { + for(let i = 0; i < e.dataTransfer.files.length; i++) { + const file = e.dataTransfer.files[i]; + files.push(onlyTypes ? file.type : file); + } + } else { + // @ts-ignore + const items = (e.dataTransfer || e.clipboardData || e.originalEvent.clipboardData).items; + + const promises: Promise[] = []; + for(let i = 0; i < items.length; ++i) { + const item: DataTransferItem = items[i]; + if(item.kind === 'file') { + const entry = (onlyTypes ? item : item.webkitGetAsEntry()) || item.getAsFile(); + promises.push(scanFiles(entry, item)); + } + } + + await Promise.all(promises); + } + + /* if(!onlyTypes) { + console.log('getFilesFromEvent: got files:', e, files); + } */ + + return files; +} diff --git a/src/helpers/schedulers.ts b/src/helpers/schedulers.ts index aac57e2c..20accbec 100644 --- a/src/helpers/schedulers.ts +++ b/src/helpers/schedulers.ts @@ -37,7 +37,7 @@ export function debounce( }; } -/* export function throttle( +export function throttle( fn: F, ms: number, shouldRunFirst = true, @@ -70,7 +70,7 @@ export function debounce( }, ms); } }; -} */ +} /* export function throttleWithRaf(fn: F) { return throttleWith(fastRaf, fn); @@ -131,7 +131,7 @@ export function fastRaf(callback: NoneToVoidFunction) { } export function doubleRaf() { - return new Promise((resolve) => { + return new Promise((resolve) => { fastRaf(() => { fastRaf(resolve); }); diff --git a/src/index.ts b/src/index.ts index e091e83e..d36b61e8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -58,9 +58,16 @@ console.timeEnd('get storage1'); */ // @ts-ignore const w = window.visualViewport || window; // * handle iOS keyboard let setViewportVH = false; + let lastVH: number; const setVH = () => { // @ts-ignore const vh = (setViewportVH && !rootScope.default.overlayIsActive ? w.height || w.innerHeight : window.innerHeight) * 0.01; + if(lastVH === vh) { + return; + } + + lastVH = vh; + //const vh = document.documentElement.scrollHeight * 0.01; document.documentElement.style.setProperty('--vh', `${vh}px`); diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index c0697c54..54907d19 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -24,7 +24,7 @@ import appPhotosManager from './appPhotosManager'; import appProfileManager from './appProfileManager'; import appStickersManager from './appStickersManager'; import appWebPagesManager from './appWebPagesManager'; -import { blurActiveElement, cancelEvent, disableTransition, getFilesFromEvent, placeCaretAtEnd, whichChild } from '../../helpers/dom'; +import { blurActiveElement, cancelEvent, disableTransition, placeCaretAtEnd, whichChild } from '../../helpers/dom'; import PopupNewMedia from '../../components/popups/newMedia'; import MarkupTooltip from '../../components/chat/markupTooltip'; import { isTouchSupported } from '../../helpers/touchSupport'; @@ -47,6 +47,7 @@ import { i18n } from '../langPack'; import { SendMessageAction } from '../../layer'; import { highlightningColor } from '../../helpers/color'; import { getObjectKeysAndSort } from '../../helpers/object'; +import { getFilesFromEvent } from '../../helpers/files'; //console.log('appImManager included33!'); diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index 9773704f..db989b25 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -67,6 +67,7 @@ export type HistoryStorage = { readPromise?: Promise, readMaxId?: number, readOutboxMaxId?: number, + triedToReadMaxId?: number, maxOutId?: number, reply_markup?: any @@ -3433,7 +3434,7 @@ export class AppMessagesManager { if((message.totalEntities as MessageEntity[]).find(e => goodEntities.includes(e._)) || RichTextProcessor.matchUrl(message.message)) { found = true; } - } else if(neededContents['avatar'] && message.action && ['messageActionChannelEditPhoto', 'messageActionChatEditPhoto'].includes(message.action._)) { + } else if(neededContents['avatar'] && message.action && ['messageActionChannelEditPhoto', 'messageActionChatEditPhoto', 'messageActionChannelEditVideo', 'messageActionChatEditVideo'].includes(message.action._)) { found = true; }/* else if(neededFlags.find(flag => message.pFlags[flag])) { found = true; @@ -3471,9 +3472,12 @@ export class AppMessagesManager { }); } + const canCache = (['inputMessagesFilterChatPhotos', 'inputMessagesFilterPinned'] as MyInputMessagesFilter[]).includes(inputFilter._); + const method = (canCache ? apiManager.invokeApiCacheable : apiManager.invokeApi).bind(apiManager); + let apiPromise: Promise; if(peerId && !nextRate && folderId === undefined/* || !query */) { - apiPromise = apiManager.invokeApi('messages.search', { + apiPromise = method('messages.search', { peer: appPeersManager.getInputPeerById(peerId), q: query || '', filter: inputFilter as any as MessagesFilter, @@ -3502,7 +3506,7 @@ export class AppMessagesManager { offsetPeerId = this.getMessagePeer(offsetMessage); } - apiPromise = apiManager.invokeApi('messages.searchGlobal', { + apiPromise = method('messages.searchGlobal', { q: query, filter: inputFilter as any as MessagesFilter, min_date: minDate, @@ -3721,7 +3725,7 @@ export class AppMessagesManager { } public readHistory(peerId: number, maxId = 0, threadId?: number, force = false) { - // return Promise.resolve(); + //return Promise.resolve(); // console.trace('start read') this.log('readHistory:', peerId, maxId, threadId); if(!this.getReadMaxIdIfUnread(peerId, threadId) && !force) { @@ -3729,9 +3733,12 @@ export class AppMessagesManager { return Promise.resolve(); } - const isChannel = appPeersManager.isChannel(peerId); const historyStorage = this.getHistoryStorage(peerId, threadId); + if(historyStorage.triedToReadMaxId >= maxId) { + return Promise.resolve(); + } + let apiPromise: Promise; if(threadId) { if(!historyStorage.readPromise) { @@ -3751,7 +3758,7 @@ export class AppMessagesManager { read_max_id: maxId } as Update.updateReadChannelDiscussionInbox }); - } else if(isChannel) { + } else if(appPeersManager.isChannel(peerId)) { if(!historyStorage.readPromise) { apiPromise = apiManager.invokeApi('channels.readHistory', { channel: appChatsManager.getChannelInput(-peerId), @@ -3811,6 +3818,8 @@ export class AppMessagesManager { return historyStorage.readPromise; } + historyStorage.triedToReadMaxId = maxId; + apiPromise.finally(() => { delete historyStorage.readPromise; diff --git a/src/lib/appManagers/appPhotosManager.ts b/src/lib/appManagers/appPhotosManager.ts index fd5a70ad..76e1ddb6 100644 --- a/src/lib/appManagers/appPhotosManager.ts +++ b/src/lib/appManagers/appPhotosManager.ts @@ -133,7 +133,7 @@ export class AppPhotosManager { public getUserPhotos(userId: number, maxId: string = '0', limit: number = 20) { const inputUser = appUsersManager.getUserInput(userId); - return apiManager.invokeApi('photos.getUserPhotos', { + return apiManager.invokeApiCacheable('photos.getUserPhotos', { user_id: inputUser, offset: 0, limit, @@ -204,13 +204,13 @@ export class AppPhotosManager { return thumb.url ?? (defineNotNumerableProperties(thumb, ['url']), thumb.url = this.getPreviewURLFromBytes(thumb.bytes, isSticker)); } - public getImageFromStrippedThumb(thumb: PhotoSize.photoCachedSize | PhotoSize.photoStrippedSize) { + public getImageFromStrippedThumb(thumb: PhotoSize.photoCachedSize | PhotoSize.photoStrippedSize, useBlur: boolean) { const url = this.getPreviewURLFromThumb(thumb, false); const image = new Image(); image.classList.add('thumbnail'); - const loadPromise = blur(url).then(url => { + const loadPromise = (useBlur ? blur(url) : Promise.resolve(url)).then(url => { return new Promise((resolve) => { renderImageFromUrl(image, url, resolve); }); @@ -252,7 +252,7 @@ export class AppPhotosManager { return photoSize; } - public getStrippedThumbIfNeeded(photo: MyPhoto | MyDocument): ReturnType { + public getStrippedThumbIfNeeded(photo: MyPhoto | MyDocument, useBlur: boolean): ReturnType { if(!photo.downloaded || (photo as MyDocument).type === 'video' || (photo as MyDocument).type === 'gif') { if(photo._ === 'document') { const cacheContext = this.getCacheContext(photo); @@ -264,7 +264,7 @@ export class AppPhotosManager { const sizes = (photo as MyPhoto).sizes || (photo as MyDocument).thumbs; const thumb = sizes?.length ? sizes.find(size => size._ === 'photoStrippedSize') : null; if(thumb && ('bytes' in thumb)) { - return appPhotosManager.getImageFromStrippedThumb(thumb as any); + return appPhotosManager.getImageFromStrippedThumb(thumb as any, useBlur); } } diff --git a/src/lib/appManagers/appProfileManager.ts b/src/lib/appManagers/appProfileManager.ts index 59c3e03b..321f7e95 100644 --- a/src/lib/appManagers/appProfileManager.ts +++ b/src/lib/appManagers/appProfileManager.ts @@ -11,7 +11,9 @@ import { MOUNT_CLASS_TO } from "../../config/debug"; import { tsNow } from "../../helpers/date"; +import { replaceContent } from "../../helpers/dom"; import renderImageFromUrl from "../../helpers/dom/renderImageFromUrl"; +import sequentialDom from "../../helpers/sequentialDom"; import { ChannelParticipantsFilter, ChannelsChannelParticipants, ChatFull, ChatParticipants, ChatPhoto, ExportedChatInvite, InputChannel, InputFile, InputFileLocation, PhotoSize, UserFull, UserProfilePhoto } from "../../layer"; //import apiManager from '../mtproto/apiManager'; import apiManager from '../mtproto/mtprotoworker'; @@ -504,8 +506,7 @@ export class AppProfileManager { if(!needFadeIn) { // смотри в misc.ts: renderImageFromUrl callback = () => { - div.innerHTML = ''; - div.append(img); + replaceContent(div, img); div.dataset.color = ''; }; } else { @@ -515,15 +516,16 @@ export class AppProfileManager { } callback = () => { - div.innerHTML = ''; - div.append(img); + replaceContent(div, img); setTimeout(() => { if(div.childElementCount) { div.dataset.color = ''; if(animate) { - img.classList.remove('fade-in'); + sequentialDom.mutateElement(img, () => { + img.classList.remove('fade-in'); + }); } } }, animate ? 200 : 0); @@ -554,7 +556,7 @@ export class AppProfileManager { //console.log('loadDialogPhoto location:', location, inputPeer); if(peerId === myId && isDialog) { - div.innerHTML = ''; + div.innerText = ''; div.dataset.color = ''; div.classList.add('tgico-saved'); div.classList.remove('tgico-deletedaccount'); @@ -564,7 +566,7 @@ export class AppProfileManager { if(peerId > 0) { const user = appUsersManager.getUser(peerId); if(user && user.pFlags && user.pFlags.deleted) { - div.innerHTML = ''; + div.innerText = ''; div.dataset.color = appPeersManager.getPeerColorById(peerId); div.classList.add('tgico-deletedaccount'); div.classList.remove('tgico-saved'); @@ -578,7 +580,7 @@ export class AppProfileManager { color = appPeersManager.getPeerColorById(peerId); } - div.innerHTML = ''; + div.innerText = ''; div.classList.remove('tgico-saved', 'tgico-deletedaccount'); div.dataset.color = color; diff --git a/src/lib/appManagers/appStateManager.ts b/src/lib/appManagers/appStateManager.ts index 7883f01a..d5f1e9b4 100644 --- a/src/lib/appManagers/appStateManager.ts +++ b/src/lib/appManagers/appStateManager.ts @@ -170,9 +170,9 @@ export class AppStateManager extends EventListenerBase<{ const time = Date.now(); if(state) { - if(state.version !== STATE_VERSION) { + /* if(state.version !== STATE_VERSION) { state = copy(STATE_INIT); - } else if((state.stateCreatedTime + REFRESH_EVERY) < time/* || true *//* && false */) { + } else */if((state.stateCreatedTime + REFRESH_EVERY) < time/* || true *//* && false */) { if(DEBUG) { this.log('will refresh state', state.stateCreatedTime, time); } diff --git a/src/scss/partials/_chatBubble.scss b/src/scss/partials/_chatBubble.scss index f70e67c9..4962df1d 100644 --- a/src/scss/partials/_chatBubble.scss +++ b/src/scss/partials/_chatBubble.scss @@ -2017,11 +2017,13 @@ $bubble-margin: .25rem; &-answer-selected { background-color: var(--message-out-primary-color); + color: var(--message-out-background-color); } html.no-touch &-answer:hover { .animation-ring { - background-color: rgba(79, 174, 78, .08); + background-color: var(--message-out-primary-color); + opacity: .08; } } diff --git a/src/scss/partials/_poll.scss b/src/scss/partials/_poll.scss index 942d2de6..484f7d4e 100644 --- a/src/scss/partials/_poll.scss +++ b/src/scss/partials/_poll.scss @@ -55,10 +55,9 @@ poll-element { color: var(--primary-color); cursor: pointer; transform: scale(1); - transition: transform .2s ease; - - body.animation-level-0 & { - transition: none; + + @include animation-level(2) { + transition: transform .2s ease; } // @include respond-to(handhelds) { @@ -100,7 +99,6 @@ poll-element { font-weight: 500; margin-top: 7px; font-size: 14px; - transition: .34s opacity; margin-left: -9px; text-align: right; width: 40px; @@ -117,10 +115,7 @@ poll-element { width: 16px; font-weight: bold; font-size: .75rem; - opacity: 0; - animation: fade-in-opacity .1s ease forwards; - animation-direction: reverse; - animation-delay: .24s; + opacity: 1; display: flex; align-items: center; justify-content: center; @@ -148,14 +143,16 @@ poll-element { &:not(.is-correct):not(.is-chosen) { .poll-answer-selected { - display: none; + opacity: 0; } } // Multiple answers &.is-chosing { - .poll-answer-selected { - opacity: 1; + .circle-hover { + .poll-answer-selected { + opacity: 1; + } } & ~ .poll-footer { @@ -238,11 +235,11 @@ poll-element { &-answer { &.is-chosen:not(.is-correct) { use { - stroke: #DF3F40; + stroke: var(--danger-color); } .poll-answer-selected { - background: #DF3F40; + background: var(--danger-color); //line-height: 16px; &:before { @@ -278,7 +275,6 @@ poll-element { left: -1px; top: -1px; transform: scale(1); - transition: .1s transform; .poll-answer-selected { display: flex!important; @@ -300,10 +296,13 @@ poll-element { border-radius: 50%; height: 34px; width: 34px; - transition: transform .12s; - background-color: #f4f4f4; + background-color: var(--light-secondary-text-color); transform: scale(.1); visibility: hidden; + + @include animation-level(2) { + transition: transform .12s ease; + } } .progress-ring { @@ -315,12 +314,11 @@ poll-element { &__circle { transform-origin: center; transform: rotate(-90deg); - transition: stroke-dashoffset .15s; stroke-dasharray: 56.5487, 56.5487; stroke-dashoffset: 0; stroke-opacity: 1; stroke-width: 2; - stroke: var(--border-color); + stroke: var(--poll-circle-color); fill: transparent; } } @@ -333,10 +331,6 @@ poll-element { .poll-answer-percents { opacity: 1; } - - .poll-answer-selected { - animation-direction: normal; - } } &.is-retracting { @@ -353,6 +347,29 @@ poll-element { .poll-line { transition: stroke-dashoffset .34s linear, stroke-dasharray .34s linear; } + + .poll-answer-selected { + transition-delay: .24s; + transition: opacity .1s ease forwards; + } + + &.is-retracting { + .poll-answer-selected { + transition-delay: 0s; + } + } + + .poll-answer-percents { + transition: .34s opacity; + } + + .progress-ring__circle { + transition: stroke-dashoffset .15s; + } + + .circle-hover { + transition: .1s transform; + } } } diff --git a/src/scss/style.scss b/src/scss/style.scss index 1de377ef..9e232919 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -42,8 +42,8 @@ $chat-padding-handhelds: .5rem; --hover-alpha: #{$hover-alpha}; --transition-standard-easing: cubic-bezier(.4, .0, .2, 1); - --transition-standard-in-time: .25s; - --transition-standard-out-time: .2s; + --transition-standard-in-time: .3s; + --transition-standard-out-time: .25s; --transition-standard-in: var(--transition-standard-in-time) var(--transition-standard-easing); --transition-standard-out: var(--transition-standard-out-time) var(--transition-standard-easing); @@ -149,6 +149,7 @@ $chat-padding-handhelds: .5rem; --badge-text-color: #fff; --link-color: #00488f; --ripple-color: rgba(0, 0, 0, .08); + --poll-circle-color: var(--border-color); --message-background-color: var(--surface-color); --message-checkbox-color: #61c642; @@ -192,6 +193,7 @@ html.night { --badge-text-color: #fff; --link-color: var(--primary-color); --ripple-color: rgba(255, 255, 255, .08); + --poll-circle-color: #fff; --message-background-color: var(--surface-color); --message-checkbox-color: var(--primary-color);