From 669cd4397878559c054c337fa2fe75c4b0a54789 Mon Sep 17 00:00:00 2001 From: Eduard Kuzmenko Date: Tue, 16 Feb 2021 19:36:26 +0400 Subject: [PATCH] 111 --- src/components/appNavigationController.ts | 62 +++++++++++++ src/components/chat/topbar.ts | 6 +- src/components/sidebarLeft/index.ts | 5 +- src/components/sidebarRight/index.ts | 53 ++++-------- src/components/slider.ts | 53 +++++++++--- src/helpers/dom.ts | 9 ++ src/helpers/schedulers.ts | 8 ++ src/lib/appManagers/appImManager.ts | 101 ++++++++++++++-------- src/scss/partials/_leftSidebar.scss | 1 + src/scss/partials/_rightSidebar.scss | 2 +- 10 files changed, 213 insertions(+), 87 deletions(-) create mode 100644 src/components/appNavigationController.ts diff --git a/src/components/appNavigationController.ts b/src/components/appNavigationController.ts new file mode 100644 index 00000000..4611e207 --- /dev/null +++ b/src/components/appNavigationController.ts @@ -0,0 +1,62 @@ +import { MOUNT_CLASS_TO } from "../config/debug"; +import { isSafari } from "../helpers/userAgent"; + +export type NavigationItem = { + type: 'left' | 'right' | 'im' | 'chat' | 'popup' | 'media' | 'menu' | 'esg', + onPop: (canAnimate: boolean) => boolean | void +}; + +export class AppNavigationController { + private navigations: Array = []; + private id = Date.now(); + + constructor() { + window.addEventListener('popstate', (e) => { + console.log('popstate', e); + + const id: number = e.state; + if(id !== this.id) { + this.pushState(); + return; + } + + const item = this.navigations.pop(); + if(!item) { + this.pushState(); + return; + } + + const good = item.onPop(isSafari ? false : undefined); + console.log('[NC]: popstate, navigation:', item, this.navigations); + if(good === false) { + this.pushItem(item); + } + + //this.pushState(); // * prevent adding forward arrow + }); + + this.pushState(); // * push init state + } + + public back() { + history.back(); + } + + public pushItem(item: NavigationItem) { + this.navigations.push(item); + console.log('[NC]: pushstate', item, this.navigations); + this.pushState(); + } + + private pushState() { + history.pushState(this.id, ''); + } + + public replaceState() { + history.replaceState(this.id, ''); + } +} + +const appNavigationController = new AppNavigationController(); +MOUNT_CLASS_TO && (MOUNT_CLASS_TO.appNavigationController = appNavigationController); +export default appNavigationController; diff --git a/src/components/chat/topbar.ts b/src/components/chat/topbar.ts index 38f594f9..ce6146e7 100644 --- a/src/components/chat/topbar.ts +++ b/src/components/chat/topbar.ts @@ -18,6 +18,7 @@ import { ButtonMenuItemOptions } from "../buttonMenu"; import ListenerSetter from "../../helpers/listenerSetter"; import appStateManager from "../../lib/appManagers/appStateManager"; import PopupDeleteDialog from "../popups/deleteDialog"; +import appNavigationController from "../appNavigationController"; export default class ChatTopbar { container: HTMLDivElement; @@ -141,8 +142,9 @@ export default class ChatTopbar { attachClickEvent(this.btnBack, (e) => { cancelEvent(e); - this.chat.appImManager.setPeer(0); - blurActiveElement(); + /* this.chat.appImManager.setPeer(0); + blurActiveElement(); */ + appNavigationController.back(); }, {listenerSetter: this.listenerSetter}); } diff --git a/src/components/sidebarLeft/index.ts b/src/components/sidebarLeft/index.ts index 4cc89ef1..4dca30ab 100644 --- a/src/components/sidebarLeft/index.ts +++ b/src/components/sidebarLeft/index.ts @@ -82,7 +82,10 @@ export class AppSidebarLeft extends SidebarSlider { searchSuper: AppSearchSuper; constructor() { - super(document.getElementById('column-left') as HTMLDivElement); + super({ + sidebarEl: document.getElementById('column-left') as HTMLDivElement, + navigationType: 'left' + }); Object.assign(this.tabs, { [AppSidebarLeft.SLIDERITEMSIDS.archived]: archivedTab, diff --git a/src/components/sidebarRight/index.ts b/src/components/sidebarRight/index.ts index f6f797e9..58cc223e 100644 --- a/src/components/sidebarRight/index.ts +++ b/src/components/sidebarRight/index.ts @@ -6,17 +6,12 @@ import AppGifsTab from "./tabs/gifs"; import mediaSizes, { ScreenSize } from "../../helpers/mediaSizes"; import AppPrivateSearchTab from "./tabs/search"; import AppSharedMediaTab from "./tabs/sharedMedia"; -//import AppForwardTab from "./tabs/forward"; -import { pause } from "../../helpers/schedulers"; -import rootScope from "../../lib/rootScope"; -import { dispatchHeavyAnimationEvent } from "../../hooks/useHeavyAnimationCheck"; import { MOUNT_CLASS_TO } from "../../config/debug"; export const RIGHT_COLUMN_ACTIVE_CLASSNAME = 'is-right-column-shown'; const sharedMediaTab = new AppSharedMediaTab(); const searchTab = new AppPrivateSearchTab(); -//const forwardTab = new AppForwardTab(); const stickersTab = new AppStickersTab(); const pollResultsTab = new AppPollResultsTab(); const gifsTab = new AppGifsTab(); @@ -37,13 +32,18 @@ export class AppSidebarRight extends SidebarSlider { public gifsTab: AppGifsTab; constructor() { - super(document.getElementById('column-right') as HTMLElement, { - [AppSidebarRight.SLIDERITEMSIDS.sharedMedia]: sharedMediaTab, - [AppSidebarRight.SLIDERITEMSIDS.search]: searchTab, - [AppSidebarRight.SLIDERITEMSIDS.stickers]: stickersTab, - [AppSidebarRight.SLIDERITEMSIDS.pollResults]: pollResultsTab, - [AppSidebarRight.SLIDERITEMSIDS.gifs]: gifsTab - }, true); + super({ + sidebarEl: document.getElementById('column-right') as HTMLElement, + tabs: { + [AppSidebarRight.SLIDERITEMSIDS.sharedMedia]: sharedMediaTab, + [AppSidebarRight.SLIDERITEMSIDS.search]: searchTab, + [AppSidebarRight.SLIDERITEMSIDS.stickers]: stickersTab, + [AppSidebarRight.SLIDERITEMSIDS.pollResults]: pollResultsTab, + [AppSidebarRight.SLIDERITEMSIDS.gifs]: gifsTab + }, + canHideFirst: true, + navigationType: 'right' + }); this.sharedMediaTab = sharedMediaTab; this.searchTab = searchTab; @@ -58,12 +58,12 @@ export class AppSidebarRight extends SidebarSlider { }); } - public onCloseTab(id: number) { + public onCloseTab(id: number, animate: boolean) { if(!this.historyTabIds.length) { - this.toggleSidebar(false); + this.toggleSidebar(false, animate); } - super.onCloseTab(id); + super.onCloseTab(id, animate); } /* public selectTab(id: number) { @@ -76,7 +76,7 @@ export class AppSidebarRight extends SidebarSlider { return res; } */ - public toggleSidebar(enable?: boolean, saveStatus = true) { + public toggleSidebar(enable?: boolean, animate?: boolean) { /////this.log('sidebarEl', this.sidebarEl, enable, isElementInViewport(this.sidebarEl)); const active = document.body.classList.contains(RIGHT_COLUMN_ACTIVE_CLASSNAME); @@ -95,30 +95,13 @@ export class AppSidebarRight extends SidebarSlider { if(!willChange) return Promise.resolve(); - if(saveStatus) { - appImManager.hideRightSidebar = false; - } - if(!active && !this.historyTabIds.length) { this.selectTab(AppSidebarRight.SLIDERITEMSIDS.sharedMedia); } - const transitionTime = rootScope.settings.animationsEnabled ? (mediaSizes.isMobile ? 250 : 200) : 0; - const promise = pause(transitionTime); - if(transitionTime) { - dispatchHeavyAnimationEvent(promise, transitionTime); - } - + const animationPromise = appImManager.selectTab(active ? 1 : 2, animate); document.body.classList.toggle(RIGHT_COLUMN_ACTIVE_CLASSNAME, enable); - //console.log('sidebar selectTab', enable, willChange); - //if(mediaSizes.isMobile) { - //appImManager._selectTab(active ? 1 : 2); - appImManager.selectTab(active ? 1 : 2); - return promise; // delay of slider animation - //} - - return pause(200); // delay for third column open - //return Promise.resolve(); + return animationPromise; /* return new Promise((resolve, reject) => { const hidden: {element: HTMLDivElement, height: number}[] = []; diff --git a/src/components/slider.ts b/src/components/slider.ts index c4d81347..432e1e3f 100644 --- a/src/components/slider.ts +++ b/src/components/slider.ts @@ -3,6 +3,8 @@ import { horizontalMenu } from "./horizontalMenu"; import ButtonIcon from "./buttonIcon"; import Scrollable from "./scrollable"; import { TransitionSlider } from "./transition"; +import appNavigationController, { NavigationItem } from "./appNavigationController"; +import { isSafari } from "../helpers/userAgent"; export interface SliderTab { onOpen?: () => void, @@ -84,28 +86,47 @@ export default class SidebarSlider { protected _selectTab: ReturnType; public historyTabIds: number[] = []; public tabsContainer: HTMLElement; + public sidebarEl: HTMLElement; + public tabs: {[id: number]: SliderTab} = {}; + private canHideFirst = false; + private navigationType: NavigationItem['type'] + + constructor(options: { + sidebarEl: SidebarSlider['sidebarEl'], + tabs?: SidebarSlider['tabs'], + canHideFirst?: SidebarSlider['canHideFirst'], + navigationType: SidebarSlider['navigationType'] + }) { + for(const i in options) { + // @ts-ignore + this[i] = options[i]; + } - constructor(public sidebarEl: HTMLElement, public tabs: {[id: number]: SliderTab} = {}, private canHideFirst = false) { this.tabsContainer = this.sidebarEl.querySelector('.sidebar-slider'); this._selectTab = TransitionSlider(this.tabsContainer, 'navigation', TRANSITION_TIME); - if(!canHideFirst) { + if(!this.canHideFirst) { this._selectTab(0); } Array.from(this.sidebarEl.querySelectorAll('.sidebar-close-button') as any as HTMLElement[]).forEach(el => { - attachClickEvent(el, () => this.closeTab()); + attachClickEvent(el, this.onCloseBtnClick); }); } - public closeTab = (tabId?: number) => { + private onCloseBtnClick = () => { + appNavigationController.back(); + // this.closeTab(); + }; + + public closeTab = (tabId?: number, animate?: boolean) => { if(tabId !== undefined && this.historyTabIds[this.historyTabIds.length - 1] !== tabId) { return false; } //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] ?? (this.canHideFirst ? -1 : 0)); + const closingId = this.historyTabIds.pop(); // pop current + this.onCloseTab(closingId, animate); + this._selectTab(this.historyTabIds[this.historyTabIds.length - 1] ?? (this.canHideFirst ? -1 : 0), animate); return true; }; @@ -130,6 +151,16 @@ export default class SidebarSlider { }, TRANSITION_TIME); } } + + //if(!this.canHideFirst || this.historyTabIds.length) { + appNavigationController.pushItem({ + type: this.navigationType, + onPop: (canAnimate) => { + this.closeTab(undefined, canAnimate); + return true; + } + }); + //} this.historyTabIds.push(id); this._selectTab(id); @@ -138,10 +169,10 @@ export default class SidebarSlider { public removeTabFromHistory(id: number) { this.historyTabIds.findAndSplice(i => i === id); - this.onCloseTab(id); + this.onCloseTab(id, undefined); } - public onCloseTab(id: number) { + public onCloseTab(id: number, animate: boolean) { let tab = this.tabs[id]; if(tab) { if(tab.onClose) { @@ -165,7 +196,7 @@ export default class SidebarSlider { this.tabsContainer.append(tab.container); if(tab.closeBtn) { - tab.closeBtn.addEventListener('click', () => this.closeTab()); + tab.closeBtn.addEventListener('click', this.onCloseBtnClick); } } @@ -173,4 +204,4 @@ export default class SidebarSlider { return id; } -} \ No newline at end of file +} diff --git a/src/helpers/dom.ts b/src/helpers/dom.ts index d732de6d..a6f569bf 100644 --- a/src/helpers/dom.ts +++ b/src/helpers/dom.ts @@ -5,6 +5,7 @@ import { isTouchSupported } from "./touchSupport"; import { isApple } from "./userAgent"; import rootScope from "../lib/rootScope"; import { MOUNT_CLASS_TO } from "../config/debug"; +import { superRaf } from "./schedulers"; /* export function isInDOM(element: Element, parentNode?: HTMLElement): boolean { if(!element) { @@ -775,3 +776,11 @@ export function isSelectionEmpty(selection = window.getSelection()) { return false; } + +export function disableTransition(elements: HTMLElement[]) { + elements.forEach(el => el.classList.add('no-transition')); + + superRaf().then(() => { + elements.forEach(el => el.classList.remove('no-transition')); + }); +} diff --git a/src/helpers/schedulers.ts b/src/helpers/schedulers.ts index 5d6bd335..0edc5d7b 100644 --- a/src/helpers/schedulers.ts +++ b/src/helpers/schedulers.ts @@ -123,3 +123,11 @@ export function fastRaf(callback: NoneToVoidFunction) { fastRafCallbacks.push(callback); } } + +export function superRaf() { + return new Promise((resolve) => { + window.requestAnimationFrame(() => { + window.requestAnimationFrame(resolve); + }); + }); +} diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index 40744ae1..3394b868 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -18,7 +18,7 @@ import appPhotosManager from './appPhotosManager'; import appProfileManager from './appProfileManager'; import appStickersManager from './appStickersManager'; import appWebPagesManager from './appWebPagesManager'; -import { cancelEvent, getFilesFromEvent, placeCaretAtEnd } from '../../helpers/dom'; +import { blurActiveElement, cancelEvent, disableTransition, getFilesFromEvent, placeCaretAtEnd, whichChild } from '../../helpers/dom'; import PopupNewMedia from '../../components/popups/newMedia'; import { numberThousandSplitter } from '../../helpers/number'; import MarkupTooltip from '../../components/chat/markupTooltip'; @@ -26,16 +26,17 @@ import { isTouchSupported } from '../../helpers/touchSupport'; import appPollsManager from './appPollsManager'; import SetTransition from '../../components/singleTransition'; import ChatDragAndDrop from '../../components/chat/dragAndDrop'; -import { debounce, pause } from '../../helpers/schedulers'; +import { debounce, pause, superRaf } from '../../helpers/schedulers'; import lottieLoader from '../lottieLoader'; import useHeavyAnimationCheck, { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck'; import appDraftsManager from './appDraftsManager'; import serverTimeManager from '../mtproto/serverTimeManager'; import sessionStorage from '../sessionStorage'; -import { renderImageFromUrl } from '../../components/misc'; import appDownloadManager from './appDownloadManager'; import appStateManager, { AppStateManager } from './appStateManager'; import { MOUNT_CLASS_TO } from '../../config/debug'; +import appNavigationController from '../../components/appNavigationController'; +import { isSafari } from '../../helpers/userAgent'; //console.log('appImManager included33!'); @@ -58,7 +59,6 @@ export class AppImManager { public setPeerPromise: Promise = null; public tabId = -1; - public hideRightSidebar = false; private chats: Chat[] = []; private prevTab: HTMLElement; @@ -179,6 +179,14 @@ export class AppImManager { this.setBackground(''); } + // * fix simultaneous opened both sidebars, can happen when floating sidebar is opened with left sidebar + mediaSizes.addListener('changeScreen', (from, to) => { + if(document.body.classList.contains(LEFT_COLUMN_ACTIVE_CLASSNAME) + && document.body.classList.contains(RIGHT_COLUMN_ACTIVE_CLASSNAME)) { + appSidebarRight.toggleSidebar(false); + } + }); + /* rootScope.on('peer_changing', (chat) => { this.saveChatPosition(chat); }); @@ -248,18 +256,35 @@ export class AppImManager { // * не могу использовать тут TransitionSlider, так как мне нужен отрисованный блок рядом // * (или под текущим чатом) чтобы правильно отрендерить чат (напр. scrollTop) - private chatsSelectTab(tab: HTMLElement) { + private chatsSelectTab(tab: HTMLElement, animate?: boolean) { if(this.prevTab === tab) { return; } + if(animate === false && this.prevTab) { // * will be used for Safari iOS history swipe + disableTransition([tab, this.prevTab].filter(Boolean)); + } + if(this.prevTab) { this.prevTab.classList.remove('active'); this.chatsSelectTabDebounced(); - if(rootScope.settings.animationsEnabled) { // ! нужно переделать на animation, так как при лаге анимация будет длиться не 250мс + // ! нужно переделать на animation, так как при лаге анимация будет длиться не 250мс + if(rootScope.settings.animationsEnabled && animate !== false) { dispatchHeavyAnimationEvent(pause(250 + 150), 250 + 150); } + + const prevIdx = whichChild(this.prevTab); + const idx = whichChild(tab); + if(idx > prevIdx) { + appNavigationController.pushItem({ + type: 'chat', + onPop: (canAnimate) => { + this.setPeer(0, undefined, canAnimate); + blurActiveElement(); + } + }); + } } tab.classList.add('active'); @@ -509,34 +534,50 @@ export class AppImManager { }); }; - public selectTab(id: number) { + public selectTab(id: number, animate?: boolean) { + if(animate === false) { // * will be used for Safari iOS history swipe + disableTransition([appSidebarLeft.sidebarEl, this.columnEl, appSidebarRight.sidebarEl]); + } + document.body.classList.toggle(LEFT_COLUMN_ACTIVE_CLASSNAME, id === 0); const prevTabId = this.tabId; this.log('selectTab', id, prevTabId); - if(prevTabId !== -1 && prevTabId !== id && rootScope.settings.animationsEnabled) { + let animationPromise: Promise = superRaf(); + if(prevTabId !== -1 && prevTabId !== id && rootScope.settings.animationsEnabled && animate !== false) { const transitionTime = (mediaSizes.isMobile ? 250 : 200) + 100; // * cause transition time could be > 250ms - const promise = pause(transitionTime); - dispatchHeavyAnimationEvent(promise, transitionTime); + animationPromise = pause(transitionTime); + dispatchHeavyAnimationEvent(animationPromise, transitionTime); this.columnEl.classList.add('disable-hover'); - promise.finally(() => { + animationPromise.finally(() => { this.columnEl.classList.remove('disable-hover'); }); } this.tabId = id; - if(mediaSizes.isMobile && prevTabId === 2 && id === 1) { - //appSidebarRight.toggleSidebar(false); + if(mediaSizes.isMobile && prevTabId === 2 && id < 2) { document.body.classList.remove(RIGHT_COLUMN_ACTIVE_CLASSNAME); } + if(prevTabId !== -1 && id > prevTabId && id < 2) { + appNavigationController.pushItem({ + type: 'im', + onPop: (canAnimate) => { + //this.selectTab(prevTabId, !isSafari); + this.setPeer(0, undefined, canAnimate); + } + }); + } + rootScope.broadcast('im_tab_change', id); //this._selectTab(id, mediaSizes.isMobile); //document.body.classList.toggle(RIGHT_COLUMN_ACTIVE_CLASSNAME, id === 2); + + return animationPromise; } public updateStatus() { @@ -556,7 +597,7 @@ export class AppImManager { this.chats.push(chat); } - private spliceChats(fromIndex: number, justReturn = true) { + private spliceChats(fromIndex: number, justReturn = true, animate?: boolean) { if(fromIndex >= this.chats.length) return; if(this.chats.length > 1 && justReturn) { @@ -572,7 +613,7 @@ export class AppImManager { }); } - this.chatsSelectTab(this.chat.container); + this.chatsSelectTab(this.chat.container, animate); if(justReturn) { rootScope.broadcast('peer_changed', this.chat.peerId); @@ -598,7 +639,7 @@ export class AppImManager { }, 250 + 100); } - public setPeer(peerId: number, lastMsgId?: number): boolean { + public setPeer(peerId: number, lastMsgId?: number, animate?: boolean): boolean { if(this.init) { this.init(); this.init = null; @@ -609,29 +650,20 @@ export class AppImManager { if(!peerId) { if(chatIndex > 0) { - this.spliceChats(chatIndex); + this.spliceChats(chatIndex, undefined, animate); return; } else if(mediaSizes.activeScreen === ScreenSize.medium) { // * floating sidebar case - const isNowOpen = document.body.classList.toggle(LEFT_COLUMN_ACTIVE_CLASSNAME); - - if(isNowOpen && document.body.classList.contains(RIGHT_COLUMN_ACTIVE_CLASSNAME)) { - appSidebarRight.toggleSidebar(false, false); - this.selectTab(0); - this.hideRightSidebar = isNowOpen; - } else if(this.hideRightSidebar) { - appSidebarRight.toggleSidebar(true); - } - + this.selectTab(+!this.tabId, animate); return; } } else if(chatIndex > 0 && chat.peerId && chat.peerId !== peerId) { - this.spliceChats(1, false); + this.spliceChats(1, false, animate); return this.setPeer(peerId, lastMsgId); } // * don't reset peer if returning - if(peerId === chat.peerId && mediaSizes.activeScreen === ScreenSize.mobile && document.body.classList.contains(LEFT_COLUMN_ACTIVE_CLASSNAME)) { - this.selectTab(1); + if(peerId === chat.peerId && mediaSizes.activeScreen <= ScreenSize.medium && document.body.classList.contains(LEFT_COLUMN_ACTIVE_CLASSNAME)) { + this.selectTab(1, animate); return false; } @@ -644,22 +676,17 @@ export class AppImManager { promise.then(() => { //window.requestAnimationFrame(() => { setTimeout(() => { // * setTimeout is better here - if(this.hideRightSidebar) { - appSidebarRight.toggleSidebar(true); - this.hideRightSidebar = false; - } - setTimeout(() => { this.chatsSelectTab(this.chat.container); }, 0); - this.selectTab(1); + this.selectTab(1, animate); }, 0); }); } } if(!peerId) { - this.selectTab(0); + this.selectTab(0, animate); return false; } } diff --git a/src/scss/partials/_leftSidebar.scss b/src/scss/partials/_leftSidebar.scss index a8dcb35e..4cadac1f 100644 --- a/src/scss/partials/_leftSidebar.scss +++ b/src/scss/partials/_leftSidebar.scss @@ -26,6 +26,7 @@ width: 26.5rem; transform: translate3d(-5rem, 0, 0); transition: transform var(--layer-transition); + max-width: unset; body.animation-level-0 & { transition: none; diff --git a/src/scss/partials/_rightSidebar.scss b/src/scss/partials/_rightSidebar.scss index fca3b573..e20c405f 100644 --- a/src/scss/partials/_rightSidebar.scss +++ b/src/scss/partials/_rightSidebar.scss @@ -27,7 +27,7 @@ border-left-width: 0; } */ - body.is-right-column-shown & { + body.is-right-column-shown:not(.is-left-column-shown) & { transform: translate3d(0, 0, 0); }