From d4e93c819e4460e65f6573e0af9c0a39113950c9 Mon Sep 17 00:00:00 2001 From: morethanwords Date: Thu, 24 Sep 2020 02:37:22 +0300 Subject: [PATCH] Right sidebar changes: Search and forward are tabs now Fix history navigation --- src/components/chat/contextMenu.ts | 4 +- src/components/horizontalMenu.ts | 50 +- .../forward.ts} | 56 +- src/components/sidebarRight/search.ts | 50 ++ src/components/sidebarRight/sharedMedia.ts | 793 +++++++++++++++++ src/components/slider.ts | 20 +- src/index.hbs | 12 +- src/lib/appManagers/appImManager.ts | 49 +- src/lib/appManagers/appMediaViewer.ts | 12 +- src/lib/appManagers/appSidebarLeft.ts | 2 + src/lib/appManagers/appSidebarRight.ts | 836 +----------------- src/lib/utils.ts | 5 +- src/scss/partials/_rightSidebar.scss | 4 + 13 files changed, 986 insertions(+), 907 deletions(-) rename src/components/{appForward.ts => sidebarRight/forward.ts} (60%) create mode 100644 src/components/sidebarRight/search.ts create mode 100644 src/components/sidebarRight/sharedMedia.ts diff --git a/src/components/chat/contextMenu.ts b/src/components/chat/contextMenu.ts index ed12b1a1..0894ef10 100644 --- a/src/components/chat/contextMenu.ts +++ b/src/components/chat/contextMenu.ts @@ -2,8 +2,8 @@ import appChatsManager from "../../lib/appManagers/appChatsManager"; import appImManager from "../../lib/appManagers/appImManager"; import appMessagesManager from "../../lib/appManagers/appMessagesManager"; import appPeersManager from "../../lib/appManagers/appPeersManager"; +import appSidebarRight from "../../lib/appManagers/appSidebarRight"; import { findUpClassName, $rootScope } from "../../lib/utils"; -import appForward from "../appForward"; import { parseMenuButtonsTo, attachContextMenuListener, positionMenu, openBtnMenu } from "../misc"; import { PopupButton, PopupPeer } from "../popup"; @@ -149,7 +149,7 @@ export class ChatContextMenu { }); this.buttons.forward.addEventListener('click', () => { - appForward.init([this.msgID]); + appSidebarRight.forwardTab.open([this.msgID]); }); this.buttons.edit.addEventListener('click', () => { diff --git a/src/components/horizontalMenu.ts b/src/components/horizontalMenu.ts index 8857ea24..2a58ef62 100644 --- a/src/components/horizontalMenu.ts +++ b/src/components/horizontalMenu.ts @@ -1,25 +1,11 @@ import { findUpTag, whichChild } from "../lib/utils"; -function slideNavigation(tabContent: HTMLElement, prevTabContent: HTMLElement, toRight: boolean) { - /* if(toRight) { - //prevTabContent.style.filter = `brightness(80%)`; - prevTabContent.style.transform = `translateX(-25%)`; - tabContent.style.transform = `translateX(20%)`; - } else { - //tabContent.style.filter = `brightness(80%)`; - tabContent.style.transform = `translateX(-25%)`; - prevTabContent.style.transform = `translateX(20%)`; - } */ - const width = prevTabContent.getBoundingClientRect().width; - if(toRight) { - prevTabContent.style.filter = `brightness(80%)`; - prevTabContent.style.transform = `translate3d(${-width * .25}px, 0, 0)`; - tabContent.style.transform = `translate3d(${width}px, 0, 0)`; - } else { - tabContent.style.filter = `brightness(80%)`; - tabContent.style.transform = `translate3d(${-width * .25}px, 0, 0)`; - prevTabContent.style.transform = `translate3d(${width}px, 0, 0)`; - } +function slideNavigation(tabContent: HTMLElement, prevTabContent: HTMLElement, width: number, toRight: boolean) { + const elements = [tabContent, prevTabContent]; + if(toRight) elements.reverse(); + elements[0].style.filter = `brightness(80%)`; + elements[0].style.transform = `translate3d(${-width * .25}px, 0, 0)`; + elements[1].style.transform = `translate3d(${width}px, 0, 0)`; tabContent.classList.add('active'); void tabContent.offsetWidth; // reflow @@ -28,15 +14,11 @@ function slideNavigation(tabContent: HTMLElement, prevTabContent: HTMLElement, t tabContent.style.filter = ''; } -function slideTabs(tabContent: HTMLElement, prevTabContent: HTMLElement, toRight: boolean) { - const width = prevTabContent.getBoundingClientRect().width; - if(toRight) { - tabContent.style.transform = `translate3d(${width}px, 0, 0)`; - prevTabContent.style.transform = `translate3d(${-width}px, 0, 0)`; - } else { - tabContent.style.transform = `translate3d(${-width}px, 0, 0)`; - prevTabContent.style.transform = `translate3d(${width}px, 0, 0)`; - } +function slideTabs(tabContent: HTMLElement, prevTabContent: HTMLElement, width: number, toRight: boolean) { + const elements = [tabContent, prevTabContent]; + if(toRight) elements.reverse(); + elements[0].style.transform = `translate3d(${-width}px, 0, 0)`; + elements[1].style.transform = `translate3d(${width}px, 0, 0)`; tabContent.classList.add('active'); void tabContent.offsetWidth; // reflow @@ -72,11 +54,15 @@ export function horizontalMenu(tabs: HTMLElement, content: HTMLElement, onClick? } const toRight = prevId < id; - if(prevId != -1) { + if(!tabContent) { + //prevTabContent.classList.remove('active'); + } else if(prevId != -1) { + const width = prevTabContent.getBoundingClientRect().width; + if(tabs || content.dataset.slider == 'tabs') { - slideTabs(tabContent, prevTabContent, toRight); + slideTabs(tabContent, prevTabContent, width, toRight); } else { - slideNavigation(tabContent, prevTabContent, toRight); + slideNavigation(tabContent, prevTabContent, width, toRight); } } else { tabContent.classList.add('active'); diff --git a/src/components/appForward.ts b/src/components/sidebarRight/forward.ts similarity index 60% rename from src/components/appForward.ts rename to src/components/sidebarRight/forward.ts index 04c66ea7..7c9a3919 100644 --- a/src/components/appForward.ts +++ b/src/components/sidebarRight/forward.ts @@ -1,20 +1,32 @@ -import appSidebarRight from "../lib/appManagers/appSidebarRight"; -import appMessagesManager from "../lib/appManagers/appMessagesManager"; -import { putPreloader } from "./misc"; -import { AppSelectPeers } from "./appSelectPeers"; +import appSidebarRight, { AppSidebarRight } from "../../lib/appManagers/appSidebarRight"; +import appMessagesManager from "../../lib/appManagers/appMessagesManager"; +import { putPreloader } from "../misc"; +import { AppSelectPeers } from "../appSelectPeers"; +import { SliderTab } from "../slider"; -class AppForward { - public container = document.getElementById('forward-container') as HTMLDivElement; - private closeBtn = this.container.querySelector('.sidebar-close-button') as HTMLButtonElement; - private sendBtn = this.container.querySelector('.btn-circle') as HTMLButtonElement; +export default class AppForwardTab implements SliderTab { + public container: HTMLElement; + public closeBtn: HTMLElement; + private sendBtn: HTMLButtonElement; private selector: AppSelectPeers; private msgIDs: number[] = []; - private sidebarWasActive: boolean; + onCloseAfterTimeout() { + this.cleanup(); + } + + public cleanup() { + if(this.selector) { + this.selector.container.remove(); + this.selector = null; + } + } - constructor() { - this.closeBtn.addEventListener('click', this.close.bind(this)); + public init() { + this.container = document.getElementById('forward-container') as HTMLDivElement; + this.closeBtn = this.container.querySelector('.sidebar-close-button') as HTMLButtonElement; + this.sendBtn = this.container.querySelector('.btn-circle') as HTMLButtonElement; this.sendBtn.addEventListener('click', () => { let peerIDs = this.selector.getSelected(); @@ -44,25 +56,15 @@ class AppForward { }); } - public close() { - (this.sidebarWasActive ? Promise.resolve() : appSidebarRight.toggleSidebar(false)).then(() => { - this.cleanup(); - this.container.classList.remove('active'); - }); - } - - public cleanup() { - if(this.selector) { - this.selector.container.remove(); - this.selector = null; + public open(ids: number[]) { + if(this.init) { + this.init(); + this.init = null; } - } - public init(ids: number[]) { this.cleanup(); this.msgIDs = ids; - this.container.classList.add('active'); this.sendBtn.innerHTML = ''; this.sendBtn.classList.add('tgico-send'); this.sendBtn.disabled = false; @@ -75,10 +77,8 @@ class AppForward { } }, ['dialogs', 'contacts'], () => { //console.log('forward rendered:', this.container.querySelector('.selector ul').childElementCount); - this.sidebarWasActive = appSidebarRight.sidebarEl.classList.contains('active'); + appSidebarRight.selectTab(AppSidebarRight.SLIDERITEMSIDS.forward); appSidebarRight.toggleSidebar(true); }); } } - -export default new AppForward(); \ No newline at end of file diff --git a/src/components/sidebarRight/search.ts b/src/components/sidebarRight/search.ts new file mode 100644 index 00000000..a87ec436 --- /dev/null +++ b/src/components/sidebarRight/search.ts @@ -0,0 +1,50 @@ +import appSidebarRight, { AppSidebarRight } from "../../lib/appManagers/appSidebarRight"; +import AppSearch, { SearchGroup } from "../appSearch"; +import SearchInput from "../searchInput"; +import { SliderTab } from "../slider"; + +export default class AppPrivateSearchTab implements SliderTab { + public container: HTMLElement; + public closeBtn: HTMLElement; + + private searchInput: SearchInput; + private appSearch: AppSearch; + + private peerID = 0; + + onOpenAfterTimeout() { + this.appSearch.beginSearch(this.peerID); + } + + onCloseAfterTimeout() { + this.peerID = 0; + this.appSearch.reset(); + } + + public init() { + this.container = document.getElementById('search-private-container'); + this.closeBtn = this.container.querySelector('.sidebar-close-button'); + this.searchInput = new SearchInput('Search'); + this.closeBtn.parentElement.append(this.searchInput.container); + this.appSearch = new AppSearch(this.container.querySelector('.chats-container'), this.searchInput, { + messages: new SearchGroup('Private Search', 'messages') + }); + } + + open(peerID: number) { + if(this.init) { + this.init(); + this.init = null; + } + + if(this.peerID != 0) { + this.appSearch.beginSearch(this.peerID); + return; + } + + this.peerID = peerID; + + appSidebarRight.selectTab(AppSidebarRight.SLIDERITEMSIDS.search); + appSidebarRight.toggleSidebar(true); + } +} \ No newline at end of file diff --git a/src/components/sidebarRight/sharedMedia.ts b/src/components/sidebarRight/sharedMedia.ts new file mode 100644 index 00000000..75ca3c89 --- /dev/null +++ b/src/components/sidebarRight/sharedMedia.ts @@ -0,0 +1,793 @@ +import appImManager from "../../lib/appManagers/appImManager"; +import appMediaViewer from "../../lib/appManagers/appMediaViewer"; +import appMessagesManager from "../../lib/appManagers/appMessagesManager"; +import appPeersManager from "../../lib/appManagers/appPeersManager"; +import appPhotosManager from "../../lib/appManagers/appPhotosManager"; +import appProfileManager from "../../lib/appManagers/appProfileManager"; +import appUsersManager from "../../lib/appManagers/appUsersManager"; +import { logger, LogLevels } from "../../lib/logger"; +import { RichTextProcessor } from "../../lib/richtextprocessor"; +import { $rootScope } from "../../lib/utils"; +import AvatarElement from "../avatar"; +import { horizontalMenu } from "../horizontalMenu"; +import LazyLoadQueue from "../lazyLoadQueue"; +import { renderImageFromUrl, putPreloader } from "../misc"; +import Scrollable from "../scrollable_new"; +import { SliderTab } from "../slider"; +import { wrapDocument, wrapAudio } from "../wrappers"; + +const testScroll = false; + +let setText = (text: string, el: HTMLDivElement) => { + window.requestAnimationFrame(() => { + if(el.childElementCount > 1) { + el.firstElementChild.remove(); + } + + let p = document.createElement('p'); + p.innerHTML = text; + el.prepend(p); + + el.style.display = ''; + }); +}; + +type ContentType = 'contentMembers' | 'contentMedia' | 'contentDocuments' | 'contentLinks' | 'contentAudio'; +type SharedMediaType = 'inputMessagesFilterContacts' | 'inputMessagesFilterPhotoVideo' | 'inputMessagesFilterDocument' | 'inputMessagesFilterUrl' | 'inputMessagesFilterMusic'; + +const contentToSharedMap: {[contentType in ContentType]: SharedMediaType} = { + contentMembers: 'inputMessagesFilterContacts', + contentMedia: 'inputMessagesFilterPhotoVideo', + contentDocuments: 'inputMessagesFilterDocument', + contentLinks: 'inputMessagesFilterUrl', + contentAudio: 'inputMessagesFilterMusic' +}; + +// TODO: отправленное сообщение с картинкой, или же новое полученное апдейтом сообщение не отобразится в медии +// TODO: по-хорошему, нужно просто сделать апдейты для всего сайдбара + +export default class AppSharedMediaTab implements SliderTab { + public container: HTMLElement; + public closeBtn: HTMLElement; + + private peerID = 0; + + public profileContentEl: HTMLDivElement; + public contentContainer: HTMLDivElement; + public profileElements: { + avatar: AvatarElement, + name: HTMLDivElement, + subtitle: HTMLDivElement, + bio: HTMLDivElement, + username: HTMLDivElement, + phone: HTMLDivElement, + notificationsRow: HTMLDivElement, + notificationsCheckbox: HTMLInputElement, + notificationsStatus: HTMLParagraphElement + } = {} as any; + public sharedMedia: { + [t in ContentType]: HTMLDivElement + } = {} as any; + + private loadSidebarMediaPromises: {[type: string]: Promise} = {}; + private loadedAllMedia: {[type: string]: boolean} = {}; + + public sharedMediaTypes: SharedMediaType[] = [ + //'members', + 'inputMessagesFilterContacts', + 'inputMessagesFilterPhotoVideo', + 'inputMessagesFilterDocument', + 'inputMessagesFilterUrl', + 'inputMessagesFilterMusic' + ]; + public sharedMediaType: SharedMediaType = 'inputMessagesFilterPhotoVideo'; + private sharedMediaSelected: HTMLDivElement = null; + + private lazyLoadQueue = new LazyLoadQueue(); + + public historiesStorage: { + [peerID: number]: Partial<{ + [type in SharedMediaType]: number[] + }> + } = {}; + public usedFromHistory: Partial<{ + [type in SharedMediaType]: number + }> = {}; + + public scroll: Scrollable = null; + + private profileTabs: HTMLUListElement; + private prevTabID = -1; + + private mediaDivsByIDs: { + [mid: number]: HTMLDivElement + } = {}; + + public urlsToRevoke: string[] = []; + + private loadMutex: Promise = Promise.resolve(); + + private log = logger('SM', LogLevels.error); + + public init() { + this.container = document.getElementById('shared-media-container'); + this.closeBtn = this.container.querySelector('.sidebar-close-button'); + + this.profileContentEl = this.container.querySelector('.profile-content'); + this.contentContainer = this.container.querySelector('.content-container'); + this.profileElements = { + avatar: this.profileContentEl.querySelector('.profile-avatar'), + name: this.profileContentEl.querySelector('.profile-name'), + subtitle: this.profileContentEl.querySelector('.profile-subtitle'), + bio: this.profileContentEl.querySelector('.profile-row-bio'), + username: this.profileContentEl.querySelector('.profile-row-username'), + phone: this.profileContentEl.querySelector('.profile-row-phone'), + notificationsRow: this.profileContentEl.querySelector('.profile-row-notifications'), + notificationsCheckbox: this.profileContentEl.querySelector('#profile-notifications'), + notificationsStatus: this.profileContentEl.querySelector('.profile-row-notifications > p') + }; + + this.sharedMedia = { + contentMembers: this.profileContentEl.querySelector('#content-members'), + contentMedia: this.profileContentEl.querySelector('#content-media'), + contentDocuments: this.profileContentEl.querySelector('#content-docs'), + contentLinks: this.profileContentEl.querySelector('#content-links'), + contentAudio: this.profileContentEl.querySelector('#content-audio'), + }; + + let container = this.profileContentEl.querySelector('.content-container .tabs-container') as HTMLDivElement; + this.profileTabs = this.profileContentEl.querySelector('.profile-tabs'); + + this.scroll = new Scrollable(this.container, '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); + + horizontalMenu(this.profileTabs, container, (id, tabContent) => { + if(this.prevTabID == id) return; + + if(this.prevTabID != -1) { + this.onTransitionStart(); + } + + this.sharedMediaType = this.sharedMediaTypes[id]; + this.sharedMediaSelected = tabContent.firstElementChild as HTMLDivElement; + + if(this.prevTabID != -1 && this.profileTabs.offsetTop) { + this.scroll.scrollTop -= this.profileTabs.offsetTop; + } + + /* this.log('setVirtualContainer', id, this.sharedMediaSelected, this.sharedMediaSelected.childElementCount); + this.scroll.setVirtualContainer(this.sharedMediaSelected); */ + + if(this.prevTabID != -1 && !this.sharedMediaSelected.childElementCount) { // quick brown fix + //this.contentContainer.classList.remove('loaded'); + this.loadSidebarMedia(true); + } + + this.prevTabID = id; + }, () => { + this.scroll.onScroll(); + this.onTransitionEnd(); + }); + + this.sharedMedia.contentMedia.addEventListener('click', (e) => { + const target = e.target as HTMLDivElement; + + const messageID = +target.dataset.mid; + if(!messageID) { + this.log.warn('no messageID by click on target:', target); + return; + } + + const message = appMessagesManager.getMessage(messageID); + + const ids = Object.keys(this.mediaDivsByIDs).map(k => +k).sort((a, b) => a - b); + const idx = ids.findIndex(i => i == messageID); + + const targets = ids.map(id => { + const element = this.mediaDivsByIDs[id] as HTMLElement; + //element = element.querySelector('img') || element; + return {element, mid: id}; + }); + + appMediaViewer.openMedia(message, target, false, this.container, targets.slice(idx + 1).reverse(), targets.slice(0, idx).reverse(), true); + }); + + this.profileElements.notificationsCheckbox.addEventListener('change', () => { + //let checked = this.profileElements.notificationsCheckbox.checked; + appImManager.mutePeer(this.peerID); + }); + + /* this.closeBtn.addEventListener('click', () => { + this.toggleSidebar(false); + }); */ + } + + private onTransitionStart = () => { + // Jolly Cobra's // Workaround for scrollable content flickering during animation. + const container = this.scroll.container; + if(container.style.overflowY !== 'hidden') { + const scrollBarWidth = container.offsetWidth - container.clientWidth; + container.style.overflowY = 'hidden'; + container.style.paddingRight = `${scrollBarWidth}px`; + } + }; + + private onTransitionEnd = () => { + // Jolly Cobra's // Workaround for scrollable content flickering during animation. + const container = this.scroll.container; + container.style.overflowY = ''; + container.style.paddingRight = '0'; + }; + + public filterMessagesByType(ids: number[], type: string) { + let messages: any[] = []; + for(let mid of ids) { + let message = appMessagesManager.getMessage(mid); + if(message.media) messages.push(message); + } + + let filtered: any[] = []; + + switch(type) { + case 'inputMessagesFilterPhotoVideo': { + for(let message of messages) { + let media = message.media.photo || message.media.document || (message.media.webpage && message.media.webpage.document); + if(!media) { + //this.log('no media!', message); + continue; + } + + if(media._ == 'document' && media.type != 'video'/* && media.type != 'gif' */) { + //this.log('broken video', media); + continue; + } + + filtered.push(message); + } + + break; + } + + case 'inputMessagesFilterDocument': { + for(let message of messages) { + if(!message.media.document || message.media.document.type == 'voice' || message.media.document.type == 'audio') { + continue; + } + + let doc = message.media.document; + if(doc.attributes) { + if(doc.attributes.find((a: any) => a._ == "documentAttributeSticker")) { + continue; + } + } + + filtered.push(message); + } + break; + } + + case 'inputMessagesFilterUrl': { + for(let message of messages) { + if(!message.media.webpage || message.media.webpage._ == 'webPageEmpty') { + continue; + } + + filtered.push(message); + } + + break; + } + + case 'inputMessagesFilterMusic': { + for(let message of messages) { + if(!message.media.document || message.media.document.type != 'audio') { + continue; + } + + filtered.push(message); + } + + break; + } + + default: + break; + } + + return filtered; + } + + public async performSearchResult(messages: any[], type: SharedMediaType) { + const peerID = this.peerID; + const elemsToAppend: HTMLElement[] = []; + const promises: Promise[] = []; + let sharedMediaDiv: HTMLDivElement; + + /* for(let contentType in contentToSharedMap) { + if(contentToSharedMap[contentType as ContentType] == type) { + sharedMediaDiv = this.sharedMedia[contentType as ContentType]; + } + } */ + + // https://core.telegram.org/type/MessagesFilter + switch(type) { + case 'inputMessagesFilterPhotoVideo': { + sharedMediaDiv = this.sharedMedia.contentMedia; + + for(const message of messages) { + const media = message.media.photo || message.media.document || (message.media.webpage && message.media.webpage.document); + + const div = document.createElement('div'); + div.classList.add('media-item'); + //console.log(message, photo); + + const isPhoto = media._ == 'photo'; + + const photo = isPhoto ? appPhotosManager.getPhoto(media.id) : null; + let isDownloaded: boolean; + if(photo) { + isDownloaded = photo.downloaded > 0; + } else { + const cachedThumb = appPhotosManager.getDocumentCachedThumb(media.id); + isDownloaded = cachedThumb?.downloaded > 0; + } + + //this.log('inputMessagesFilterPhotoVideo', message, media); + + if(!isPhoto) { + const span = document.createElement('span'); + span.classList.add('video-time'); + div.append(span); + + if(media.type != 'gif') { + span.innerText = (media.duration + '').toHHMMSS(false); + + /* const spanPlay = document.createElement('span'); + spanPlay.classList.add('video-play', 'tgico-largeplay', 'btn-circle', 'position-center'); + div.append(spanPlay); */ + } else { + span.innerText = 'GIF'; + } + } + + const load = () => appPhotosManager.preloadPhoto(isPhoto ? media.id : media, appPhotosManager.choosePhotoSize(media, 200, 200)) + .then(() => { + if($rootScope.selectedPeerID != peerID) { + this.log.warn('peer changed'); + return; + } + + const url = (photo && photo.url) || appPhotosManager.getDocumentCachedThumb(media.id).url; + if(url) { + //if(needBlur) return; + + const needBlurCallback = needBlur ? () => { + //void img.offsetLeft; // reflow + img.style.opacity = ''; + + if(thumb) { + window.setTimeout(() => { + thumb.remove(); + }, 200); + } + } : undefined; + renderImageFromUrl(img, url, needBlurCallback); + } + }); + + let thumb: HTMLImageElement; + const sizes = media.sizes || media.thumbs; + + const willHaveThumb = !isDownloaded && sizes && sizes[0].bytes; + if(willHaveThumb) { + thumb = new Image(); + thumb.classList.add('media-image', 'thumbnail'); + thumb.dataset.mid = '' + message.mid; + appPhotosManager.setAttachmentPreview(sizes[0].bytes, thumb, false, false); + div.append(thumb); + } + + const needBlur = !isDownloaded || !willHaveThumb; + const img = new Image(); + img.dataset.mid = '' + message.mid; + img.classList.add('media-image'); + if(needBlur) img.style.opacity = '0'; + div.append(img); + + if(isDownloaded || willHaveThumb) { + const promise = new Promise((resolve, reject) => { + (thumb || img).addEventListener('load', () => { + clearTimeout(timeout); + resolve(); + }); + + const timeout = setTimeout(() => { + this.log('didn\'t load', thumb, media, isDownloaded, sizes); + reject(); + }, 1e3); + }); + + promises.push(promise); + } + + if(sizes?.length) { + if(isDownloaded) load(); + else this.lazyLoadQueue.push({div, load}); + } + + elemsToAppend.push(div); + this.mediaDivsByIDs[message.mid] = div; + } + + break; + } + + case 'inputMessagesFilterDocument': { + sharedMediaDiv = this.sharedMedia.contentDocuments; + + for(let message of messages) { + let div = wrapDocument(message.media.document, true, false, message.mid); + elemsToAppend.push(div); + } + break; + } + + case 'inputMessagesFilterUrl': { + sharedMediaDiv = this.sharedMedia.contentLinks; + + for(let message of messages) { + let webpage = message.media.webpage; + let div = document.createElement('div'); + + let previewDiv = document.createElement('div'); + previewDiv.classList.add('preview'); + + //this.log('wrapping webpage', webpage); + + previewDiv.innerText = (webpage.title || webpage.description || webpage.url || webpage.display_url).slice(0, 1); + previewDiv.classList.add('empty'); + if(webpage.photo) { + let load = () => appPhotosManager.preloadPhoto(webpage.photo.id, appPhotosManager.choosePhotoSize(webpage.photo, 60, 60)) + .then(() => { + if($rootScope.selectedPeerID != peerID) { + this.log.warn('peer changed'); + return; + } + + previewDiv.classList.remove('empty'); + + previewDiv.innerText = ''; + renderImageFromUrl(previewDiv, webpage.photo.url); + }); + + this.lazyLoadQueue.push({div: previewDiv, load}); + } + + let title = webpage.rTitle || ''; + let subtitle = webpage.rDescription || ''; + let url = RichTextProcessor.wrapRichText(webpage.url || ''); + + if(!title) { + //title = new URL(webpage.url).hostname; + title = webpage.display_url.split('/', 1)[0]; + } + + div.append(previewDiv); + div.insertAdjacentHTML('beforeend', ` +
${title} +
${subtitle}
+
${url}
+ `); + + if(div.innerText.trim().length) { + elemsToAppend.push(div); + } + + } + + break; + } + + case 'inputMessagesFilterMusic': { + sharedMediaDiv = this.sharedMedia.contentAudio; + + for(let message of messages) { + let div = wrapAudio(message.media.document, true, message.mid); + elemsToAppend.push(div); + } + break; + } + + default: + console.warn('death is my friend', messages); + break; + } + + if(this.loadMutex) { + promises.push(this.loadMutex); + } + + if(promises.length) { + await Promise.all(promises); + if(this.peerID != peerID) { + this.log.warn('peer changed'); + return; + } + } + + if(elemsToAppend.length) { + sharedMediaDiv.append(...elemsToAppend); + } + + if(sharedMediaDiv) { + const parent = sharedMediaDiv.parentElement; + Array.from(parent.children).slice(1).forEach(child => { + child.remove(); + }); + + //this.contentContainer.classList.add('loaded'); + + if(!messages.length && !sharedMediaDiv.childElementCount) { + const div = document.createElement('div'); + div.innerText = 'Nothing interesting here yet...'; + div.classList.add('position-center', 'text-center', 'content-empty', 'no-select'); + + parent.append(div); + } + } + } + + public loadSidebarMedia(single = false) { + if(testScroll/* || 1 == 1 */) { + return; + } + + this.log('loadSidebarMedia', single, this.peerID, this.loadSidebarMediaPromises); + + const peerID = this.peerID; + + let typesToLoad = single ? [this.sharedMediaType] : this.sharedMediaTypes; + typesToLoad = typesToLoad.filter(type => !this.loadedAllMedia[type]); + if(!typesToLoad.length) return; + + const loadCount = (appPhotosManager.windowH / 130 | 0) * 3; // that's good for all types + + const historyStorage = this.historiesStorage[peerID] ?? (this.historiesStorage[peerID] = {}); + + const promises = typesToLoad.map(type => { + if(this.loadSidebarMediaPromises[type]) return this.loadSidebarMediaPromises[type]; + + const history = historyStorage[type] ?? (historyStorage[type] = []); + + // render from cache + if(history.length && this.usedFromHistory[type] < history.length) { + let messages: any[] = []; + let used = this.usedFromHistory[type]; + + do { + let ids = history.slice(used, used + loadCount); + this.log('loadSidebarMedia: will render from cache', used, history, ids, loadCount); + used += ids.length; + + messages.push(...this.filterMessagesByType(ids, type)); + } while(messages.length < loadCount && used < history.length); + + // если перебор + if(messages.length > loadCount) { + let diff = messages.length - loadCount; + messages = messages.slice(0, messages.length - diff); + used -= diff; + } + + this.usedFromHistory[type] = used; + //if(messages.length) { + return this.performSearchResult(messages, type); + //} + + return Promise.resolve(); + } + + // заливать новую картинку сюда только после полной отправки! + let maxID = history[history.length - 1] || 0; + + let ids = !maxID && appMessagesManager.historiesStorage[peerID] + ? appMessagesManager.historiesStorage[peerID].history.slice() : []; + + maxID = !maxID && ids.length ? ids[ids.length - 1] : maxID; + this.log('loadSidebarMedia: search house of glass pre', type, ids, maxID); + + //let loadCount = history.length ? 50 : 15; + return this.loadSidebarMediaPromises[type] = appMessagesManager.getSearch(peerID, '', {_: type}, maxID, loadCount) + .then(value => { + ids = ids.concat(value.history); + history.push(...ids); + + this.log('loadSidebarMedia: search house of glass', type, value, ids); + + if($rootScope.selectedPeerID != peerID) { + this.log.warn('peer changed'); + return; + } + + if(value.history.length < loadCount) { + this.loadedAllMedia[type] = true; + } + + this.usedFromHistory[type] = history.length; + + //if(ids.length) { + return this.performSearchResult(this.filterMessagesByType(ids, type), type); + //} + }, (err) => { + this.log.error('load error:', err); + }).then(() => { + this.loadSidebarMediaPromises[type] = null; + }); + }); + + return Promise.all(promises); + } + + public cleanup() { + this.loadSidebarMediaPromises = {}; + this.loadedAllMedia = {}; + + this.prevTabID = -1; + this.mediaDivsByIDs = {}; + this.lazyLoadQueue.clear(); + + this.sharedMediaTypes.forEach(type => { + this.usedFromHistory[type] = 0; + }); + + this.sharedMediaType = 'inputMessagesFilterPhotoVideo'; + } + + public cleanupHTML() { + //this.contentContainer.classList.remove('loaded'); + + //this.profileContentEl.parentElement.scrollTop = 0; + this.profileElements.bio.style.display = 'none'; + this.profileElements.phone.style.display = 'none'; + this.profileElements.username.style.display = 'none'; + this.profileElements.notificationsRow.style.display = ''; + this.profileElements.notificationsCheckbox.checked = true; + this.profileElements.notificationsStatus.innerText = 'Enabled'; + + if(this.urlsToRevoke.length) { + this.urlsToRevoke.forEach(url => { + URL.revokeObjectURL(url); + }); + this.urlsToRevoke.length = 0; + } + + (Object.keys(this.sharedMedia) as ContentType[]).forEach(key => { + this.sharedMedia[key].innerHTML = ''; + + const inputFilter = contentToSharedMap[key]; + if(!this.historiesStorage[this.peerID] || !this.historiesStorage[this.peerID][inputFilter]) { + const parent = this.sharedMedia[key].parentElement; + if(!testScroll) { + if(!parent.querySelector('.preloader')) { + putPreloader(parent, true); + } + } + + const empty = parent.querySelector('.content-empty'); + if(empty) { + empty.remove(); + } + } + }); + + if(testScroll) { + for(let i = 0; i < 1500; ++i) { + let div = document.createElement('div'); + div.insertAdjacentHTML('beforeend', ``); + div.classList.add('media-item'); + div.dataset.id = '' + (i / 3 | 0); + //div.innerText = '' + (i / 3 | 0); + this.sharedMedia.contentMedia.append(div); + } + } + + (this.profileTabs.firstElementChild.children[1] as HTMLLIElement).click(); // set media + } + + public setLoadMutex(promise: Promise) { + this.loadMutex = promise; + } + + public setPeer(peerID: number) { + if(this.init) { + this.init(); + this.init = null; + } + + this.peerID = peerID; + this.cleanup(); + } + + public fillProfileElements() { + let peerID = this.peerID = $rootScope.selectedPeerID; + + this.cleanupHTML(); + + this.profileElements.avatar.setAttribute('peer', '' + peerID); + + // username + if(peerID != $rootScope.myID) { + let username = appPeersManager.getPeerUsername(peerID); + if(username) { + setText(appPeersManager.getPeerUsername(peerID), this.profileElements.username); + } + + let dialog = appMessagesManager.getDialogByPeerID(peerID)[0]; + if(dialog) { + let muted = false; + if(dialog.notify_settings && dialog.notify_settings.mute_until) { + muted = new Date(dialog.notify_settings.mute_until * 1000) > new Date(); + } + + appImManager.setMutedState(muted); + } + } else { + window.requestAnimationFrame(() => { + this.profileElements.notificationsRow.style.display = 'none'; + }); + } + + //let membersLi = this.profileTabs.firstElementChild.children[0] as HTMLLIElement; + if(peerID > 0) { + //membersLi.style.display = 'none'; + + let user = appUsersManager.getUser(peerID); + if(user.phone && peerID != $rootScope.myID) { + setText(user.rPhone, this.profileElements.phone); + } + + appProfileManager.getProfile(peerID, true).then(userFull => { + if(this.peerID != peerID) { + this.log.warn('peer changed'); + return; + } + + if(userFull.rAbout && peerID != $rootScope.myID) { + setText(userFull.rAbout, this.profileElements.bio); + } + + //this.log('userFull', userFull); + }); + } else { + //membersLi.style.display = appPeersManager.isBroadcast(peerID) ? 'none' : ''; + let chat = appPeersManager.getPeer(peerID); + + appProfileManager.getChatFull(chat.id).then((chatFull: any) => { + if(this.peerID != peerID) { + this.log.warn('peer changed'); + return; + } + + //this.log('chatInfo res 2:', chatFull); + + if(chatFull.about) { + setText(RichTextProcessor.wrapRichText(chatFull.about), this.profileElements.bio); + } + }); + } + } + + /* onOpen() { + this.scroll.onScroll(); + } */ + + onOpenAfterTimeout() { + this.scroll.onScroll(); + } +} \ No newline at end of file diff --git a/src/components/slider.ts b/src/components/slider.ts index b4de565a..b421fc2f 100644 --- a/src/components/slider.ts +++ b/src/components/slider.ts @@ -7,21 +7,23 @@ export interface SliderTab { onCloseAfterTimeout?: () => void } -const TRANSITIONTIME = 250; +const TRANSITION_TIME = 250; export default class SidebarSlider { protected _selectTab: (id: number) => void; public historyTabIDs: number[] = []; - constructor(public sidebarEl: HTMLElement, public tabs: {[id: number]: SliderTab}) { - this._selectTab = horizontalMenu(null, this.sidebarEl.querySelector('.sidebar-slider') as HTMLDivElement, null, null, TRANSITIONTIME); - this._selectTab(0); + constructor(public sidebarEl: HTMLElement, public tabs: {[id: number]: SliderTab}, canHideFirst = false) { + this._selectTab = horizontalMenu(null, this.sidebarEl.querySelector('.sidebar-slider') as HTMLDivElement, null, null, TRANSITION_TIME); + if(!canHideFirst) { + this._selectTab(0); + } let onCloseBtnClick = () => { //console.log('sidebar-close-button click:', this.historyTabIDs); let closingID = this.historyTabIDs.pop(); // pop current this.onCloseTab(closingID); - this._selectTab(this.historyTabIDs[this.historyTabIDs.length - 1] || 0); + this._selectTab(this.historyTabIDs[this.historyTabIDs.length - 1] ?? (canHideFirst ? -1 : 0)); }; Array.from(this.sidebarEl.querySelectorAll('.sidebar-close-button') as any as HTMLElement[]).forEach(el => { el.addEventListener('click', onCloseBtnClick); @@ -30,7 +32,7 @@ export default class SidebarSlider { public selectTab(id: number) { if(this.historyTabIDs[this.historyTabIDs.length - 1] == id) { - return; + return false; } const tab = this.tabs[id]; @@ -42,13 +44,13 @@ export default class SidebarSlider { if(tab.onOpenAfterTimeout) { setTimeout(() => { tab.onOpenAfterTimeout(); - }, TRANSITIONTIME); + }, TRANSITION_TIME); } } - this.historyTabIDs.push(id); this._selectTab(id); + return true; } public removeTabFromHistory(id: number) { @@ -66,7 +68,7 @@ export default class SidebarSlider { if(tab.onCloseAfterTimeout) { setTimeout(() => { tab.onCloseAfterTimeout(); - }, TRANSITIONTIME); + }, TRANSITION_TIME); } } } diff --git a/src/index.hbs b/src/index.hbs index a333c4f4..b9f71666 100644 --- a/src/index.hbs +++ b/src/index.hbs @@ -588,14 +588,14 @@