From 22ceba971d994d96c540cd2cb453f4a4a7143ea9 Mon Sep 17 00:00:00 2001 From: morethanwords Date: Sat, 23 May 2020 08:31:18 +0300 Subject: [PATCH] sticker thumbs & safari open chat fix & online blink fix & login added idle monkey & webp safari save to indexeddb & lazyload queue reorder & minor fixes --- src/components/appForward.ts | 4 +- src/components/appSelectPeers.ts | 16 +- src/components/chatInput.ts | 4 +- src/components/emoticonsDropdown.ts | 25 +- src/components/lazyLoadQueue.ts | 14 +- src/components/misc.ts | 25 +- src/components/scrollable_new.ts | 109 ++++++--- src/components/wrappers.ts | 237 ++++++++++--------- src/lib/appManagers/appChatsManager.ts | 65 ++++- src/lib/appManagers/appDialogsManager.ts | 6 +- src/lib/appManagers/appDocsManager.ts | 106 +++++---- src/lib/appManagers/appImManager.ts | 145 +++++------- src/lib/appManagers/appMediaViewer.ts | 21 +- src/lib/appManagers/appMessagesManager.ts | 8 +- src/lib/appManagers/appPhotosManager.ts | 50 ++-- src/lib/appManagers/appSidebarRight.ts | 4 +- src/lib/appManagers/appStickersManager.ts | 55 ++++- src/lib/appManagers/appWebpManager.ts | 94 +++----- src/lib/bin_utils.ts | 18 -- src/lib/filemanager.ts | 12 +- src/lib/idb.ts | 2 + src/lib/lottieLoader.ts | 8 +- src/lib/mtproto/apiFileManager.ts | 122 ++++++---- src/lib/mtproto/networker.ts | 9 +- src/lib/mtproto/networkerFactory.ts | 18 -- src/lib/mtproto/tl_utils.ts | 14 +- src/lib/utils.js | 6 +- src/lib/webp.ts | 44 ++-- src/pages/pageAuthCode.ts | 75 +++++- src/pages/pageSignIn.ts | 2 + src/scss/partials/_chat.scss | 1 + src/scss/partials/_chatBubble.scss | 16 +- src/scss/partials/_emojiDropdown.scss | 9 +- src/scss/partials/_rightSIdebar.scss | 17 +- src/scss/partials/_scrollable.scss | 8 +- src/scss/partials/popups/_mediaAttacher.scss | 1 - src/scss/style.scss | 34 +-- 37 files changed, 748 insertions(+), 656 deletions(-) diff --git a/src/components/appForward.ts b/src/components/appForward.ts index 615df98c..d8643d93 100644 --- a/src/components/appForward.ts +++ b/src/components/appForward.ts @@ -56,7 +56,6 @@ class AppForward { this.cleanup(); this.msgIDs = ids; - appSidebarRight.toggleSidebar(true); this.container.classList.add('active'); this.sendBtn.innerHTML = ''; this.sendBtn.classList.add('tgico-send'); @@ -68,6 +67,9 @@ class AppForward { } else { this.sendBtn.classList.remove('is-visible'); } + }, 'dialogs', () => { + console.log('forward rendered:', this.container.querySelector('.selector ul').childElementCount); + appSidebarRight.toggleSidebar(true); }); } } diff --git a/src/components/appSelectPeers.ts b/src/components/appSelectPeers.ts index 36e2d8fa..6f7c9fb0 100644 --- a/src/components/appSelectPeers.ts +++ b/src/components/appSelectPeers.ts @@ -29,7 +29,7 @@ export class AppSelectPeers { private query = ''; private cachedContacts: number[]; - constructor(private appendTo: HTMLDivElement, private onChange?: (length: number) => void, private peerType: 'contacts' | 'dialogs' = 'dialogs') { + constructor(private appendTo: HTMLDivElement, private onChange?: (length: number) => void, private peerType: 'contacts' | 'dialogs' = 'dialogs', onFirstRender?: () => void) { this.container.classList.add('selector'); let topContainer = document.createElement('div'); @@ -107,12 +107,18 @@ export class AppSelectPeers { this.container.append(topContainer, delimiter, this.chatsContainer); appendTo.append(this.container); - this.getMoreResults(); + let getResultsPromise = this.getMoreResults() as Promise; + if(onFirstRender) { + getResultsPromise.then(() => { + onFirstRender(); + }); + } } private getMoreDialogs() { // в десктопе - сначала без группы, потом архивные, потом контакты без сообщений - appMessagesManager.getConversations(this.offsetIndex, 50, 0).then(value => { + let pageCount = appPhotosManager.windowH / 72 * 1.25 | 0; + return appMessagesManager.getConversations(this.offsetIndex, pageCount, 0).then(value => { let dialogs = value.dialogs; let newOffsetIndex = dialogs[value.dialogs.length - 1].index || 0; @@ -150,9 +156,9 @@ export class AppSelectPeers { private getMoreResults() { if(this.peerType == 'dialogs') { - this.getMoreDialogs(); + return this.getMoreDialogs(); } else { - this.getMoreContacts(); + return this.getMoreContacts(); } } diff --git a/src/components/chatInput.ts b/src/components/chatInput.ts index b2835bc5..1600d6ba 100644 --- a/src/components/chatInput.ts +++ b/src/components/chatInput.ts @@ -13,7 +13,7 @@ import lottieLoader from "../lib/lottieLoader"; import { Layouter, RectPart } from "./groupedLayout"; export class ChatInput { - public pageEl = document.querySelector('.page-chats') as HTMLDivElement; + public pageEl = document.getElementById('page-chats') as HTMLDivElement; public messageInput = document.getElementById('input-message') as HTMLDivElement/* HTMLInputElement */; public fileInput = document.getElementById('input-file') as HTMLInputElement; public inputMessageContainer = document.getElementsByClassName('input-message-container')[0] as HTMLDivElement; @@ -463,6 +463,7 @@ export class ChatInput { this.emoticonsDropdown.classList.remove('active'); this.toggleEmoticons.classList.remove('active'); lottieLoader.checkAnimations(true, EMOTICONSSTICKERGROUP); + this.emoticonsLazyLoadQueue.lock(); }, 200); }; @@ -471,6 +472,7 @@ export class ChatInput { }; } else { this.emoticonsDropdown.classList.add('active'); + this.emoticonsLazyLoadQueue.unlock(); } this.toggleEmoticons.classList.add('active'); diff --git a/src/components/emoticonsDropdown.ts b/src/components/emoticonsDropdown.ts index cf1ae996..e5fbc45a 100644 --- a/src/components/emoticonsDropdown.ts +++ b/src/components/emoticonsDropdown.ts @@ -1,6 +1,6 @@ import { AppImManager } from "../lib/appManagers/appImManager"; import { AppMessagesManager } from "../lib/appManagers/appMessagesManager"; -import { horizontalMenu } from "./misc"; +import { horizontalMenu, renderImageFromUrl } from "./misc"; import lottieLoader from "../lib/lottieLoader"; //import Scrollable from "./scrollable"; import Scrollable from "./scrollable_new"; @@ -12,7 +12,6 @@ import apiManager from '../lib/mtproto/mtprotoworker'; //import CryptoWorker from '../lib/crypto/cryptoworker'; import LazyLoadQueue from "./lazyLoadQueue"; import { MTDocument, wrapSticker } from "./wrappers"; -import appWebpManager from "../lib/appManagers/appWebpManager"; import appDocsManager from "../lib/appManagers/appDocsManager"; import ProgressivePreloader from "./preloader"; import Config from "../lib/config"; @@ -29,15 +28,18 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement, let container = pageEl.querySelector('.emoji-container .tabs-container') as HTMLDivElement; let tabs = pageEl.querySelector('.emoji-dropdown .emoji-tabs') as HTMLUListElement; + let tabID = -1; horizontalMenu(tabs, container, (id) => { lottieLoader.checkAnimations(true, EMOTICONSSTICKERGROUP); - if(id == 1 && stickersInit) { + tabID = id; + }, () => { + if(tabID == 1 && stickersInit) { stickersInit(); - } else if(id == 2 && gifsInit) { + } else if(tabID == 2 && gifsInit) { gifsInit(); } - }, () => { + lottieLoader.checkAnimations(false, EMOTICONSSTICKERGROUP); }); @@ -347,15 +349,7 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement, stickersInit = null; Promise.all([ - apiManager.invokeApi('messages.getRecentStickers', {flags: 0, hash: 0}).then((res) => { - let stickers: { - _: string, - hash: number, - packs: any[], - stickers: MTDocument[], - dates: number[] - } = res as any; - + appStickersManager.getRecentStickers().then(stickers => { let categoryDiv = document.createElement('div'); categoryDiv.classList.add('sticker-category'); @@ -408,8 +402,7 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement, reader.readAsArrayBuffer(blob); } else { let image = new Image(); - //image.src = URL.createObjectURL(blob); - appWebpManager.polyfillImage(image, blob); + renderImageFromUrl(image, URL.createObjectURL(blob)); li.append(image); } diff --git a/src/components/lazyLoadQueue.ts b/src/components/lazyLoadQueue.ts index 323e5500..bc6fd069 100644 --- a/src/components/lazyLoadQueue.ts +++ b/src/components/lazyLoadQueue.ts @@ -19,16 +19,18 @@ export default class LazyLoadQueue { constructor(private parallelLimit = 5) { this.observer = new IntersectionObserver(entries => { + if(this.lockPromise) return; + for(let entry of entries) { if(entry.isIntersecting) { let target = entry.target as HTMLElement; - for(let item of this.lazyLoadMedia) { - if(item.div == target) { - item.wasSeen = true; - this.processQueue(item); - break; - } + // need for set element first if scrolled + let item = this.lazyLoadMedia.findAndSplice(i => i.div == target); + if(item) { + item.wasSeen = true; + this.lazyLoadMedia.unshift(item); + this.processQueue(item); } } } diff --git a/src/components/misc.ts b/src/components/misc.ts index 23e24c7c..87f7f25e 100644 --- a/src/components/misc.ts +++ b/src/components/misc.ts @@ -121,26 +121,29 @@ let set = (elem: HTMLElement | HTMLImageElement | SVGImageElement | HTMLSourceEl else elem.style.backgroundImage = 'url(' + url + ')'; }; -export function renderImageFromUrl(elem: HTMLElement | HTMLImageElement | SVGImageElement | HTMLSourceElement, url: string) { +export function renderImageFromUrl(elem: HTMLElement | HTMLImageElement | SVGImageElement | HTMLSourceElement, url: string): Promise { if(loadedURLs[url]) { set(elem, url); - return true; + return Promise.resolve(true); } if(elem instanceof HTMLSourceElement) { elem.src = url; + return Promise.resolve(false); } else { - let loader = new Image(); - loader.src = url; - //let perf = performance.now(); - loader.addEventListener('load', () => { - set(elem, url); - loadedURLs[url] = true; - //console.log('onload:', url, performance.now() - perf); + return new Promise((resolve, reject) => { + let loader = new Image(); + loader.src = url; + //let perf = performance.now(); + loader.addEventListener('load', () => { + set(elem, url); + loadedURLs[url] = true; + //console.log('onload:', url, performance.now() - perf); + resolve(false); + }); + loader.addEventListener('error', reject); }); } - - return false; } export function putPreloader(elem: Element, returnDiv = false) { diff --git a/src/components/scrollable_new.ts b/src/components/scrollable_new.ts index 636bc349..8ff61374 100644 --- a/src/components/scrollable_new.ts +++ b/src/components/scrollable_new.ts @@ -56,6 +56,9 @@ export default class Scrollable { private lastBottomID = 0; private lastScrollDirection = 0; // true = bottom + private onScrolledTopFired = false; + private onScrolledBottomFired = false; + public scrollLocked = 0; private setVisible(element: HTMLElement) { @@ -184,37 +187,56 @@ export default class Scrollable { //this.onScroll(); } - public attachSentinels(container = this.container, offset = this.onScrollOffset) { - if(!this.sentinelsObserver) { - this.topSentinel = document.createElement('div'); - this.topSentinel.classList.add('scrollable-sentinel'); - this.topSentinel.style.top = offset + 'px'; - this.bottomSentinel = document.createElement('div'); - this.bottomSentinel.classList.add('scrollable-sentinel'); - this.bottomSentinel.style.bottom = offset + 'px'; - - this.container.append(this.topSentinel, this.bottomSentinel); - - this.sentinelsObserver = new IntersectionObserver(entries => { - for(let entry of entries) { - if(entry.isIntersecting) { - let top = entry.target == this.topSentinel; - if(top) { - this.onScrolledTop && this.onScrolledTop(); - } else { - this.onScrolledBottom && this.onScrolledBottom(); - } - } - } - }); - - this.sentinelsObserver.observe(this.topSentinel); - this.sentinelsObserver.observe(this.bottomSentinel); - } - - container.prepend(this.topSentinel); - container.append(this.bottomSentinel); - } + // public attachSentinels(container = this.container, offset = this.onScrollOffset) { + // if(!this.sentinelsObserver) { + // this.topSentinel = document.createElement('div'); + // this.topSentinel.classList.add('scrollable-sentinel'); + // this.topSentinel.style.top = offset + 'px'; + // this.bottomSentinel = document.createElement('div'); + // this.bottomSentinel.classList.add('scrollable-sentinel'); + // this.bottomSentinel.style.bottom = offset + 'px'; + + // this.container.append(this.topSentinel, this.bottomSentinel); + + // //let fire: () => void; + + // this.sentinelsObserver = new IntersectionObserver(entries => { + // for(let entry of entries) { + // let top = entry.target == this.topSentinel; + // if(top) { + // this.onScrolledTopFired = entry.isIntersecting; + // } else { + // this.onScrolledBottomFired = entry.isIntersecting; + // } + // } + + // /* this.debug && */this.log('Set onScrolledFires:', this.onScrolledTopFired, this.onScrolledBottomFired); + + // /* if((this.onScrolledTopFired || this.onScrolledBottomFired) && !fire) { + // fire = () => window.requestAnimationFrame(() => { + // if(!this.scrollLocked) { + // if(this.onScrolledTopFired && this.onScrolledTop) this.onScrolledTop(); + // if(this.onScrolledBottomFired && this.onScrolledBottom) this.onScrolledBottom(); + // } + + // if(!this.onScrolledTopFired && !this.onScrolledBottomFired) { + // fire = undefined; + // } else { + // fire(); + // } + // }); + + // fire(); + // } */ + // }); + + // this.sentinelsObserver.observe(this.topSentinel); + // this.sentinelsObserver.observe(this.bottomSentinel); + // } + + // container.prepend(this.topSentinel); + // container.append(this.bottomSentinel); + // } public setVirtualContainer(el?: HTMLElement) { this.splitUp = el; @@ -231,7 +253,7 @@ export default class Scrollable { /* if(this.debug) { this.log('onScroll call', this.onScrollMeasure); } */ - + let appendTo = this.splitUp || this.appendTo; clearTimeout(this.disableHoverTimeout); @@ -246,8 +268,13 @@ export default class Scrollable { this.lastScrollDirection = 0; }, 100); - if(!this.splitUp || this.onScrollMeasure) return; + if(this.onScrollMeasure) return; this.onScrollMeasure = window.requestAnimationFrame(() => { + this.checkForTriggers(); + + this.onScrollMeasure = 0; + if(!this.splitUp) return; + let scrollTop = this.container.scrollTop; if(this.lastScrollTop != scrollTop) { this.lastScrollDirection = this.lastScrollTop < scrollTop ? 1 : -1; @@ -255,10 +282,24 @@ export default class Scrollable { } else { this.lastScrollDirection = 0; } - this.onScrollMeasure = 0; }); } + public checkForTriggers() { + if(this.scrollLocked || (!this.onScrolledTop && !this.onScrolledBottom)) return; + + let scrollTop = this.container.scrollTop; + let maxScrollTop = this.container.scrollHeight - this.container.clientHeight; + + if(this.onScrolledTop && scrollTop <= this.onScrollOffset) { + this.onScrolledTop(); + } + + if(this.onScrolledBottom && (maxScrollTop - scrollTop) <= this.onScrollOffset) { + this.onScrolledBottom(); + } + } + public reorder() { (Array.from(this.splitUp.children) as HTMLElement[]).forEach((el, idx) => { el.dataset.virtual = '' + idx; diff --git a/src/components/wrappers.ts b/src/components/wrappers.ts index f5ef68ed..b8aa9ff8 100644 --- a/src/components/wrappers.ts +++ b/src/components/wrappers.ts @@ -1,4 +1,4 @@ -import appPhotosManager, { MTPhoto } from '../lib/appManagers/appPhotosManager'; +import appPhotosManager from '../lib/appManagers/appPhotosManager'; //import CryptoWorker from '../lib/crypto/cryptoworker'; import apiManager from '../lib/mtproto/mtprotoworker'; import LottieLoader from '../lib/lottieLoader'; @@ -8,7 +8,6 @@ import { formatBytes } from "../lib/utils"; import ProgressivePreloader from './preloader'; import LazyLoadQueue from './lazyLoadQueue'; import apiFileManager from '../lib/mtproto/apiFileManager'; -import appWebpManager from '../lib/appManagers/appWebpManager'; import VideoPlayer, { MediaProgressLine } from '../lib/mediaPlayer'; import { RichTextProcessor } from '../lib/richtextprocessor'; import { CancellablePromise } from '../lib/polyfill'; @@ -16,6 +15,7 @@ import { renderImageFromUrl } from './misc'; import appMessagesManager from '../lib/appManagers/appMessagesManager'; import { Layouter, RectPart } from './groupedLayout'; import PollElement from './poll'; +import appWebpManager from '../lib/appManagers/appWebpManager'; export type MTDocument = { _: 'document' | 'documentEmpty', @@ -40,15 +40,15 @@ export type MTDocument = { duration?: number, downloaded?: boolean, url?: string, - version?: any, audioTitle?: string, audioPerformer?: string, - sticker?: boolean, + sticker?: number, stickerEmoji?: string, stickerEmojiRaw?: string, stickerSetInput?: any, + stickerThumbConverted?: true, animated?: boolean }; @@ -74,49 +74,40 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai middleware: () => boolean, lazyLoadQueue: LazyLoadQueue }) { - let img: HTMLImageElement | SVGImageElement; - + let img: HTMLImageElement; if(withTail) { img = wrapMediaWithTail(doc, message, container, boxWidth, boxHeight, isOut); - } else if(!boxWidth && !boxHeight) { // album - let sizes = doc.thumbs; - if(!doc.downloaded && sizes && sizes[0].bytes) { - appPhotosManager.setAttachmentPreview(sizes[0].bytes, container, false); - } - - img = container.firstElementChild as HTMLImageElement || new Image(); - - if(!container.contains(img)) { - container.append(img); - } } else { - if(!container.firstElementChild || (container.firstElementChild.tagName != 'IMG' && container.firstElementChild.tagName != 'VIDEO')) { - let size = appPhotosManager.setAttachmentSize(doc, container, boxWidth, boxHeight); + if(!boxWidth && !boxHeight) { // album + let sizes = doc.thumbs; + if(!doc.downloaded && sizes && sizes[0].bytes) { + appPhotosManager.setAttachmentPreview(sizes[0].bytes, container, false); + } + } else { + if(!container.firstElementChild || (container.firstElementChild.tagName != 'IMG' && container.firstElementChild.tagName != 'VIDEO')) { + appPhotosManager.setAttachmentSize(doc, container, boxWidth, boxHeight); + } } - - img = container.firstElementChild as HTMLImageElement || new Image(); - - if(!container.contains(img)) { - container.append(img); + + img = container.lastElementChild as HTMLImageElement; + if(!img || img.tagName != 'IMG') { + container.append(img = new Image()); } - } + } let video = document.createElement('video'); + let source = document.createElement('source'); + video.append(source); + if(withTail) { - let foreignObject = document.createElementNS("http://www.w3.org/2000/svg", 'foreignObject'); - let width = img.getAttributeNS(null, 'width'); - let height = img.getAttributeNS(null, 'height'); - foreignObject.setAttributeNS(null, 'width', width); - foreignObject.setAttributeNS(null, 'height', height); - video.width = +width; - video.height = +height; + let foreignObject = img.parentElement; + video.width = +foreignObject.getAttributeNS(null, 'width'); + video.height = +foreignObject.getAttributeNS(null, 'height'); foreignObject.append(video); - img.parentElement.append(foreignObject); + } else { + container.append(video); } - let source = document.createElement('source'); - video.append(source); - let span: HTMLSpanElement; if(doc.type != 'round') { span = document.createElement('span'); @@ -154,12 +145,8 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai source.type = doc.mime_type; video.append(source); - if(!withTail) { - if(img && container.contains(img)) { - container.removeChild(img); - } - - container.append(video); + if(img && img.parentElement) { + img.remove(); } if(doc.type == 'gif') { @@ -593,14 +580,16 @@ export function wrapVoiceMessage(doc: MTDocument, withTime = false): HTMLDivElem function wrapMediaWithTail(photo: any, message: {mid: number, message: string}, container: HTMLDivElement, boxWidth: number, boxHeight: number, isOut: boolean) { let svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.classList.add('bubble__media-container', isOut ? 'is-out' : 'is-in'); - - let image = document.createElementNS("http://www.w3.org/2000/svg", "image"); - svg.append(image); - let size = appPhotosManager.setAttachmentSize(photo._ == 'document' ? photo : photo.id, svg, boxWidth, boxHeight); + let foreignObject = document.createElementNS("http://www.w3.org/2000/svg", 'foreignObject'); - let width = +svg.getAttributeNS(null, 'width'); - let height = +svg.getAttributeNS(null, 'height'); + appPhotosManager.setAttachmentSize(photo._ == 'document' ? photo : photo.id, foreignObject, boxWidth, boxHeight); + + let width = +foreignObject.getAttributeNS(null, 'width'); + let height = +foreignObject.getAttributeNS(null, 'height'); + + svg.setAttributeNS(null, 'width', '' + width); + svg.setAttributeNS(null, 'height', '' + height); let clipID = 'clip' + message.mid; svg.dataset.clipID = clipID; @@ -626,36 +615,38 @@ function wrapMediaWithTail(photo: any, message: {mid: number, message: string}, defs.innerHTML = `${clipPathHTML}`; - svg.prepend(defs); - container.appendChild(svg); + container.style.width = parseInt(container.style.width) - 9 + 'px'; + + svg.append(defs, foreignObject); + container.append(svg); + + let img = foreignObject.firstElementChild as HTMLImageElement; + if(!img) { + foreignObject.append(img = new Image()); + } - return image; + return img; } export function wrapPhoto(photoID: string, message: any, container: HTMLDivElement, boxWidth = 380, boxHeight = 380, withTail = true, isOut = false, lazyLoadQueue: LazyLoadQueue, middleware: () => boolean, size: MTPhotoSize = null) { let photo = appPhotosManager.getPhoto(photoID); - let image: SVGImageElement | HTMLImageElement; + let image: HTMLImageElement; if(withTail) { image = wrapMediaWithTail(photo, message, container, boxWidth, boxHeight, isOut); - } else if(size) { // album - let sizes = photo.sizes; - if(!photo.downloaded && sizes && sizes[0].bytes) { - appPhotosManager.setAttachmentPreview(sizes[0].bytes, container, false); + } else { + if(size) { // album + let sizes = photo.sizes; + if(!photo.downloaded && sizes && sizes[0].bytes) { + appPhotosManager.setAttachmentPreview(sizes[0].bytes, container, false); + } + } else if(boxWidth && boxHeight) { // means webpage's preview + size = appPhotosManager.setAttachmentSize(photoID, container, boxWidth, boxHeight, false); } - image = container.firstElementChild as HTMLImageElement || new Image(); - - if(!container.contains(image)) { - container.appendChild(image); - } - } else if(boxWidth && boxHeight) { // means webpage's preview - size = appPhotosManager.setAttachmentSize(photoID, container, boxWidth, boxHeight, false); - - image = container.firstElementChild as HTMLImageElement || new Image(); - - if(!container.contains(image)) { - container.appendChild(image); + image = container.lastElementChild as HTMLImageElement; + if(!image || image.tagName != 'IMG') { + container.append(image = new Image()); } } @@ -694,7 +685,7 @@ export function wrapPhoto(photoID: string, message: any, container: HTMLDivEleme } export function wrapSticker(doc: MTDocument, div: HTMLDivElement, middleware?: () => boolean, lazyLoadQueue?: LazyLoadQueue, group?: string, canvas?: boolean, play = false, onlyThumb = false) { - let stickerType = doc.mime_type == "application/x-tgsticker" ? 2 : (doc.mime_type == "image/webp" ? 1 : 0); + let stickerType = doc.sticker; if(stickerType == 2 && !LottieLoader.loaded) { LottieLoader.loadLottie(); @@ -702,8 +693,10 @@ export function wrapSticker(doc: MTDocument, div: HTMLDivElement, middleware?: ( if(!stickerType) { console.error('wrong doc for wrapSticker!', doc); - return Promise.resolve(); + throw new Error('wrong doc for wrapSticker!'); } + + div.dataset.docID = doc.id; //console.log('wrap sticker', doc, div, onlyThumb); @@ -713,32 +706,61 @@ export function wrapSticker(doc: MTDocument, div: HTMLDivElement, middleware?: ( //console.log('wrap sticker', thumb, div); if(thumb.bytes) { - apiFileManager.saveSmallFile(thumb.location, thumb.bytes); + let img = new Image(); + + if(appWebpManager.isSupported() || doc.stickerThumbConverted) { + renderImageFromUrl(img, appPhotosManager.getPreviewURLFromThumb(thumb, true)); + + div.append(img); + } else { + appWebpManager.convertToPng(thumb.bytes).then(bytes => { + if(middleware && !middleware()) return; + + thumb.bytes = bytes; + doc.stickerThumbConverted = true; + + if(!div.childElementCount) { + renderImageFromUrl(img, appPhotosManager.getPreviewURLFromThumb(thumb, true)).then(() => { + div.append(img); + }); + } + }); + } - appPhotosManager.setAttachmentPreview(thumb.bytes, div, true); + if(onlyThumb) { + return Promise.resolve(); + } + } else if(!onlyThumb && stickerType == 2) { + let img = new Image(); + let load = () => appDocsManager.downloadDocThumb(doc, thumb.type).then(url => { + if(!img.parentElement || (middleware && !middleware())) return; + let promise = renderImageFromUrl(img, url); + + if(!downloaded) { + promise.then(() => { + div.append(img); + }); + } + }); - if(onlyThumb) return Promise.resolve(); + let downloaded = appDocsManager.hasDownloadedThumb(doc.id, thumb.type); + if(downloaded) { + div.append(img); + } + + lazyLoadQueue && !downloaded ? lazyLoadQueue.push({div, load}) : load(); } } - - if(onlyThumb && doc.thumbs) { + + if(onlyThumb && doc.thumbs) { // for sticker panel let thumb = doc.thumbs[0]; - let load = () => apiFileManager.downloadSmallFile({ - _: 'inputDocumentFileLocation', - access_hash: doc.access_hash, - file_reference: doc.file_reference, - thumb_size: thumb.type, - id: doc.id - }, {dcID: doc.dc_id}).then(blob => { + let load = () => appDocsManager.downloadDocThumb(doc, thumb.type).then(url => { let img = new Image(); - - appWebpManager.polyfillImage(img, blob); - - div.append(img); - - div.dataset.docID = doc.id; - appStickersManager.saveSticker(doc); + renderImageFromUrl(img, url).then(() => { + if(middleware && !middleware()) return; + div.append(img); + }); }); return lazyLoadQueue ? (lazyLoadQueue.push({div, load}), Promise.resolve()) : load(); @@ -748,10 +770,8 @@ export function wrapSticker(doc: MTDocument, div: HTMLDivElement, middleware?: ( let load = () => appDocsManager.downloadDoc(doc.id).then(blob => { //console.log('loaded sticker:', blob, div); if(middleware && !middleware()) return; - - /* if(div.firstElementChild) { - div.firstElementChild.remove(); - } */ + + //return; if(stickerType == 2) { const reader = new FileReader(); @@ -802,15 +822,7 @@ export function wrapSticker(doc: MTDocument, div: HTMLDivElement, middleware?: ( }, {once: true}); } }); - } /* else { - let canvas = div.firstElementChild as HTMLCanvasElement; - if(!canvas.width && !canvas.height) { - console.log('Need lottie resize'); - - // @ts-ignore - animation.resize(); - } - } */ + } if(play) { animation.play(); @@ -831,23 +843,14 @@ export function wrapSticker(doc: MTDocument, div: HTMLDivElement, middleware?: ( }); } - if(!doc.url) { - appWebpManager.polyfillImage(img, blob).then((url) => { - doc.url = url; - - if(div.firstElementChild && div.firstElementChild != img) { - div.firstElementChild.remove(); - } - }); - } else { - img.src = doc.url; - } + renderImageFromUrl(img, doc.url).then(() => { + if(div.firstElementChild && div.firstElementChild != img) { + div.firstElementChild.remove(); + } - div.append(img); + div.append(img); + }); } - - div.dataset.docID = doc.id; - appStickersManager.saveSticker(doc); }); return lazyLoadQueue && (!doc.downloaded || stickerType == 2) ? (lazyLoadQueue.push({div, load, wasSeen: group == 'chat'}), Promise.resolve()) : load(); diff --git a/src/lib/appManagers/appChatsManager.ts b/src/lib/appManagers/appChatsManager.ts index 989235b6..c8f9028f 100644 --- a/src/lib/appManagers/appChatsManager.ts +++ b/src/lib/appManagers/appChatsManager.ts @@ -3,6 +3,7 @@ import { RichTextProcessor } from "../richtextprocessor"; import appUsersManager from "./appUsersManager"; import apiManager from '../mtproto/mtprotoworker'; import apiUpdatesManager from "./apiUpdatesManager"; +import appProfileManager from "./appProfileManager"; type Channel = { _: 'channel', @@ -42,6 +43,8 @@ export class AppChatsManager { public megagroups: any = {}; public cachedPhotoLocations: any = {}; + public megagroupOnlines: {[id: number]: {timestamp: number, onlines: number}} = {}; + constructor() { $rootScope.$on('apiUpdate', (e: CustomEvent) => { // console.log('on apiUpdate', update) @@ -130,7 +133,7 @@ export class AppChatsManager { return false; } - var chat = this.getChat(id); + let chat = this.getChat(id); if(chat._ == 'chatForbidden' || chat._ == 'channelForbidden' || chat.pFlags.kicked || @@ -191,7 +194,7 @@ export class AppChatsManager { } public isChannel(id: number) { - var chat = this.chats[id]; + let chat = this.chats[id]; if(chat && (chat._ == 'channel' || chat._ == 'channelForbidden') || this.channelAccess[id]) { return true; } @@ -203,7 +206,7 @@ export class AppChatsManager { return true; } - var chat = this.chats[id]; + let chat = this.chats[id]; if(chat && chat._ == 'channel' && chat.pFlags.megagroup) { return true; } @@ -246,12 +249,12 @@ export class AppChatsManager { } public hasChat(id: number, allowMin?: any) { - var chat = this.chats[id] + let chat = this.chats[id] return isObject(chat) && (allowMin || !chat.pFlags.min); } public getChatPhoto(id: number) { - var chat = this.getChat(id); + let chat = this.getChat(id); if(this.cachedPhotoLocations[id] === undefined) { this.cachedPhotoLocations[id] = chat && chat.photo ? chat.photo : {empty: true}; @@ -261,7 +264,7 @@ export class AppChatsManager { } public getChatString(id: number) { - var chat = this.getChat(id); + let chat = this.getChat(id); if(this.isChannel(id)) { return (this.isMegagroup(id) ? 's' : 'c') + id + '_' + chat.access_hash; } @@ -277,8 +280,8 @@ export class AppChatsManager { } public wrapForFull(id: number, fullChat: any) { - var chatFull = copy(fullChat); - var chat = this.getChat(id); + let chatFull = copy(fullChat); + let chat = this.getChat(id); if(!chatFull.participants_count) { chatFull.participants_count = chat.participants_count; @@ -300,10 +303,10 @@ export class AppChatsManager { } public wrapParticipants(id: number, participants: any[]) { - var chat = this.getChat(id); - var myID = appUsersManager.getSelf().id; + let chat = this.getChat(id); + let myID = appUsersManager.getSelf().id; if(this.isChannel(id)) { - var isAdmin = chat.pFlags.creator || chat.pFlags.editor || chat.pFlags.moderator; + let isAdmin = chat.pFlags.creator || chat.pFlags.editor || chat.pFlags.moderator; participants.forEach((participant) => { participant.canLeave = myID == participant.user_id; participant.canKick = isAdmin && participant._ == 'channelParticipant'; @@ -312,7 +315,7 @@ export class AppChatsManager { participant.user = appUsersManager.getUser(participant.user_id); }); } else { - var isAdmin = chat.pFlags.creator || chat.pFlags.admins_enabled && chat.pFlags.admin; + let isAdmin = chat.pFlags.creator || chat.pFlags.admins_enabled && chat.pFlags.admin; participants.forEach((participant) => { participant.canLeave = myID == participant.user_id; participant.canKick = !participant.canLeave && ( @@ -388,6 +391,44 @@ export class AppChatsManager { }); } } + + public async getOnlines(id: number): Promise { + if(this.isMegagroup(id)) { + let timestamp = Date.now() / 1000 | 0; + let cached = this.megagroupOnlines[id] ?? (this.megagroupOnlines[id] = {timestamp: 0, onlines: 1}); + if((timestamp - cached.timestamp) < 60) { + return cached.onlines; + } + + let res = await apiManager.invokeApi('messages.getOnlines', { + peer: this.getChannelInputPeer(id) + }); + + let onlines = res.onlines ?? 1; + cached.timestamp = timestamp; + cached.onlines = onlines; + + return onlines; + } else if(this.isBroadcast(id)) { + return 1; + } + + let chatInfo = appProfileManager.getChatFull(id); + if(chatInfo._ == 'chatFull' && chatInfo.participants && chatInfo.participants.participants) { + let participants = chatInfo.participants.participants; + + return participants.reduce((acc: number, participant: any) => { + let user = appUsersManager.getUser(participant.user_id); + if(user && user.status && user.status._ == 'userStatusOnline') { + return acc + 1; + } + + return acc; + }, 0); + } else { + return 1; + } + } } export default new AppChatsManager(); diff --git a/src/lib/appManagers/appDialogsManager.ts b/src/lib/appManagers/appDialogsManager.ts index 6a3ecb17..e2747874 100644 --- a/src/lib/appManagers/appDialogsManager.ts +++ b/src/lib/appManagers/appDialogsManager.ts @@ -409,12 +409,12 @@ export class AppDialogsManager { this.scroll = new Scrollable(this.chatsContainer, 'y', 'CL', this.chatList, 500); this.scroll.setVirtualContainer(this.chatList); this.scroll.onScrolledBottom = this.onChatsScroll.bind(this); - this.scroll.attachSentinels(); + //this.scroll.attachSentinels(); this.scrollArchived = new Scrollable(this.chatsArchivedContainer, 'y', 'CLA', this.chatListArchived, 500); this.scrollArchived.setVirtualContainer(this.chatListArchived); this.scrollArchived.onScrolledBottom = this.onChatsArchivedScroll.bind(this); - this.scroll.attachSentinels(); + ///this.scroll.attachSentinels(); this.setListClickListener(this.chatList); this.setListClickListener(this.chatListArchived); @@ -828,7 +828,7 @@ export class AppDialogsManager { dom.lastTimeSpan.innerHTML = timeStr; } else dom.lastTimeSpan.innerHTML = ''; - if(this.doms[peerID] || this.domsArchived[peerID]) { + if((this.doms[peerID] || this.domsArchived[peerID]) == dom) { this.setUnreadMessages(dialog); } else { // means search dom.listEl.dataset.mid = lastMessage.mid; diff --git a/src/lib/appManagers/appDocsManager.ts b/src/lib/appManagers/appDocsManager.ts index 7589568e..625a8275 100644 --- a/src/lib/appManagers/appDocsManager.ts +++ b/src/lib/appManagers/appDocsManager.ts @@ -7,8 +7,9 @@ import { isObject } from '../utils'; class AppDocsManager { private docs: {[docID: string]: MTDocument} = {}; + private thumbs: {[docIDAndSize: string]: Promise} = {}; - public saveDoc(apiDoc: MTDocument/* any */, context?: any) { + public saveDoc(apiDoc: MTDocument, context?: any) { //console.log('saveDoc', apiDoc, this.docs[apiDoc.id]); if(this.docs[apiDoc.id]) { let d = this.docs[apiDoc.id]; @@ -68,8 +69,6 @@ class AppDocsManager { break; case 'documentAttributeSticker': - apiDoc.sticker = true; - if(attribute.alt !== undefined) { apiDoc.stickerEmojiRaw = attribute.alt; apiDoc.stickerEmoji = RichTextProcessor.wrapRichText(apiDoc.stickerEmojiRaw, {noLinks: true, noLinebreaks: true}); @@ -83,11 +82,13 @@ class AppDocsManager { } } - if(apiDoc.thumbs && apiDoc.mime_type == 'image/webp') { + if(/* apiDoc.thumbs && */apiDoc.mime_type == 'image/webp') { apiDoc.type = 'sticker'; + apiDoc.sticker = 1; } else if(apiDoc.mime_type == 'application/x-tgsticker') { apiDoc.type = 'sticker'; apiDoc.animated = true; + apiDoc.sticker = 2; } break; @@ -145,7 +146,7 @@ class AppDocsManager { return isObject(docID) ? docID : this.docs[docID]; } - public getInputByID(docID: any) { + public getMediaInputByID(docID: any) { let doc = this.getDoc(docID); return { _: 'inputMediaDocument', @@ -159,6 +160,18 @@ class AppDocsManager { ttl_seconds: 0 }; } + + public getInputByID(docID: any, thumbSize?: string) { + let doc = this.getDoc(docID); + + return { + _: 'inputDocumentFileLocation', + id: doc.id, + access_hash: doc.access_hash, + file_reference: doc.file_reference, + thumb_size: thumbSize + }; + } public getFileName(doc: MTDocument) { if(doc.file_name) { @@ -173,42 +186,10 @@ class AppDocsManager { return 't_' + (doc.type || 'file') + doc.id + fileExt; } - public updateDocDownloaded(docID: string) { - var doc = this.docs[docID]; - var inputFileLocation = { - _: 'inputDocumentFileLocation', - id: docID, - access_hash: doc.access_hash, - version: doc.version, - file_name: this.getFileName(doc) - }; - - if(doc.downloaded === undefined) { - apiFileManager.getDownloadedFile(inputFileLocation, doc.size).then(() => { - doc.downloaded = true; - }, () => { - doc.downloaded = false; - }); - } - } - - public downloadDoc(docID: string | MTDocument, toFileEntry?: any): CancellablePromise { - let doc: MTDocument; - if(typeof(docID) === 'string') { - doc = this.docs[docID]; - } else { - doc = docID; - } + public downloadDoc(docID: any, toFileEntry?: any): CancellablePromise { + let doc = this.getDoc(docID); - var inputFileLocation = { - _: 'inputDocumentFileLocation', - id: doc.id, - access_hash: doc.access_hash, - file_reference: doc.file_reference, - thumb_size: '', - version: doc.version, - file_name: this.getFileName(doc) - }; + let inputFileLocation = this.getInputByID(doc); if(doc._ == 'documentEmpty') { return Promise.reject(); @@ -217,26 +198,27 @@ class AppDocsManager { if(doc.downloaded && !toFileEntry) { if(doc.url) return Promise.resolve(null); - var cachedBlob = apiFileManager.getCachedFile(inputFileLocation); + let cachedBlob = apiFileManager.getCachedFile(inputFileLocation); if(cachedBlob) { return Promise.resolve(cachedBlob); } } //historyDoc.progress = {enabled: !historyDoc.downloaded, percent: 1, total: doc.size}; - + // нет смысла делать объект с выполняющимися промисами, нижняя строка и так вернёт загружающийся - var downloadPromise: CancellablePromise = apiFileManager.downloadFile(doc.dc_id, inputFileLocation, doc.size, { + let downloadPromise: CancellablePromise = apiFileManager.downloadFile(doc.dc_id, inputFileLocation, doc.size, { mimeType: doc.mime_type || 'application/octet-stream', - toFileEntry: toFileEntry + toFileEntry: toFileEntry, + stickerType: doc.sticker }); - + downloadPromise.then((blob) => { if(blob) { doc.downloaded = true; - if(/* !doc.animated || */doc.type && doc.type != 'sticker') { - doc.url = FileManager.getFileCorrectUrl(blob, doc.mime_type); + if(doc.type && doc.sticker != 2) { + doc.url = URL.createObjectURL(blob); } } @@ -266,6 +248,36 @@ class AppDocsManager { return downloadPromise; } + + public downloadDocThumb(docID: any, thumbSize: string) { + let doc = this.getDoc(docID); + + let key = doc.id + '-' + thumbSize; + if(this.thumbs[key]) { + return this.thumbs[key]; + } + + let input = this.getInputByID(doc, thumbSize); + + if(doc._ == 'documentEmpty') { + return Promise.reject(); + } + + let mimeType = doc.sticker ? 'image/webp' : doc.mime_type; + let promise = apiFileManager.downloadSmallFile(input, { + dcID: doc.dc_id, + stickerType: doc.sticker ? 1 : undefined, + mimeType: mimeType + }); + + return this.thumbs[key] = promise.then((blob) => { + return URL.createObjectURL(blob); + }); + } + + public hasDownloadedThumb(docID: string, thumbSize: string) { + return !!this.thumbs[docID + '-' + thumbSize]; + } public async saveDocFile(docID: string) { var doc = this.docs[docID]; diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index 563c7829..3935e501 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -2,7 +2,7 @@ import apiManager from '../mtproto/mtprotoworker'; import { $rootScope, numberWithCommas, findUpClassName, formatNumber, placeCaretAtEnd, findUpTag, langPack, whichChild } from "../utils"; import appUsersManager from "./appUsersManager"; -import appMessagesManager, { Dialog } from "./appMessagesManager"; +import appMessagesManager from "./appMessagesManager"; import appPeersManager from "./appPeersManager"; import appProfileManager from "./appProfileManager"; import appDialogsManager from "./appDialogsManager"; @@ -38,7 +38,7 @@ appSidebarLeft; // just to include let testScroll = false; export class AppImManager { - public pageEl = document.querySelector('.page-chats') as HTMLDivElement; + public pageEl = document.getElementById('page-chats') as HTMLDivElement; public btnMute = this.pageEl.querySelector('.tool-mute') as HTMLButtonElement; public btnMenuMute = this.pageEl.querySelector('.menu-mute') as HTMLButtonElement; public avatarEl = document.getElementById('im-avatar') as AvatarElement; @@ -754,34 +754,22 @@ export class AppImManager { this.log('loadMoreHistory', top); if(!this.peerID || testScroll || this.setPeerPromise || (top && this.getHistoryTopPromise) || (!top && this.getHistoryBottomPromise)) return; - let history = Object.keys(this.bubbles).map(id => +id).sort((a, b) => a - b); + // warning, если иды только отрицательные то вниз не попадёт (хотя мб и так не попадёт) + let history = Object.keys(this.bubbles).map(id => +id).filter(id => id > 0).sort((a, b) => a - b); if(!history.length) return; - /* let history = appMessagesManager.historiesStorage[this.peerID].history; - let length = history.length; */ - - // filter negative ids - let lastBadIndex = -1; - for(let i = 0; i < history.length; ++i) { - if(history[i] <= 0) lastBadIndex = i; - else break; - } - if(lastBadIndex != -1) { - history = history.slice(lastBadIndex + 1); - } - if(top && !this.scrolledAll) { this.log('Will load more (up) history by id:', history[0], 'maxID:', history[history.length - 1], history); + /* if(history.length == 75) { + this.log('load more', this.scrollable.scrollHeight, this.scrollable.scrollTop, this.scrollable); + return; + } */ /* false && */this.getHistory(history[0], true); } if(this.scrolledAllDown) return; let dialog = appMessagesManager.getDialogByPeerID(this.peerID)[0]; - /* if(!dialog) { - this.log.warn('no dialog for load history'); - return; - } */ // if scroll down after search if(!top && (!dialog || history.indexOf(dialog.top_message) === -1)) { @@ -822,14 +810,14 @@ export class AppImManager { } public setScroll() { - this.scrollable = new Scrollable(this.bubblesContainer, 'y', 'IM', this.chatInner); + this.scrollable = new Scrollable(this.bubblesContainer, 'y', 'IM', this.chatInner, 300); this.scroll = this.scrollable.container; this.bubblesContainer.append(this.goDownBtn); this.scrollable.onScrolledTop = () => this.loadMoreHistory(true); this.scrollable.onScrolledBottom = () => this.loadMoreHistory(false); - this.scrollable.attachSentinels(undefined, 300); + //this.scrollable.attachSentinels(undefined, 300); this.scroll.addEventListener('scroll', this.onScroll.bind(this)); this.scroll.parentElement.classList.add('scrolled-down'); @@ -850,30 +838,7 @@ export class AppImManager { this.subtitleEl.innerText = appSidebarRight.profileElements.subtitle.innerText = ''; } - Promise.all([ - appPeersManager.isMegagroup(this.peerID) ? apiManager.invokeApi('messages.getOnlines', { - peer: appPeersManager.getInputPeerByID(this.peerID) - }) as Promise : Promise.resolve(), - // will redirect if wrong - appProfileManager.getChatFull(chat.id) - ]).then(results => { - let [chatOnlines, chatInfo] = results; - - let onlines = chatOnlines ? chatOnlines.onlines : 1; - - if(chatInfo._ == 'chatFull' && chatInfo.participants && chatInfo.participants.participants) { - let participants = chatInfo.participants.participants; - - onlines = participants.reduce((acc: number, participant: any) => { - let user = appUsersManager.getUser(participant.user_id); - if(user && user.status && user.status._ == 'userStatusOnline') { - return acc + 1; - } - - return acc; - }, 0); - } - + appProfileManager.getChatFull(chat.id).then((chatInfo: any) => { this.log('chatInfo res:', chatInfo); if(chatInfo.pinned_msg_id) { // request pinned message @@ -884,12 +849,17 @@ export class AppImManager { let participants_count = chatInfo.participants_count || (chatInfo.participants && chatInfo.participants.participants && chatInfo.participants.participants.length); if(participants_count) { let subtitle = numberWithCommas(participants_count) + ' ' + (isChannel ? 'subscribers' : 'members'); - - if(onlines > 1) { - subtitle += ', ' + numberWithCommas(onlines) + ' online'; - } - + this.subtitleEl.innerText = appSidebarRight.profileElements.subtitle.innerText = subtitle; + + if(participants_count < 2) return; + appChatsManager.getOnlines(chat.id).then(onlines => { + if(onlines > 1) { + subtitle += ', ' + numberWithCommas(onlines) + ' online'; + } + + this.subtitleEl.innerText = appSidebarRight.profileElements.subtitle.innerText = subtitle; + }); } }); } else if(!appUsersManager.isBot(this.peerID)) { // user @@ -1071,7 +1041,7 @@ export class AppImManager { } this.scrollable.container.append(this.chatInner); - this.scrollable.attachSentinels(); + //this.scrollable.attachSentinels(); //this.scrollable.container.insertBefore(this.chatInner, this.scrollable.container.lastElementChild); this.lazyLoadQueue.unlock(); @@ -1328,34 +1298,40 @@ export class AppImManager { public renderMessagesQueue(message: any, bubble: HTMLDivElement, reverse: boolean) { let promises: Promise[] = []; - (Array.from(bubble.querySelectorAll('img, image, video')) as HTMLImageElement[]).forEach(el => { - if(el.tagName == 'VIDEO') { + (Array.from(bubble.querySelectorAll('img, video')) as HTMLImageElement[]).forEach(el => { + if(el instanceof HTMLVideoElement) { let source = el.firstElementChild as HTMLSourceElement; if(!source || !source.src) { this.log.warn('no source', el, source, 'src', source.src); return; - } - } else if(el.complete || (!el.src && !el.getAttribute('href'))) return; + } else if(el.readyState >= 4) return; + } else if(el.complete || !el.src) return; - let src = el.src || el.getAttributeNS(null, 'href'); + let src = el.src; let promise = new Promise((resolve, reject) => { - if(el.tagName == 'VIDEO') { - el.addEventListener('loadeddata', () => { - clearTimeout(timeout); - resolve(); - }); + let r: () => boolean; + let onLoad = () => { + clearTimeout(timeout); + resolve(); + }; + + if(el instanceof HTMLVideoElement) { + el.addEventListener('loadeddata', onLoad); + r = () => el.readyState >= 4; } else { - el.addEventListener('load', () => { - clearTimeout(timeout); - resolve(); - }); + el.addEventListener('load', onLoad); + r = () => el.complete; } + // for safari + let c = () => r() ? onLoad() : window.requestAnimationFrame(c); + window.requestAnimationFrame(c); + let timeout = setTimeout(() => { console.log('did not called', el, el.parentElement, el.complete, src); reject(); - }, 5000); + }, 1500); }); promises.push(promise); @@ -2092,7 +2068,7 @@ export class AppImManager { avatarElem.setAttribute('peer-title', message.fwd_from.from_name); } - avatarElem.setAttribute('peer', '' + ((message.fwd_from ? message.fwdFromID : message.fromID) || 0)); + avatarElem.setAttribute('peer', '' + ((message.fwd_from && this.peerID == this.myID ? message.fwdFromID : message.fromID) || 0)); this.log('exec loadDialogPhoto', message); @@ -2151,43 +2127,28 @@ export class AppImManager { console.time('appImManager render history'); - let firstLoad = !!this.setPeerPromise && false; - - let peerID = this.peerID; return new Promise((resolve, reject) => { let method = (reverse ? history.shift : history.pop).bind(history); - let r = () => { - if(this.peerID != peerID) { - return reject('peer changed'); - } - - let msgID = method(); - if(!msgID) { - return; - } - - let message = appMessagesManager.getMessage(msgID); - this.renderMessage(message, reverse, true); - - firstLoad ? window.requestAnimationFrame(r) : r(); - }; - - firstLoad ? window.requestAnimationFrame(r) : r(); - let realLength = this.scrollable.length; let previousScrollHeightMinusTop: number; if(realLength > 0 && reverse) { this.messagesQueueOnRender = () => { - let scrollTop = realLength ? this.scrollable.scrollTop : 0; - previousScrollHeightMinusTop = realLength ? this.scrollable.scrollHeight - scrollTop : 0; + let scrollTop = this.scrollable.scrollTop; + previousScrollHeightMinusTop = this.scrollable.scrollHeight - scrollTop; this.log('performHistoryResult: messagesQueueOnRender, scrollTop:', scrollTop, previousScrollHeightMinusTop); + this.messagesQueueOnRender = undefined; }; } + while(history.length) { + let message = appMessagesManager.getMessage(method()); + this.renderMessage(message, reverse, true); + } + (this.messagesQueuePromise || Promise.resolve()).then(() => { - if(realLength > 0 && reverse && previousScrollHeightMinusTop !== undefined) { + if(previousScrollHeightMinusTop !== undefined) { this.log('performHistoryResult: will set scrollTop', this.scrollable.scrollHeight, previousScrollHeightMinusTop); this.scrollable.scrollTop = this.scrollable.scrollHeight - previousScrollHeightMinusTop; } diff --git a/src/lib/appManagers/appMediaViewer.ts b/src/lib/appManagers/appMediaViewer.ts index e6737529..980af485 100644 --- a/src/lib/appManagers/appMediaViewer.ts +++ b/src/lib/appManagers/appMediaViewer.ts @@ -57,6 +57,8 @@ export class AppMediaViewer { private reverse = false; // reverse means next = higher msgid private needLoadMore = true; + + private pageEl = document.getElementById('page-chats') as HTMLDivElement; constructor() { this.log = logger('AMV'); @@ -568,7 +570,7 @@ export class AppMediaViewer { return promise; } - public updateMediaSource(target: HTMLElement, url: string, tagName: 'source' | 'image') { + public updateMediaSource(target: HTMLElement, url: string, tagName: 'source' | 'img') { //if(target instanceof SVGSVGElement) { let el = target.querySelector(tagName) as HTMLElement; renderImageFromUrl(el, url); @@ -665,17 +667,20 @@ export class AppMediaViewer { ////////this.log('wasActive:', wasActive); + if(useContainerAsTarget) { + target = target.querySelector('img, video') || target; + } + let mover = this.content.mover; - let maxWidth = appPhotosManager.windowW - 16; + //let maxWidth = appPhotosManager.windowW - 16; + let maxWidth = this.pageEl.scrollWidth - 16; let maxHeight = appPhotosManager.windowH - 100; if(isVideo) { - let size = appPhotosManager.setAttachmentSize(media, container, maxWidth, maxHeight); + appPhotosManager.setAttachmentSize(media, container, maxWidth, maxHeight); ////////this.log('will wrap video', media, size); - if(useContainerAsTarget) target = target.querySelector('img, video') || target; - let afterTimeout = this.setMoverToTarget(target, false, fromRight); //return; // set and don't move //if(wasActive) return; @@ -745,8 +750,6 @@ export class AppMediaViewer { } else { let size = appPhotosManager.setAttachmentSize(media.id, container, maxWidth, maxHeight); - if(useContainerAsTarget) target = target.querySelector('img, video') || target; - let afterTimeout = this.setMoverToTarget(target, false, fromRight); //return; // set and don't move //if(wasActive) return; @@ -766,8 +769,8 @@ export class AppMediaViewer { let url = media.url; if(target instanceof SVGSVGElement) { - this.updateMediaSource(target, url, 'image'); - this.updateMediaSource(mover, url, 'image'); + this.updateMediaSource(target, url, 'img'); + this.updateMediaSource(mover, url, 'img'); } else { let aspecter = mover.firstElementChild; let image = aspecter.firstElementChild as HTMLImageElement; diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index a55869ac..dd6f0489 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -224,7 +224,7 @@ export class AppMessagesManager { }).catch(resolve); }); - //setInterval(() => this.saveState(), 10000); + setInterval(() => this.saveState(), 10000); } public saveState() { @@ -1166,7 +1166,7 @@ export class AppMessagesManager { } else { let doc = messageMedia.document; appDocsManager.saveDoc(doc); - inputMedia = appDocsManager.getInputByID(doc.id); + inputMedia = appDocsManager.getMediaInputByID(doc.id); } inputs.push({ @@ -3002,7 +3002,11 @@ export class AppMessagesManager { if(messageID > maxID) { continue; } + message = this.messagesStorage[messageID]; + if(!message) { + continue; + } if(message.pFlags.out != isOut) { continue; diff --git a/src/lib/appManagers/appPhotosManager.ts b/src/lib/appManagers/appPhotosManager.ts index b384bfca..02a70e9b 100644 --- a/src/lib/appManagers/appPhotosManager.ts +++ b/src/lib/appManagers/appPhotosManager.ts @@ -176,8 +176,8 @@ export class AppPhotosManager { }; }); } - - public setAttachmentPreview(bytes: Uint8Array, element: HTMLElement | SVGSVGElement, isSticker = false, background = false) { + + public getPreviewURLFromBytes(bytes: Uint8Array | number[], isSticker = false) { let arr: Uint8Array; if(!isSticker) { arr = AppPhotosManager.jf.concat(bytes.slice(3), AppPhotosManager.Df); @@ -187,9 +187,7 @@ export class AppPhotosManager { arr = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes); } - //console.log('setAttachmentPreview', bytes, arr, div, isSticker); - - let blob = new Blob([arr], {type: "image/jpeg"}); + //console.log('getPreviewURLFromBytes', bytes, arr, div, isSticker); /* let reader = new FileReader(); reader.onloadend = () => { @@ -197,8 +195,19 @@ export class AppPhotosManager { }; reader.readAsDataURL(blob); */ + let blob = new Blob([arr], {type: "image/jpeg"}); + + return URL.createObjectURL(blob); + } + + public getPreviewURLFromThumb(thumb: any, isSticker = false) { + return thumb.url ?? (thumb.url = this.getPreviewURLFromBytes(thumb.bytes, isSticker)); + } + + public setAttachmentPreview(bytes: Uint8Array | number[], element: HTMLElement | SVGForeignObjectElement, isSticker = false, background = false) { + let url = this.getPreviewURLFromBytes(bytes, isSticker); + if(background) { - let url = URL.createObjectURL(blob); let img = new Image(); img.src = url; img.addEventListener('load', () => { @@ -206,30 +215,23 @@ export class AppPhotosManager { }); return element; - //element.style.backgroundImage = 'url(' + url + ')'; } else { - if(element instanceof SVGSVGElement) { - let image = element.firstElementChild as SVGImageElement || document.createElementNS("http://www.w3.org/2000/svg", "image"); - image.setAttributeNS(null, 'href', URL.createObjectURL(blob)); - element.append(image); - - return image; - } else if(element instanceof HTMLImageElement) { - element.src = URL.createObjectURL(blob); + if(element instanceof HTMLImageElement) { + element.src = url; return element; } else { let img = new Image(); - img.style.width = '100%'; - img.style.height = '100%'; + /* img.style.width = '100%'; + img.style.height = '100%'; */ - img.src = URL.createObjectURL(blob); + img.src = url; element.append(img); return img; } } } - public setAttachmentSize(photoID: any, element: HTMLElement | SVGSVGElement, boxWidth = 380, boxHeight = 380, isSticker = false) { + public setAttachmentSize(photoID: any, element: HTMLElement | SVGForeignObjectElement, boxWidth = 380, boxHeight = 380, isSticker = false) { let photo: /* MTDocument | MTPhoto */any = null; if(typeof(photoID) === 'string') { @@ -243,7 +245,7 @@ export class AppPhotosManager { //console.log('setAttachmentSize', photo, photo.sizes[0].bytes, div); let sizes = photo.sizes || photo.thumbs; - if((!photo.downloaded || (isSticker && photo.animated)) && sizes && sizes[0].bytes) { + if(!photo.downloaded && !isSticker && sizes && sizes[0].bytes) { this.setAttachmentPreview(sizes[0].bytes, element, isSticker); } @@ -258,17 +260,11 @@ export class AppPhotosManager { } let {w, h} = calcImageInBox(width, height, boxWidth, boxHeight); - if(element instanceof SVGSVGElement) { + if(element instanceof SVGForeignObjectElement) { element.setAttributeNS(null, 'width', '' + w); element.setAttributeNS(null, 'height', '' + h); //console.log('set dimensions to svg element:', element, w, h); - - if(element.firstElementChild) { - let imageSvg = element.firstElementChild as SVGImageElement; - imageSvg.setAttributeNS(null, 'width', '' + w); - imageSvg.setAttributeNS(null, 'height', '' + h); - } } else { element.style.width = w + 'px'; element.style.height = h + 'px'; diff --git a/src/lib/appManagers/appSidebarRight.ts b/src/lib/appManagers/appSidebarRight.ts index b12d0a8a..5a81d7ed 100644 --- a/src/lib/appManagers/appSidebarRight.ts +++ b/src/lib/appManagers/appSidebarRight.ts @@ -111,14 +111,14 @@ class AppSidebarRight { let container = this.profileContentEl.querySelector('.content-container .tabs-container') as HTMLDivElement; this.profileTabs = this.profileContentEl.querySelector('.profile-tabs') as HTMLUListElement; - this.scroll = new Scrollable(this.profileContainer, 'y', 'SR'); + this.scroll = new Scrollable(this.profileContainer, 'y', 'SR', undefined, 400); this.scroll.onScrolledBottom = () => { if(this.sharedMediaSelected && this.sharedMediaSelected.childElementCount/* && false */) { this.log('onScrolledBottom will load media'); this.loadSidebarMedia(true); } }; - this.scroll.attachSentinels(undefined, 400); + //this.scroll.attachSentinels(undefined, 400); horizontalMenu(this.profileTabs, container, (id, tabContent) => { if(this.prevTabID == id) return; diff --git a/src/lib/appManagers/appStickersManager.ts b/src/lib/appManagers/appStickersManager.ts index 7bb783eb..5a31eaf2 100644 --- a/src/lib/appManagers/appStickersManager.ts +++ b/src/lib/appManagers/appStickersManager.ts @@ -51,6 +51,8 @@ class AppStickersManager { private stickerSets: { [stickerSetID: string]: MTStickerSetFull } = {}; + + private saveSetsTimeout: number; constructor() { AppStorage.get<{ @@ -59,17 +61,15 @@ class AppStickersManager { if(sets) { for(let id in sets) { let set = sets[id]; - set.documents.forEach(doc => { - this.saveSticker(doc); - }); + this.saveStickers(set.documents); } this.stickerSets = sets; } - if(!this.stickerSets['emoji']) { + //if(!this.stickerSets['emoji']) { this.getStickerSet({id: 'emoji', access_hash: ''}); - } + //} }); } @@ -81,6 +81,12 @@ class AppStickersManager { return doc; } + + public saveStickers(docs: MTDocument[]) { + docs.forEach((doc, idx) => { + docs[idx] = this.saveSticker(doc); + }); + } public getSticker(fileID: string) { return this.documents[fileID]; @@ -115,6 +121,20 @@ class AppStickersManager { return stickerSet; } + public async getRecentStickers() { + let res: { + _: string, + hash: number, + packs: any[], + stickers: MTDocument[], + dates: number[] + } = await apiManager.invokeApi('messages.getRecentStickers', {flags: 0, hash: 0}); + + this.saveStickers(res.stickers); + + return res; + } + public getAnimatedEmojiSticker(emoji: string) { let stickerSet = this.stickerSets.emoji; @@ -122,7 +142,7 @@ class AppStickersManager { return stickerSet.documents.find(doc => doc.stickerEmojiRaw == emoji); } - public async saveStickerSet(res: { + public saveStickerSet(res: { _: "messages.stickerSet", set: MTStickerSet, packs: any[], @@ -136,12 +156,18 @@ class AppStickersManager { documents: res.documents }; - res.documents.forEach(this.saveSticker.bind(this)); + this.saveStickers(res.documents); //console.log('stickers wrote', this.stickerSets); - await AppStorage.set({ - stickerSets: this.stickerSets - }); + if(this.saveSetsTimeout) return; + this.saveSetsTimeout = setTimeout(() => { + AppStorage.set({ + stickerSets: this.stickerSets + }); + + this.saveSetsTimeout = 0; + }, 0); + /* AppStorage.get('stickerSets').then((sets: any) => { this.stickerSets = sets; @@ -153,7 +179,9 @@ class AppStickersManager { let thumb = stickerSet.thumb; let dcID = stickerSet.thumb_dc_id; - let promise = apiFileManager.downloadSmallFile({ + let isAnimated = stickerSet.pFlags?.animated; + + let promise = apiFileManager.downloadFile(dcID, { _: 'inputStickerSetThumb', stickerset: { _: 'inputStickerSetID', @@ -162,7 +190,10 @@ class AppStickersManager { }, volume_id: thumb.location.volume_id, local_id: thumb.location.local_id - }, {dcID: dcID}); + }, thumb.size, { + stickerType: isAnimated ? 2 : 1, + mimeType: isAnimated ? "application/x-tgsticker" : 'image/webp' + }); return promise; } diff --git a/src/lib/appManagers/appWebpManager.ts b/src/lib/appManagers/appWebpManager.ts index b9c3c587..715b06b5 100644 --- a/src/lib/appManagers/appWebpManager.ts +++ b/src/lib/appManagers/appWebpManager.ts @@ -1,31 +1,21 @@ -// @ts-ignore -//import createWorker from 'offscreen-canvas/create-worker'; - class AppWebpManager { - public webpMachine: any = null; - public loaded: Promise; - public busyPromise: Promise; - public queue: {bytes: Uint8Array, img: HTMLImageElement, callback: (url: string) => void}[] = []; - //public worker: any; - public webpSupport: Promise = null; + private webpMachine: any = null; + private loaded: Promise; + private busyPromise: Promise; + private queue: {bytes: Uint8Array, callback: (res: Uint8Array) => void}[] = []; + + private testPromise: Promise = null; + public webpSupport = false; constructor() { - //let canvas = document.createElement('canvas'); - //console.log('got message from worker:', canvas.toDataURL()); - /* this.worker = createWorker(canvas, '/webp.bundle.js', (e: any) => { - // Messages from the worker - console.log('got message from worker:', e, canvas.toDataURL()); - }); */ - - this.webpSupported().then(res => { - }); + this.testWebpSupport(); } - public loadWebpHero() { + private loadWebpHero() { if(this.loaded) return this.loaded; this.loaded = new Promise(async(resolve, reject) => { - let res = await this.webpSupported(); + let res = await this.testWebpSupport(); if(!res) { (window as any).webpLoaded = () => { @@ -46,17 +36,16 @@ class AppWebpManager { }); } - convert(bytes: Uint8Array): Promise { + private convert(bytes: Uint8Array): AppWebpManager['busyPromise'] { return this.webpMachine.decode(bytes); - //return this.worker.post({message: 'webpBytes', bytes}); } - async processQueue() { + private async processQueue() { if(this.busyPromise) return; - this.busyPromise = Promise.resolve(''); + this.busyPromise = Promise.resolve(); - let {img, bytes, callback} = this.queue.pop(); + let {bytes, callback} = this.queue.pop(); if(!this.loaded) { this.loadWebpHero(); @@ -65,13 +54,11 @@ class AppWebpManager { await this.loaded; this.busyPromise = this.convert(bytes); - let url = await this.busyPromise; - let imgTemp = new Image(); - imgTemp.src = url; - imgTemp.onload = () => { - img.src = imgTemp.src; - }; - callback(url); + let res = await this.busyPromise; + + console.log('converted webp', res); + + callback(res as Uint8Array); this.busyPromise = null; @@ -80,42 +67,33 @@ class AppWebpManager { } } - webpSupported() { - if(this.webpSupport) return this.webpSupport; + public testWebpSupport() { + if(this.testPromise) return this.testPromise; - return this.webpSupport = new Promise((resolve, reject) => { - var webP = new Image(); + return this.testPromise = new Promise((resolve, reject) => { + let webP = new Image(); webP.src = 'data:image/webp;base64,UklGRi4AAABXRUJQVlA4TCEAAAAvAUAAEB8wAiMw' + 'AgSSNtse/cXjxyCCmrYNWPwmHRH9jwMA'; webP.onload = webP.onerror = () => { - resolve(webP.height === 2); + resolve(this.webpSupport = webP.height === 2/* && false */); }; }); } - async polyfillImage(img: HTMLImageElement, blob: Blob) { - /* console.log('polyfillImage', this); - return this.webpMachine.polyfillImage(image); */ - - //if(await this.webpMachine.webpSupport) { - if(await this.webpSupport) { - let url = URL.createObjectURL(blob); - img.src = url; - return url; - } + public isSupported() { + return this.webpSupport; + } - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.addEventListener('loadend', (e) => { - // @ts-ignore - let bytes = new Uint8Array(e.srcElement.result); - - this.queue.push({bytes, img, callback: resolve}); - this.processQueue(); - }); - reader.readAsArrayBuffer(blob); + public convertToPng(bytes: Uint8Array) { + console.warn('convertToPng!'); + return new Promise((resolve, reject) => { + // @ts-ignore + this.queue.push({bytes, callback: resolve}); + this.processQueue(); }); } } -export default new AppWebpManager(); +const appWebpManager = new AppWebpManager(); +(window as any).appWebpManager = appWebpManager; +export default appWebpManager; diff --git a/src/lib/bin_utils.ts b/src/lib/bin_utils.ts index 872505fa..82829664 100644 --- a/src/lib/bin_utils.ts +++ b/src/lib/bin_utils.ts @@ -342,24 +342,6 @@ export function longFromInts(high: number, low: number) { return bigint(high).shiftLeft(32).add(bigint(low)).toString(10); } -export function intToUint(val: number | string) { - if(typeof(val) === 'string') val = parseInt(val); - - /* if(val < 0) { - val = val + 4294967296; - } */ - - return val; -} - -export function uintToInt(val: number) { - /* if(val > 2147483647) { - val = val - 4294967296; - } */ - - return val; -} - export function addPadding(bytes: any, blockSize: number = 16, zeroes?: boolean, full = false, prepend = false) { let len = bytes.byteLength || bytes.length; let needPadding = blockSize - (len % blockSize); diff --git a/src/lib/filemanager.ts b/src/lib/filemanager.ts index 29ef400e..8eefcb7e 100644 --- a/src/lib/filemanager.ts +++ b/src/lib/filemanager.ts @@ -1,4 +1,4 @@ -import {blobSafeMimeType, blobConstruct, bytesToBase64} from './bin_utils'; +import {blobConstruct} from './bin_utils'; /* import 'web-streams-polyfill/ponyfill'; // @ts-ignore @@ -139,14 +139,6 @@ class FileManager { return fakeFileWriter; } - - public getFileCorrectUrl(fileData: Blob | number[], mimeType: string): string { - var safeMimeType = blobSafeMimeType(mimeType); - if(fileData instanceof Blob) { - return URL.createObjectURL(fileData); - } - return 'data:' + safeMimeType + ';base64,' + bytesToBase64(fileData); - } public download(blob: Blob, mimeType: string, fileName: string) { if(window.navigator && navigator.msSaveBlob !== undefined) { @@ -180,7 +172,7 @@ class FileManager { return; } - let url = this.getFileCorrectUrl(blob, mimeType); + let url = URL.createObjectURL(blob); var anchor = document.createElementNS('http://www.w3.org/1999/xhtml', 'a') as HTMLAnchorElement; anchor.href = url as string; anchor.download = fileName; diff --git a/src/lib/idb.ts b/src/lib/idb.ts index 659b6bff..d0bec7e6 100644 --- a/src/lib/idb.ts +++ b/src/lib/idb.ts @@ -51,6 +51,8 @@ class IdbFileStorage { request.onsuccess = (event) => { finished = true; var db = request.result; + + console.log('Opened IndexedDB'); db.onerror = (error) => { this.storageIsAvailable = false; diff --git a/src/lib/lottieLoader.ts b/src/lib/lottieLoader.ts index f852cf11..06372d1a 100644 --- a/src/lib/lottieLoader.ts +++ b/src/lib/lottieLoader.ts @@ -96,7 +96,7 @@ class LottieLoader { params.renderer = 'svg'; //} - params.rendererSettings = { + let rendererSettings = { //context: context, // the canvas context //preserveAspectRatio: 'xMinYMin slice', // Supports the same options as the svg element's preserveAspectRatio property clearCanvas: true, @@ -104,6 +104,12 @@ class LottieLoader { hideOnTransparent: true, //Boolean, only svg renderer, hides elements when opacity reaches 0 (defaults to true), }; + if(params.rendererSettings) { + params.rendererSettings = Object.assign(params.rendererSettings, rendererSettings); + } else { + params.rendererSettings = rendererSettings; + } + if(!this.lottie) { if(!this.loaded) this.loadLottie(); await this.loaded; diff --git a/src/lib/mtproto/apiFileManager.ts b/src/lib/mtproto/apiFileManager.ts index e5ac45e1..17a7c878 100644 --- a/src/lib/mtproto/apiFileManager.ts +++ b/src/lib/mtproto/apiFileManager.ts @@ -5,6 +5,7 @@ import FileManager from "../filemanager"; //import apiManager from "./apiManager"; import apiManager from "./mtprotoworker"; import { logger, deferredPromise, CancellablePromise } from "../polyfill"; +import appWebpManager from "../appManagers/appWebpManager"; export class ApiFileManager { public cachedSavePromises: { @@ -14,7 +15,7 @@ export class ApiFileManager { [fileName: string]: any } = {}; public cachedDownloads: { - [fileName: string]: any + [fileName: string]: Blob } = {}; /* public indexedKeys: Set = new Set(); @@ -84,25 +85,38 @@ export class ApiFileManager { }); } - public getFileName(location: any) { + public getFileName(location: any, options?: Partial<{ + stickerType: number + }>) { switch(location._) { - case 'inputDocumentFileLocation': - var fileName = (location.file_name as string || '').split('.'); - var ext: string = fileName[fileName.length - 1] || ''; + case 'inputDocumentFileLocation': { + let fileName = (location.file_name as string || '').split('.'); + let ext = fileName[fileName.length - 1] || ''; + + if(options?.stickerType == 1 && !appWebpManager.isSupported()) { + ext += '.png' + } - var versionPart = location.version ? ('v' + location.version) : ''; - return (fileName[0] ? fileName[0] + '_' : '') + location.id + versionPart + (ext ? '.' + ext : ext); + let thumbPart = location.thumb_size ? '_' + location.thumb_size : ''; + return (fileName[0] ? fileName[0] + '_' : '') + location.id + thumbPart + (ext ? '.' + ext : ext); + } - default: + default: { if(!location.volume_id && !location.file_reference) { this.log.trace('Empty location', location); } + let ext = 'jpg'; + if(options?.stickerType == 1 && !appWebpManager.isSupported()) { + ext += '.png' + } + if(location.volume_id) { return location.volume_id + '_' + location.local_id + '.' + ext; } else { return location.id + '_' + location.access_hash + '.' + ext; } + } } } @@ -145,10 +159,11 @@ export class ApiFileManager { return this.cachedSavePromises[fileName]; } - public downloadSmallFile(location: any, options: { - mimeType?: string, - dcID?: number, - } = {}): Promise { + public downloadSmallFile(location: any, options: Partial<{ + mimeType: string, + dcID: number, + stickerType: number + }> = {}): Promise { if(!FileManager.isAvailable()) { return Promise.reject({type: 'BROWSER_BLOB_NOT_SUPPORTED'}); } @@ -159,10 +174,16 @@ export class ApiFileManager { //this.log('downloadSmallFile', location, options); + let processSticker = false; + if(options.stickerType == 1 && !appWebpManager.isSupported()) { + processSticker = true; + options.mimeType = 'image/png'; + } + let dcID = options.dcID || location.dc_id; let mimeType = options.mimeType || 'image/jpeg'; - var fileName = this.getFileName(location); - var cachedPromise = this.cachedSavePromises[fileName] || this.cachedDownloadPromises[fileName]; + let fileName = this.getFileName(location, options); + let cachedPromise = this.cachedSavePromises[fileName] || this.cachedDownloadPromises[fileName]; //this.log('downloadSmallFile!', location, options, fileName, cachedPromise); @@ -170,13 +191,14 @@ export class ApiFileManager { return cachedPromise; } - var fileStorage = this.getFileStorage(); + let fileStorage = this.getFileStorage(); - return this.cachedDownloadPromises[fileName] = fileStorage.getFile(fileName).then((blob: any) => { + return this.cachedDownloadPromises[fileName] = fileStorage.getFile(fileName).then((blob) => { + //throw ''; return this.cachedDownloads[fileName] = blob; - }, () => { - var downloadPromise = this.downloadRequest(dcID, () => { - var inputLocation = location; + }).catch(() => { + let downloadPromise = this.downloadRequest(dcID, () => { + let inputLocation = location; if(!inputLocation._ || inputLocation._ == 'fileLocation') { inputLocation = Object.assign({}, location, {_: 'inputFileLocation'}); } @@ -196,18 +218,17 @@ export class ApiFileManager { }); }, dcID); - var processDownloaded = (bytes: Uint8Array) => { + let processDownloaded = (bytes: Uint8Array) => { //this.log('processDownloaded', location, bytes); - return Promise.resolve(bytes); - /* if(!location.sticker || WebpManager.isWebpSupported()) { - return qSync.when(bytes); + if(processSticker) { + return appWebpManager.convertToPng(bytes); } - return WebpManager.getPngBlobFromWebp(bytes); */ + return Promise.resolve(bytes); }; - return fileStorage.getFileWriter(fileName, mimeType).then((fileWriter: any) => { + return fileStorage.getFileWriter(fileName, mimeType).then(fileWriter => { return downloadPromise.then((result: any) => { return processDownloaded(result.bytes).then((proccessedResult) => { return FileManager.write(fileWriter, proccessedResult).then(() => { @@ -236,12 +257,12 @@ export class ApiFileManager { }); } */ - public downloadFile(dcID: number, location: any, size: number, options: { - mimeType?: string, - dcID?: number, - toFileEntry?: any, - limitPart?: number - } = {}): CancellablePromise { + public downloadFile(dcID: number, location: any, size: number, options: Partial<{ + mimeType: string, + toFileEntry: any, + limitPart: number, + stickerType: number + }> = {}): CancellablePromise { if(!FileManager.isAvailable()) { return Promise.reject({type: 'BROWSER_BLOB_NOT_SUPPORTED'}); } @@ -250,11 +271,21 @@ export class ApiFileManager { this.getIndexedKeys(); } */ + let processSticker = false; + if(options.stickerType == 1 && !appWebpManager.isSupported()) { + if(options.toFileEntry || size > 524288) { + delete options.stickerType; + } else { + processSticker = true; + options.mimeType = 'image/png'; + } + } + // this.log('Dload file', dcID, location, size) - var fileName = this.getFileName(location); - var toFileEntry = options.toFileEntry || null; - var cachedPromise = this.cachedSavePromises[fileName] || this.cachedDownloadPromises[fileName]; - var fileStorage = this.getFileStorage(); + let fileName = this.getFileName(location, options); + let toFileEntry = options.toFileEntry || null; + let cachedPromise = this.cachedSavePromises[fileName] || this.cachedDownloadPromises[fileName]; + let fileStorage = this.getFileStorage(); //this.log('downloadFile', fileStorage.name, fileName, fileName.length, location, arguments); @@ -272,7 +303,7 @@ export class ApiFileManager { if(blob.size < size) { this.log('downloadFile need to deleteFile, wrong size:', blob.size, size); - return this.deleteFile(location).then(() => { + return this.deleteFile(fileName).then(() => { return this.downloadFile(dcID, location, size, options); }).catch(() => { return this.downloadFile(dcID, location, size, options); @@ -303,10 +334,11 @@ export class ApiFileManager { fileStorage.getFile(fileName, size).then(async(blob: Blob) => { //this.log('is that i wanted'); + //throw ''; if(blob.size < size) { this.log('downloadFile need to deleteFile 2, wrong size:', blob.size, size); - await this.deleteFile(location); + await this.deleteFile(fileName); throw false; } @@ -322,13 +354,12 @@ export class ApiFileManager { //var fileWriterPromise = toFileEntry ? FileManager.getFileWriter(toFileEntry) : fileStorage.getFileWriter(fileName, mimeType); var fileWriterPromise = toFileEntry ? Promise.resolve(toFileEntry) : fileStorage.getFileWriter(fileName, mimeType); - var processDownloaded = (bytes: any) => { - return Promise.resolve(bytes); - /* if(!processSticker) { - return Promise.resolve(bytes); + var processDownloaded = (bytes: Uint8Array) => { + if(processSticker) { + return appWebpManager.convertToPng(bytes); } - return WebpManager.getPngBlobFromWebp(bytes); */ + return Promise.resolve(bytes); }; fileWriterPromise.then((fileWriter: any) => { @@ -391,7 +422,7 @@ export class ApiFileManager { return Promise.resolve(); } - return processDownloaded(result.bytes).then((processedResult: Uint8Array) => { + return processDownloaded(result.bytes).then((processedResult) => { return FileManager.write(fileWriter, processedResult).then(() => { writeFileDeferred.resolve(); }, errorHandler).then(() => { @@ -438,12 +469,13 @@ export class ApiFileManager { return deferred; } - public deleteFile(fileName: any) { - fileName = typeof(fileName) == 'string' ? fileName : this.getFileName(fileName); + public deleteFile(fileName: string) { this.log('will delete file:', fileName); + delete this.cachedDownloadPromises[fileName]; delete this.cachedDownloads[fileName]; delete this.cachedSavePromises[fileName]; + return this.getFileStorage().deleteFile(fileName); } diff --git a/src/lib/mtproto/networker.ts b/src/lib/mtproto/networker.ts index e9e5310a..65362be6 100644 --- a/src/lib/mtproto/networker.ts +++ b/src/lib/mtproto/networker.ts @@ -1,7 +1,7 @@ import {isObject} from '../bin_utils'; import {convertToUint8Array, bufferConcat, nextRandomInt, bytesToHex, longToBytes, - bytesCmp, uintToInt, bigStringInt} from '../bin_utils'; + bytesCmp, bigStringInt} from '../bin_utils'; import {TLDeserialization, TLSerialization} from './tl_utils'; import CryptoWorker from '../crypto/cryptoworker'; import AppStorage from '../storage'; @@ -296,8 +296,7 @@ class MTPNetworker { var isClean = this.cleanupSent(); //this.log('Check lp', this.longPollPending, tsNow(), this.dcID, isClean, this); if((this.longPollPending && Date.now() < this.longPollPending) || - this.offline || - NetworkerFactory.akStopped) { + this.offline) { //this.log('No lp this time'); return false; } @@ -502,7 +501,7 @@ class MTPNetworker { public performScheduledRequest() { // this.log('scheduled', this.dcID, this.iii) - if(this.offline || NetworkerFactory.akStopped) { + if(this.offline) { this.log('Cancel scheduled'); return false; } @@ -1027,7 +1026,7 @@ class MTPNetworker { public processError(rawError: {error_message: string, error_code: number}) { var matches = (rawError.error_message || '').match(/^([A-Z_0-9]+\b)(: (.+))?/) || []; - rawError.error_code = uintToInt(rawError.error_code); + rawError.error_code = rawError.error_code; return { code: !rawError.error_code || rawError.error_code <= 0 ? 500 : rawError.error_code, diff --git a/src/lib/mtproto/networkerFactory.ts b/src/lib/mtproto/networkerFactory.ts index 04927493..df16d8ab 100644 --- a/src/lib/mtproto/networkerFactory.ts +++ b/src/lib/mtproto/networkerFactory.ts @@ -2,24 +2,6 @@ import { MTPNetworker } from "./networker"; export class NetworkerFactory { public updatesProcessor: (obj: any, bool: boolean) => void = null; - //public offlineInited = false; - public akStopped = false; - - /* public startAll() { - if(this.akStopped) { - this.akStopped = false; - - if(this.updatesProcessor) { - this.updatesProcessor({ - _: 'new_session_created' - }, true); - } - } - } - - public stopAll() { - this.akStopped = true; - } */ public setUpdatesProcessor(callback: (obj: any, bool: boolean) => void) { this.updatesProcessor = callback; diff --git a/src/lib/mtproto/tl_utils.ts b/src/lib/mtproto/tl_utils.ts index b6766cc7..da33e243 100644 --- a/src/lib/mtproto/tl_utils.ts +++ b/src/lib/mtproto/tl_utils.ts @@ -5,7 +5,7 @@ * https://github.com/zhukov/webogram/blob/master/LICENSE */ -import {bigint, intToUint, bigStringInt, bytesToHex, uintToInt, isObject} from '../bin_utils'; +import {bigint, bigStringInt, bytesToHex, isObject} from '../bin_utils'; import Schema from './schema'; /// #if MTPROTO_WORKER @@ -131,8 +131,8 @@ class TLSerialization { } var divRem = bigStringInt(sLong).divideAndRemainder(bigint(0x100000000)); - this.writeInt(intToUint(divRem[1].intValue()), (field || '') + ':long[low]'); - this.writeInt(intToUint(divRem[0].intValue()), (field || '') + ':long[high]'); + this.writeInt(divRem[1].intValue(), (field || '') + ':long[low]'); + this.writeInt(divRem[0].intValue(), (field || '') + ':long[high]'); } public storeDouble(f: any, field?: string) { @@ -250,7 +250,7 @@ class TLSerialization { throw new Error('No method ' + methodName + ' found'); } - this.storeInt(intToUint(methodData.id), methodName + '[id]'); + this.storeInt(methodData.id, methodName + '[id]'); var param, type; var i, condType; @@ -347,7 +347,7 @@ class TLSerialization { } if(!isBare) { - this.writeInt(intToUint(constructorData.id), field + '[' + predicate + '][id]'); + this.writeInt(constructorData.id, field + '[' + predicate + '][id]'); } var param, type: string; @@ -601,7 +601,7 @@ class TLDeserialization { if(type.substr(0, 6) == 'Vector' || type.substr(0, 6) == 'vector') { if(type.charAt(0) == 'V') { var constructor = this.readInt(field + '[id]'); - var constructorCmp = uintToInt(constructor); + var constructorCmp = constructor; if(constructorCmp == gzipPacked) { // Gzip packed var compressed = this.fetchBytes(field + '[packed_string]'); @@ -657,7 +657,7 @@ class TLDeserialization { } } else { var constructor = this.readInt(field + '[id]'); - var constructorCmp = uintToInt(constructor); + var constructorCmp = constructor; if(constructorCmp == gzipPacked) { // Gzip packed var compressed = this.fetchBytes(field + '[packed_string]'); diff --git a/src/lib/utils.js b/src/lib/utils.js index 8bc4def4..6e09542b 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -289,8 +289,10 @@ export function getSelectedText() { export const $rootScope = { $broadcast: (name/* : string */, detail/*? : any */) => { - console.log(dT(), 'Broadcasting ' + name + ' event, with args:', detail); - //console.trace(); + if(name != 'user_update') { + console.log(dT(), 'Broadcasting ' + name + ' event, with args:', detail); + } + let myCustomEvent = new CustomEvent(name, {detail}); document.dispatchEvent(myCustomEvent); }, diff --git a/src/lib/webp.ts b/src/lib/webp.ts index 23794367..2cebb7a0 100644 --- a/src/lib/webp.ts +++ b/src/lib/webp.ts @@ -1,7 +1,6 @@ import {Webp} from "webp-hero/libwebp/dist/webp.js" -import {detectWebpSupport} from "webp-hero/dist/detect-webp-support.js" -const relax = () => new Promise(resolve => requestAnimationFrame(resolve)) +const relax = () => new Promise(resolve => requestAnimationFrame(resolve)); export class WebpMachineError extends Error {} @@ -11,34 +10,39 @@ export class WebpMachineError extends Error {} * - can only decode images one-at-a-time (otherwise will throw busy error) */ export class WebpMachine { - private readonly webp: Webp - private readonly webpSupport: Promise - private busy = false - private cache: {[key: string]: string} = {} - - constructor({ - webp = new Webp(), - webpSupport = detectWebpSupport() - } = {}) { - this.webp = webp; + private readonly webp: Webp; + private busy = false; + + constructor() { + this.webp = new Webp(); this.webp.Module.doNotCaptureKeyboard = true; - this.webpSupport = webpSupport; } /** * Decode raw webp data into a png data url */ - async decode(webpData: Uint8Array): Promise { + decode(webpData: Uint8Array): Promise { if(this.busy) throw new WebpMachineError("cannot decode when already busy"); this.busy = true; try { - await relax(); - const canvas = document.createElement("canvas"); - this.webp.setCanvas(canvas); - this.webp.webpToSdl(webpData, webpData.length); - this.busy = false; - return canvas.toDataURL(); + return relax().then(() => { + const canvas = document.createElement("canvas"); + this.webp.setCanvas(canvas); + this.webp.webpToSdl(webpData, webpData.length); + this.busy = false; + + return new Promise((resolve, reject) => { + canvas.toBlob(blob => { + let reader = new FileReader(); + reader.onload = (event) => { + resolve(new Uint8Array(event.target.result as ArrayBuffer)); + }; + reader.onerror = reject; + reader.readAsArrayBuffer(blob); + }, 'image/png', 1); + }); + }); } catch(error) { this.busy = false; error.name = WebpMachineError.name; diff --git a/src/pages/pageAuthCode.ts b/src/pages/pageAuthCode.ts index 945e083c..b3d62ffd 100644 --- a/src/pages/pageAuthCode.ts +++ b/src/pages/pageAuthCode.ts @@ -28,7 +28,12 @@ let sentTypeElement: HTMLParagraphElement = null; let onFirstMount = (): Promise => { let needFrame = 0, lastLength = 0; - let animation: /* AnimationItem */any = undefined; + + let animation: /* AnimationItem */any; + let idleAnimation: any; + + let mTrackingSvg: SVGSVGElement; + let mIdleSvg: SVGSVGElement; const CODELENGTH = authCode.type.length; @@ -113,6 +118,13 @@ let onFirstMount = (): Promise => { }); } + let cleanup = () => { + setTimeout(() => { + if(animation) animation.destroy(); + if(idleAnimation) idleAnimation.destroy(); + }, 300); + }; + let submitCode = (code: string) => { codeInput.setAttribute('disabled', 'true'); @@ -135,7 +147,7 @@ let onFirstMount = (): Promise => { }); pageIm.mount(); - if(animation) animation.destroy(); + cleanup(); break; case 'auth.authorizationSignUpRequired': console.log('Registration needed!'); @@ -145,7 +157,7 @@ let onFirstMount = (): Promise => { 'phone_code_hash': authCode.phone_code_hash }); - if(animation) animation.destroy(); + cleanup(); break; default: codeInput.innerText = response._; @@ -158,7 +170,7 @@ let onFirstMount = (): Promise => { case 'SESSION_PASSWORD_NEEDED': console.warn('pageAuthCode: SESSION_PASSWORD_NEEDED'); err.handled = true; - if(animation) animation.destroy(); + cleanup(); pagePassword.mount(); break; case 'PHONE_CODE_EMPTY': @@ -196,8 +208,14 @@ let onFirstMount = (): Promise => { if(!animation) return; let frame: number; - if(length) frame = Math.round((length > max ? max : length) * (165 / max) + 11.33); - else frame = 0; + if(length) { + frame = Math.round((length > max ? max : length) * (165 / max) + 11.33); + + mIdleSvg.style.display = 'none'; + mTrackingSvg.style.display = ''; + } else { + frame = 0; + } //animation.playSegments([1, 2]); let direction = needFrame > frame ? -1 : 1; @@ -217,33 +235,68 @@ let onFirstMount = (): Promise => { //animation.goToAndStop(length / max * ); }); + let imageDiv = page.pageEl.querySelector('.auth-image'); return Promise.all([ LottieLoader.loadLottie(), + fetch('assets/img/TwoFactorSetupMonkeyIdle.tgs') + .then(res => res.arrayBuffer()) + .then(data => apiManager.gzipUncompress(data, true)) + .then(str => LottieLoader.loadAnimation({ + container: imageDiv, + renderer: 'svg', + loop: true, + autoplay: true, + animationData: JSON.parse(str), + rendererSettings: { + className: 'monkey-idle' + } + })) + .then(_animation => { + idleAnimation = _animation; + + mIdleSvg = imageDiv.querySelector('.monkey-idle'); + }), + fetch('assets/img/TwoFactorSetupMonkeyTracking.tgs') .then(res => res.arrayBuffer()) .then(data => apiManager.gzipUncompress(data, true)) .then(str => LottieLoader.loadAnimation({ - container: page.pageEl.querySelector('.auth-image'), + container: imageDiv, renderer: 'svg', loop: false, autoplay: false, - animationData: JSON.parse(str) + animationData: JSON.parse(str), + rendererSettings: { + className: 'monkey-tracking' + } })) .then(_animation => { animation = _animation; animation.setSpeed(1); //console.log(animation.getDuration(), animation.getDuration(true)); + mTrackingSvg = imageDiv.querySelector('.monkey-tracking'); + if(!codeInput.value.length) { + mTrackingSvg.style.display = 'none'; + } + animation.addEventListener('enterFrame', (e: any) => { //console.log('enterFrame', e, needFrame); let currentFrame = Math.round(e.currentTime); if((e.direction == 1 && currentFrame >= needFrame) || (e.direction == -1 && currentFrame <= needFrame)) { - animation.setSpeed(1); - animation.pause(); - } + animation.setSpeed(1); + animation.pause(); + } + + if(currentFrame == 0 && needFrame == 0 && mIdleSvg) { + mTrackingSvg.style.display = 'none'; + mIdleSvg.style.display = ''; + idleAnimation.stop(); + idleAnimation.play(); + } }); }) ]); diff --git a/src/pages/pageSignIn.ts b/src/pages/pageSignIn.ts index 2156f42c..24c10649 100644 --- a/src/pages/pageSignIn.ts +++ b/src/pages/pageSignIn.ts @@ -206,6 +206,8 @@ let onFirstMount = () => { putPreloader(this); //this.innerHTML = 'PLEASE WAIT...'; + return; + let phone_number = telEl.value; apiManager.invokeApi('auth.sendCode', { //flags: 0, diff --git a/src/scss/partials/_chat.scss b/src/scss/partials/_chat.scss index 51a5889c..14447dcf 100644 --- a/src/scss/partials/_chat.scss +++ b/src/scss/partials/_chat.scss @@ -205,6 +205,7 @@ $time-background: rgba(0, 0, 0, .35); #attach-file { &.menu-open { color: $color-blue; + background-color: transparent; } .btn-menu { diff --git a/src/scss/partials/_chatBubble.scss b/src/scss/partials/_chatBubble.scss index eca43311..346f27f2 100644 --- a/src/scss/partials/_chatBubble.scss +++ b/src/scss/partials/_chatBubble.scss @@ -355,8 +355,6 @@ position: relative; img, video { - width: auto; - height: auto; max-width: 100%; cursor: pointer; opacity: 1; @@ -394,13 +392,13 @@ width: max-content; } - - img:not(.emoji), video { - /* object-fit: contain; */ - object-fit: cover; - width: 100%; - height: 100%; - } + } + + img:not(.emoji), video { + /* object-fit: contain; */ + object-fit: cover; + width: 100%; + height: 100%; } &.is-album { diff --git a/src/scss/partials/_emojiDropdown.scss b/src/scss/partials/_emojiDropdown.scss index c25f0da4..cd69c73c 100644 --- a/src/scss/partials/_emojiDropdown.scss +++ b/src/scss/partials/_emojiDropdown.scss @@ -68,7 +68,7 @@ .category-items { display: grid; grid-column-gap: 2.44px; - grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; + grid-template-columns: repeat(9, 1fr); font-size: 2.25rem; line-height: 2.25rem; @@ -121,10 +121,9 @@ .category-items { width: 100%; - display: flex; - align-items: center; - justify-content: space-between; - flex-wrap: wrap; + display: grid; + grid-template-columns: repeat(5, 1fr); + grid-column-gap: 1px; > div { width: 80px; diff --git a/src/scss/partials/_rightSIdebar.scss b/src/scss/partials/_rightSIdebar.scss index b76082ff..b813c67e 100644 --- a/src/scss/partials/_rightSIdebar.scss +++ b/src/scss/partials/_rightSIdebar.scss @@ -1,20 +1,13 @@ #column-right { width: 0%; - /* grid-column: 3; */ position: relative; transition: .2s ease-in-out; - .profile-container { - > .scrollable { - min-width: 25vw; - display: flex; - flex-direction: column; - } + .sidebar-content { + min-width: 25vw; @media (min-width: $large-screen) { - > .scrollable { - min-width: calc(#{$large-screen} / 4 - 1px); - } + min-width: calc(#{$large-screen} / 4 - 1px); } } @@ -51,7 +44,7 @@ flex: 1 1 auto; display: flex; flex-direction: column; - height: 100%; + /* height: 100%; */ position: relative; width: 100%; @@ -172,7 +165,7 @@ &-content { //min-height: 100%; min-height: calc(100% - 49px); - position: absolute; // FIX THE SAFARI! + //position: absolute; // FIX THE SAFARI! //position: relative; /* width: 500%; margin-left: -100%; diff --git a/src/scss/partials/_scrollable.scss b/src/scss/partials/_scrollable.scss index 0c005e16..1c46a7e6 100644 --- a/src/scss/partials/_scrollable.scss +++ b/src/scss/partials/_scrollable.scss @@ -34,8 +34,8 @@ div.scrollable::-webkit-scrollbar-thumb { left: 0px; bottom: 0px; right: 0px; - display: flex; - flex-direction: column; + /* display: flex; + flex-direction: column; */ &.scrollable-x { overflow-x: auto; @@ -50,7 +50,7 @@ div.scrollable::-webkit-scrollbar-thumb { -ms-overflow-style: none; } - &-sentinel { + /* &-sentinel { position: relative; left: 0; height: 1px; @@ -58,7 +58,7 @@ div.scrollable::-webkit-scrollbar-thumb { background-color: transparent; width: 1px; min-width: 1px; - } + } */ /* &.scrollable-x ~ .scrollbar-thumb { top: auto; diff --git a/src/scss/partials/popups/_mediaAttacher.scss b/src/scss/partials/popups/_mediaAttacher.scss index 23e67610..ec28ca21 100644 --- a/src/scss/partials/popups/_mediaAttacher.scss +++ b/src/scss/partials/popups/_mediaAttacher.scss @@ -15,7 +15,6 @@ #{$parent}-photo { max-height: 320px; margin: 0 auto; - padding-bottom: 8px; img { object-fit: contain; diff --git a/src/scss/style.scss b/src/scss/style.scss index 77fa41a0..509b7605 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -418,18 +418,11 @@ avatar-element { @keyframes ripple-effect { 0% { - //transform: translate(-50%, -50%) scale(0); transform: scale(0); } - /* 50% { - opacity: 1; - } */ - to { transform: scale(2); - //transform: translate(-50%, -50%) scale(2); - //opacity: 0; } } @@ -1062,30 +1055,6 @@ input:focus, button:focus { } } -/* button { - position: relative; - overflow: hidden; - - &:hover { - &:before { - display: block; - } - } - - &:before { - display: none; - content: " "; - position: absolute; - z-index: 2; - background: #000; - top: 0; - left: 0; - right: 0; - bottom: 0; - opacity: .08; - } -} */ - .btn-primary { background: $color-blue; color: #fff; @@ -1106,9 +1075,8 @@ input:focus, button:focus { svg, use { height: calc(100% - 20px); - right: 12.5px; + right: 15px; left: auto; - margin: 4px 0 auto; } }