From 2d6d47f7e86755d51ae8c1328fd69aff74268c47 Mon Sep 17 00:00:00 2001 From: Eduard Kuzmenko Date: Mon, 15 Feb 2021 19:49:58 +0400 Subject: [PATCH] Shared media sticky header Shared media close icon animation New left sidebar icon menu animation Debounce opening right sidebar Fix chat input overflow when multiselecting Fix chat input height when ESG doesn't fit --- src/components/appSearchSuper..ts | 4 +- src/components/horizontalMenu.ts | 4 +- src/components/scrollable.ts | 9 +- src/components/sidebarLeft/index.ts | 2 + src/components/sidebarRight/index.ts | 2 +- .../sidebarRight/tabs/sharedMedia.ts | 42 +++++++- src/components/slider.ts | 3 +- src/components/transition.ts | 30 ++++-- src/helpers/blur.ts | 4 +- src/helpers/fastSmoothScroll.ts | 2 +- src/index.hbs | 22 ++-- src/lib/appManagers/appImManager.ts | 8 +- src/lib/mtproto/networker.ts | 38 +++---- src/scss/partials/_animatedIcon.scss | 81 ++++++++++++++ src/scss/partials/_chat.scss | 15 +-- src/scss/partials/_leftSidebar.scss | 39 ++----- src/scss/partials/_rightSidebar.scss | 13 ++- src/scss/style.scss | 100 ++++++++++++++++++ 18 files changed, 327 insertions(+), 91 deletions(-) create mode 100644 src/scss/partials/_animatedIcon.scss diff --git a/src/components/appSearchSuper..ts b/src/components/appSearchSuper..ts index c9611cce..849c8150 100644 --- a/src/components/appSearchSuper..ts +++ b/src/components/appSearchSuper..ts @@ -80,7 +80,7 @@ export default class AppSearchSuper { private searchGroupMedia: SearchGroup; - constructor(public types: {inputFilter: SearchSuperType, name: string}[], public scrollable: Scrollable, public searchGroups?: {[group in SearchGroupType]: SearchGroup}, public asChatList = false) { + constructor(public types: {inputFilter: SearchSuperType, name: string}[], public scrollable: Scrollable, public searchGroups?: {[group in SearchGroupType]: SearchGroup}, public asChatList = false, public groupByMonth = true) { this.container = document.createElement('div'); this.container.classList.add('search-super'); @@ -549,7 +549,7 @@ export default class AppSearchSuper { const method = append ? 'append' : 'prepend'; elemsToAppend.forEach(details => { const {element, message} = details; - const monthContainer = this.getMonthContainerByTimestamp(message.date, type); + const monthContainer = this.getMonthContainerByTimestamp(this.groupByMonth ? message.date : 0, type); element.classList.add('search-super-item'); element.dataset.mid = '' + message.mid; element.dataset.peerId = '' + message.peerId; diff --git a/src/components/horizontalMenu.ts b/src/components/horizontalMenu.ts index c83ade0c..77334720 100644 --- a/src/components/horizontalMenu.ts +++ b/src/components/horizontalMenu.ts @@ -1,4 +1,4 @@ -import { findUpTag, whichChild, findUpAsChild } from "../helpers/dom"; +import { whichChild, findUpAsChild } from "../helpers/dom"; import { TransitionSlider } from "./transition"; import { ScrollableX } from "./scrollable"; import rootScope from "../lib/rootScope"; @@ -105,4 +105,4 @@ export function horizontalMenu(tabs: HTMLElement, content: HTMLElement, onClick? } return selectTab; -} \ No newline at end of file +} diff --git a/src/components/scrollable.ts b/src/components/scrollable.ts index 61b7f696..eb2efbb5 100644 --- a/src/components/scrollable.ts +++ b/src/components/scrollable.ts @@ -52,6 +52,7 @@ export class ScrollableBase { protected onScroll: () => void; public isHeavyAnimationInProgress = false; + protected needCheckAfterAnimation = false; constructor(public el: HTMLElement, logPrefix = '', public container: HTMLElement = document.createElement('div')) { this.container.classList.add('scrollable'); @@ -74,11 +75,16 @@ export class ScrollableBase { this.isHeavyAnimationInProgress = true; if(this.onScrollMeasure) { + this.needCheckAfterAnimation = true; window.cancelAnimationFrame(this.onScrollMeasure); } }, () => { this.isHeavyAnimationInProgress = false; - this.onScroll(); + + if(this.needCheckAfterAnimation) { + this.onScroll(); + this.needCheckAfterAnimation = false; + } }); } @@ -146,6 +152,7 @@ export default class Scrollable extends ScrollableBase { window.cancelAnimationFrame(this.onScrollMeasure); } + this.needCheckAfterAnimation = true; return; } diff --git a/src/components/sidebarLeft/index.ts b/src/components/sidebarLeft/index.ts index 0ff80cf1..4cc89ef1 100644 --- a/src/components/sidebarLeft/index.ts +++ b/src/components/sidebarLeft/index.ts @@ -418,6 +418,7 @@ export class AppSidebarLeft extends SidebarSlider { this.toolsBtn.classList.remove('active'); this.backBtn.classList.add('active'); this.newBtnMenu.classList.add('is-hidden'); + this.toolsBtn.parentElement.firstElementChild.classList.toggle('state-back', true); transition(1); }; @@ -428,6 +429,7 @@ export class AppSidebarLeft extends SidebarSlider { this.backBtn.addEventListener('click', (e) => { this.toolsBtn.classList.add('active'); this.backBtn.classList.remove('active'); + this.toolsBtn.parentElement.firstElementChild.classList.toggle('state-back', false); transition(0); }); diff --git a/src/components/sidebarRight/index.ts b/src/components/sidebarRight/index.ts index 4e1fe27a..f6f797e9 100644 --- a/src/components/sidebarRight/index.ts +++ b/src/components/sidebarRight/index.ts @@ -103,7 +103,7 @@ export class AppSidebarRight extends SidebarSlider { this.selectTab(AppSidebarRight.SLIDERITEMSIDS.sharedMedia); } - const transitionTime = rootScope.settings.animationsEnabled ? mediaSizes.isMobile ? 250 : 200 : 0; + const transitionTime = rootScope.settings.animationsEnabled ? (mediaSizes.isMobile ? 250 : 200) : 0; const promise = pause(transitionTime); if(transitionTime) { dispatchHeavyAnimationEvent(promise, transitionTime); diff --git a/src/components/sidebarRight/tabs/sharedMedia.ts b/src/components/sidebarRight/tabs/sharedMedia.ts index a71d9a47..b0be4f8d 100644 --- a/src/components/sidebarRight/tabs/sharedMedia.ts +++ b/src/components/sidebarRight/tabs/sharedMedia.ts @@ -11,6 +11,9 @@ import AvatarElement from "../../avatar"; import Scrollable from "../../scrollable"; import { SliderTab } from "../../slider"; import CheckboxField from "../../checkbox"; +import { attachClickEvent, cancelEvent } from "../../../helpers/dom"; +import appSidebarRight from ".."; +import { TransitionSlider } from "../../transition"; let setText = (text: string, el: HTMLDivElement) => { window.requestAnimationFrame(() => { @@ -29,7 +32,7 @@ let setText = (text: string, el: HTMLDivElement) => { // TODO: отредактированное сообщение не изменится export default class AppSharedMediaTab implements SliderTab { public container: HTMLElement; - public closeBtn: HTMLElement; + public closeBtn: HTMLButtonElement; private peerId = 0; private threadId = 0; @@ -62,7 +65,8 @@ export default class AppSharedMediaTab implements SliderTab { public init() { this.container = document.getElementById('shared-media-container'); - this.closeBtn = this.container.querySelector('.sidebar-close-button'); + this.closeBtn = this.container.querySelector('.sidebar-header .btn-icon'); + this.closeBtn.classList.add('sidebar-close-button'); this.profileContentEl = this.container.querySelector('.profile-content'); this.profileElements = { @@ -86,7 +90,37 @@ export default class AppSharedMediaTab implements SliderTab { this.profileElements.notificationsRow.prepend(checkboxField.label); this.scroll = new Scrollable(this.container, 'SR', 400); - + + const HEADER_HEIGHT = 56; + const closeIcon = this.closeBtn.firstElementChild as HTMLElement; + this.scroll.onAdditionalScroll = () => { + const rect = this.searchSuper.nav.getBoundingClientRect(); + if(!rect.width) return; + + //console.log('daddy issues', this.searchSuper.nav.getBoundingClientRect()); + + const top = rect.top; + const isSharedMedia = top <= HEADER_HEIGHT; + closeIcon.classList.toggle('state-back', isSharedMedia); + transition(+isSharedMedia); + }; + + const transition = TransitionSlider(this.closeBtn.nextElementSibling as HTMLElement, 'slide-fade', 400, null, false); + + transition(0); + + attachClickEvent(this.closeBtn, (e) => { + if(this.closeBtn.firstElementChild.classList.contains('state-back')) { + this.scroll.scrollIntoViewNew(this.scroll.container.firstElementChild as HTMLElement, 'start'); + transition(0); + closeIcon.classList.remove('state-back'); + } else if(!this.scroll.isHeavyAnimationInProgress) { + appSidebarRight.closeTab(); + } + }); + + this.container.prepend(this.closeBtn.parentElement); + this.profileElements.notificationsCheckbox.addEventListener('change', () => { //let checked = this.profileElements.notificationsCheckbox.checked; appMessagesManager.mutePeer(this.peerId); @@ -134,7 +168,7 @@ export default class AppSharedMediaTab implements SliderTab { }, { inputFilter: 'inputMessagesFilterMusic', name: 'Music' - }], this.scroll); + }], this.scroll/* , undefined, undefined, false */); this.profileContentEl.append(this.searchSuper.container); } diff --git a/src/components/slider.ts b/src/components/slider.ts index 5d121cf0..c4d81347 100644 --- a/src/components/slider.ts +++ b/src/components/slider.ts @@ -2,6 +2,7 @@ import { attachClickEvent } from "../helpers/dom"; import { horizontalMenu } from "./horizontalMenu"; import ButtonIcon from "./buttonIcon"; import Scrollable from "./scrollable"; +import { TransitionSlider } from "./transition"; export interface SliderTab { onOpen?: () => void, @@ -86,7 +87,7 @@ export default class SidebarSlider { constructor(public sidebarEl: HTMLElement, public tabs: {[id: number]: SliderTab} = {}, private canHideFirst = false) { this.tabsContainer = this.sidebarEl.querySelector('.sidebar-slider'); - this._selectTab = horizontalMenu(null, this.tabsContainer as HTMLDivElement, null, null, TRANSITION_TIME); + this._selectTab = TransitionSlider(this.tabsContainer, 'navigation', TRANSITION_TIME); if(!canHideFirst) { this._selectTab(0); } diff --git a/src/components/transition.ts b/src/components/transition.ts index e26ec4a8..d23cb62a 100644 --- a/src/components/transition.ts +++ b/src/components/transition.ts @@ -46,7 +46,7 @@ function slideTabs(tabContent: HTMLElement, prevTabContent: HTMLElement, toRight }; } -export const TransitionSlider = (content: HTMLElement, type: 'tabs' | 'navigation' | 'zoom-fade' | 'none'/* | 'counter' */, transitionTime: number, onTransitionEnd?: (id: number) => void) => { +export const TransitionSlider = (content: HTMLElement, type: 'tabs' | 'navigation' | 'zoom-fade' | 'slide-fade' | 'none'/* | 'counter' */, transitionTime: number, onTransitionEnd?: (id: number) => void, isHeavy = true) => { let animationFunction: TransitionFunction = null; switch(type) { @@ -62,12 +62,12 @@ export const TransitionSlider = (content: HTMLElement, type: 'tabs' | 'navigatio content.dataset.animation = type; - return Transition(content, animationFunction, transitionTime, onTransitionEnd); + return Transition(content, animationFunction, transitionTime, onTransitionEnd, isHeavy); }; type TransitionFunction = (tabContent: HTMLElement, prevTabContent: HTMLElement, toRight: boolean) => void | (() => void); -const Transition = (content: HTMLElement, animationFunction: TransitionFunction, transitionTime: number, onTransitionEnd?: (id: number) => void) => { +const Transition = (content: HTMLElement, animationFunction: TransitionFunction, transitionTime: number, onTransitionEnd?: (id: number) => void, isHeavy = true) => { const onTransitionEndCallbacks: Map = new Map(); let animationDeferred: CancellablePromise; let animationStarted = 0; @@ -84,10 +84,16 @@ const Transition = (content: HTMLElement, animationFunction: TransitionFunction, const callback = onTransitionEndCallbacks.get(e.target as HTMLElement); if(callback) callback(); - if(!animationDeferred || e.target !== from) return; + if(e.target !== from) { + return; + } - animationDeferred.resolve(); - animationDeferred = undefined; + if(!animationDeferred && isHeavy) return; + + if(animationDeferred) { + animationDeferred.resolve(); + animationDeferred = undefined; + } if(onTransitionEnd) { onTransitionEnd(selectTab.prevId); @@ -180,12 +186,14 @@ const Transition = (content: HTMLElement, animationFunction: TransitionFunction, }); } - if(!animationDeferred) { - animationDeferred = deferredPromise(); - animationStarted = performance.now(); + if(isHeavy) { + if(!animationDeferred) { + animationDeferred = deferredPromise(); + animationStarted = performance.now(); + } + + dispatchHeavyAnimationEvent(animationDeferred, transitionTime * 2); } - - dispatchHeavyAnimationEvent(animationDeferred, transitionTime * 2); } self.prevId = id; diff --git a/src/helpers/blur.ts b/src/helpers/blur.ts index 50d67c48..12c3c98d 100644 --- a/src/helpers/blur.ts +++ b/src/helpers/blur.ts @@ -1,10 +1,12 @@ -import DEBUG from '../config/debug'; +import _DEBUG from '../config/debug'; import fastBlur from '../vendor/fastBlur'; import pushHeavyTask from './heavyQueue'; const RADIUS = 2; const ITERATIONS = 2; +const DEBUG = _DEBUG && false; + function processBlur(dataUri: string, radius: number, iterations: number) { return new Promise((resolve) => { const img = new Image(); diff --git a/src/helpers/fastSmoothScroll.ts b/src/helpers/fastSmoothScroll.ts index b52ffce2..be1229d5 100644 --- a/src/helpers/fastSmoothScroll.ts +++ b/src/helpers/fastSmoothScroll.ts @@ -43,7 +43,7 @@ export default function fastSmoothScroll( return Promise.resolve(); */ } - if(axis === 'y' && isInDOM(element) && container.getBoundingClientRect) { + if(axis === 'y' && element !== container && isInDOM(element) && container.getBoundingClientRect) { const elementRect = element.getBoundingClientRect(); const containerRect = container.getBoundingClientRect(); diff --git a/src/index.hbs b/src/index.hbs index 5eb1af2c..07321ac7 100644 --- a/src/index.hbs +++ b/src/index.hbs @@ -98,17 +98,18 @@