
Floating avatars Dialogs placeholder Bubble grouping fixes Layer 143 Improve profile change performance Webpack 5 Message reading fixes
269 lines
7.7 KiB
TypeScript
269 lines
7.7 KiB
TypeScript
/*
|
|
* https://github.com/morethanwords/tweb
|
|
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
|
*/
|
|
|
|
import { MOUNT_CLASS_TO } from "../config/debug";
|
|
import { IS_MOBILE_SAFARI } from "../environment/userAgent";
|
|
import { logger } from "../lib/logger";
|
|
import blurActiveElement from "../helpers/dom/blurActiveElement";
|
|
import cancelEvent from "../helpers/dom/cancelEvent";
|
|
import isSwipingBackSafari from "../helpers/dom/isSwipingBackSafari";
|
|
import indexOfAndSplice from "../helpers/array/indexOfAndSplice";
|
|
|
|
export type NavigationItem = {
|
|
type: 'left' | 'right' | 'im' | 'chat' | 'popup' | 'media' | 'menu' |
|
|
'esg' | 'multiselect' | 'input-helper' | 'autocomplete-helper' | 'markup' |
|
|
'global-search' | 'voice' | 'mobile-search' | 'filters' | 'global-search-focus',
|
|
onPop: (canAnimate: boolean) => boolean | void,
|
|
onEscape?: () => boolean,
|
|
noHistory?: boolean,
|
|
noBlurOnPop?: boolean,
|
|
};
|
|
|
|
export class AppNavigationController {
|
|
private navigations: Array<NavigationItem>;
|
|
private id: number;
|
|
private manual: boolean;
|
|
private log: ReturnType<typeof logger>;
|
|
private debug: boolean;
|
|
private currentHash: string; // have to start with # if not empty
|
|
private overriddenHash: string; // have to start with # if not empty
|
|
private isPossibleSwipe: boolean;
|
|
public onHashChange: () => void;
|
|
|
|
constructor() {
|
|
this.navigations = [];
|
|
this.id = Date.now();
|
|
this.manual = false;
|
|
this.log = logger('NC');
|
|
this.debug = true;
|
|
this.currentHash = window.location.hash;
|
|
this.overriddenHash = '';
|
|
this.isPossibleSwipe = false;
|
|
|
|
window.addEventListener('popstate', this.onPopState);
|
|
window.addEventListener('keydown', this.onKeyDown, {capture: true, passive: false});
|
|
|
|
if(IS_MOBILE_SAFARI) {
|
|
const options = {passive: true};
|
|
window.addEventListener('touchstart', this.onTouchStart, options);
|
|
}
|
|
|
|
history.scrollRestoration = 'manual';
|
|
|
|
this.pushState(); // * push init state
|
|
}
|
|
|
|
private onPopState = (e: PopStateEvent) => {
|
|
let hash = window.location.hash;
|
|
const id: number = e.state;
|
|
this.debug && this.log('popstate', e, this.isPossibleSwipe, hash);
|
|
if(hash !== this.currentHash) {
|
|
this.debug && this.log.warn(`hash changed, new=${hash}, current=${this.currentHash}, overridden=${this.overriddenHash}`);
|
|
// fix for returning to wrong hash (e.g. chat -> archive -> chat -> 3x back)
|
|
if(id === this.id && this.overriddenHash && this.overriddenHash !== hash) {
|
|
this.overrideHash(this.overriddenHash);
|
|
} else if(id/* === this.id */ && !this.overriddenHash && hash) {
|
|
this.overrideHash();
|
|
} else {
|
|
this.currentHash = hash;
|
|
this.onHashChange && this.onHashChange();
|
|
// this.replaceState();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if(id !== this.id/* && !this.navigations.length */) {
|
|
this.pushState();
|
|
|
|
if(!this.navigations.length) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
const item = this.navigations.pop();
|
|
if(!item) {
|
|
this.pushState();
|
|
return;
|
|
}
|
|
|
|
this.manual = !this.isPossibleSwipe;
|
|
this.handleItem(item);
|
|
//this.pushState(); // * prevent adding forward arrow
|
|
};
|
|
|
|
private onKeyDown = (e: KeyboardEvent) => {
|
|
const item = this.navigations[this.navigations.length - 1];
|
|
if(!item) return;
|
|
if(e.key === 'Escape' && (item.onEscape ? item.onEscape() : true)) {
|
|
cancelEvent(e);
|
|
this.back(item.type);
|
|
}
|
|
};
|
|
|
|
private onTouchStart = (e: TouchEvent) => {
|
|
if(e.touches.length > 1) return;
|
|
this.debug && this.log('touchstart');
|
|
|
|
if(isSwipingBackSafari(e)) {
|
|
this.isPossibleSwipe = true;
|
|
|
|
window.addEventListener('touchend', () => {
|
|
setTimeout(() => {
|
|
this.isPossibleSwipe = false;
|
|
}, 100);
|
|
}, {passive: true, once: true});
|
|
}
|
|
|
|
/* const detach = () => {
|
|
window.removeEventListener('touchend', onTouchEnd);
|
|
window.removeEventListener('touchmove', onTouchMove);
|
|
};
|
|
|
|
let moved = false;
|
|
const onTouchMove = (e: TouchEvent) => {
|
|
this.debug && this.log('touchmove');
|
|
if(e.touches.length > 1) {
|
|
detach();
|
|
return;
|
|
}
|
|
|
|
moved = true;
|
|
};
|
|
|
|
const onTouchEnd = (e: TouchEvent) => {
|
|
this.debug && this.log('touchend');
|
|
if(e.touches.length > 1 || !moved) {
|
|
detach();
|
|
return;
|
|
}
|
|
|
|
isPossibleSwipe = true;
|
|
doubleRaf().then(() => {
|
|
isPossibleSwipe = false;
|
|
});
|
|
|
|
detach();
|
|
};
|
|
|
|
window.addEventListener('touchend', onTouchEnd, options);
|
|
window.addEventListener('touchmove', onTouchMove, options); */
|
|
};
|
|
|
|
public overrideHash(hash: string = '') {
|
|
if(hash && hash[0] !== '#') hash = '#' + hash;
|
|
else if(hash === '#') hash = '';
|
|
|
|
this.overriddenHash = this.currentHash = hash;
|
|
this.replaceState();
|
|
this.pushState();
|
|
}
|
|
|
|
private handleItem(item: NavigationItem) {
|
|
const good = item.onPop(!this.manual ? false : undefined);
|
|
this.debug && this.log('popstate, navigation:', item, this.navigations);
|
|
if(good === false) {
|
|
this.pushItem(item);
|
|
} else if(!item.noBlurOnPop) {
|
|
blurActiveElement(); // no better place for it
|
|
}
|
|
|
|
this.manual = false;
|
|
}
|
|
|
|
public findItemByType(type: NavigationItem['type']) {
|
|
for(let i = this.navigations.length - 1; i >= 0; --i) {
|
|
const item = this.navigations[i];
|
|
if(item.type === type) {
|
|
return {item, index: i};
|
|
}
|
|
}
|
|
}
|
|
|
|
public back(type?: NavigationItem['type']) {
|
|
if(type) {
|
|
const ret = this.findItemByType(type);
|
|
if(ret) {
|
|
this.backByItem(ret.item, ret.index);
|
|
return;
|
|
}
|
|
}
|
|
|
|
history.back();
|
|
}
|
|
|
|
public backByItem(item: NavigationItem, index = this.navigations.indexOf(item)) {
|
|
this.manual = true;
|
|
// ! commented because 'popstate' event will be fired with delay
|
|
//if(index !== (this.navigations.length - 1)) {
|
|
this.navigations.splice(index, 1);
|
|
this.handleItem(item);
|
|
//}
|
|
}
|
|
|
|
private onItemAdded(item: NavigationItem) {
|
|
this.debug && this.log('onItemAdded', item, this.navigations);
|
|
|
|
if(!item.noHistory) {
|
|
this.pushState();
|
|
}
|
|
}
|
|
|
|
public pushItem(item: NavigationItem) {
|
|
this.navigations.push(item);
|
|
this.onItemAdded(item);
|
|
}
|
|
|
|
public unshiftItem(item: NavigationItem) {
|
|
this.navigations.unshift(item);
|
|
this.onItemAdded(item);
|
|
}
|
|
|
|
public spliceItems(index: number, length: number, ...items: NavigationItem[]) {
|
|
this.navigations.splice(index, length, ...items);
|
|
items.forEach((item) => {
|
|
this.onItemAdded(item);
|
|
});
|
|
}
|
|
|
|
private pushState() {
|
|
this.debug && this.log('push');
|
|
this.manual = false;
|
|
history.pushState(this.id, '');
|
|
}
|
|
|
|
public replaceState() {
|
|
this.debug && this.log.warn('replace');
|
|
|
|
const url = location.origin + location.pathname + this.overriddenHash;
|
|
history.replaceState(this.id, '', url);
|
|
}
|
|
|
|
public removeItem(item: NavigationItem) {
|
|
if(!item) {
|
|
return;
|
|
}
|
|
|
|
indexOfAndSplice(this.navigations, item);
|
|
}
|
|
|
|
public removeByType(type: NavigationItem['type'], single = false) {
|
|
for(let i = this.navigations.length - 1; i >= 0; --i) {
|
|
const item = this.navigations[i];
|
|
if(item.type === type) {
|
|
this.navigations.splice(i, 1);
|
|
|
|
if(single) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const appNavigationController = new AppNavigationController();
|
|
MOUNT_CLASS_TO.appNavigationController = appNavigationController;
|
|
export default appNavigationController;
|