Migrated to IndexedDB as session storage

Fix stickers saving with state
Fix emoji line height on Apple devices
New preloader layout for audio, docs
Fix 30fps stickers
Fix slider animation
Animation of input extension (alpha version)
Fixed reading messages
Fixed following messages, first unread
Fixed 'Unread messages' line
New heavy queue
This commit is contained in:
morethanwords 2021-01-18 22:34:41 +04:00
parent 211af0bf6c
commit 6a467556db
69 changed files with 2130 additions and 1268 deletions

View File

@ -80,7 +80,12 @@ class AppMediaViewerBase<ContentAdditionType extends string, ButtonsAdditionType
constructor(topButtons: Array<keyof AppMediaViewerBase<ContentAdditionType, ButtonsAdditionType, TargetType>['buttons']>) {
this.log = logger('AMV');
this.preloader = new ProgressivePreloader();
this.preloaderStreamable = new ProgressivePreloader(undefined, false, true);
this.preloaderStreamable = new ProgressivePreloader({
cancelable: false,
streamable: true
});
this.preloader.construct();
this.preloaderStreamable.construct();
this.lazyLoadQueue = new LazyLoadQueueBase();
this.wholeDiv = document.createElement('div');

View File

@ -21,6 +21,8 @@ import { renderImageFromUrl, putPreloader, formatPhoneNumber } from "./misc";
import { ripple } from "./ripple";
import Scrollable, { ScrollableX } from "./scrollable";
import { wrapDocument, wrapPhoto, wrapVideo } from "./wrappers";
import useHeavyAnimationCheck, { getHeavyAnimationPromise } from "../hooks/useHeavyAnimationCheck";
import { p } from "../mock/srp";
const testScroll = false;
@ -47,7 +49,7 @@ export default class AppSearchSuper {
public container: HTMLElement;
public nav: HTMLElement;
private tabsContainer: HTMLElement;
private tabsMenu: HTMLUListElement;
private tabsMenu: HTMLElement;
private prevTabId = -1;
private lazyLoadQueue = new LazyLoadQueue();
@ -89,25 +91,25 @@ export default class AppSearchSuper {
const navScrollable = new ScrollableX(navScrollableContainer);
const nav = this.nav = document.createElement('nav');
nav.classList.add('search-super-tabs', 'menu-horizontal');
this.tabsMenu = document.createElement('ul');
nav.append(this.tabsMenu);
nav.classList.add('search-super-tabs', 'menu-horizontal-div');
this.tabsMenu = nav;
navScrollable.container.append(nav);
for(const type of types) {
const li = document.createElement('li');
const menuTab = document.createElement('div');
menuTab.classList.add('menu-horizontal-div-item');
const span = document.createElement('span');
const i = document.createElement('i');
span.innerText = type.name;
span.append(i);
li.append(span);
menuTab.append(span);
ripple(li);
ripple(menuTab);
this.tabsMenu.append(li);
this.tabsMenu.append(menuTab);
}
this.tabsContainer = document.createElement('div');
@ -194,6 +196,12 @@ export default class AppSearchSuper {
});
this.type = this.types[0].inputFilter;
useHeavyAnimationCheck(() => {
this.lazyLoadQueue.lock();
}, () => {
this.lazyLoadQueue.unlockAndRefresh(); // ! maybe not so efficient
});
}
private onTransitionStart = () => {
@ -313,6 +321,8 @@ export default class AppSearchSuper {
const sharedMediaDiv: HTMLElement = this.tabs[type];
const promises: Promise<any>[] = [];
const middleware = this.getMiddleware();
await getHeavyAnimationPromise();
let searchGroup: SearchGroup;
if(type === 'inputMessagesFilterPhotoVideo' && !!this.searchContext.query.trim()) {
@ -323,7 +333,6 @@ export default class AppSearchSuper {
searchGroup = this.searchGroups.messages;
}
// https://core.telegram.org/type/MessagesFilter
switch(type) {
case 'inputMessagesFilterEmpty': {
@ -362,7 +371,8 @@ export default class AppSearchSuper {
lazyLoadQueue: this.lazyLoadQueue,
middleware,
onlyPreview: true,
withoutPreloader: true
withoutPreloader: true,
noPlayButton: true
}).thumb;
} else {
wrapped = wrapPhoto({

View File

@ -5,14 +5,12 @@ import ProgressivePreloader from "./preloader";
import { MediaProgressLine } from "../lib/mediaPlayer";
import appMediaPlaybackController from "./appMediaPlaybackController";
import { DocumentAttribute } from "../layer";
import { Download } from "../lib/appManagers/appDownloadManager";
import mediaSizes from "../helpers/mediaSizes";
import { isSafari } from "../helpers/userAgent";
import appMessagesManager from "../lib/appManagers/appMessagesManager";
import rootScope from "../lib/rootScope";
import './middleEllipsis';
import { attachClickEvent, cancelEvent, detachClickEvent } from "../helpers/dom";
import appPeersManager from "../lib/appManagers/appPeersManager";
import { SearchSuperContext } from "./appSearchSuper.";
import { formatDateAccordingToToday } from "../helpers/date";
@ -346,6 +344,7 @@ export default class AudioElement extends HTMLElement {
private attachedHandlers: {[name: string]: any[]} = {};
private onTypeDisconnect: () => void;
public onLoad: (autoload?: boolean) => void;
constructor() {
super();
@ -362,18 +361,15 @@ export default class AudioElement extends HTMLElement {
const durationStr = String(doc.duration | 0).toHHMMSS();
this.innerHTML = `<div class="audio-toggle audio-ico tgico-largeplay">
this.innerHTML = `<div class="audio-toggle audio-ico">
<div class="part one" x="0" y="0" fill="#fff"></div>
<div class="part two" x="0" y="0" fill="#fff"></div>
</div>`;
const downloadDiv = document.createElement('div');
downloadDiv.classList.add('audio-download');
if(!uploading && isVoice) {
downloadDiv.innerHTML = '<div class="tgico-download"></div>';
}
if(isVoice || uploading) {
if(uploading) {
this.append(downloadDiv);
}
@ -382,7 +378,9 @@ export default class AudioElement extends HTMLElement {
const audioTimeDiv = this.querySelector('.audio-time') as HTMLDivElement;
audioTimeDiv.innerHTML = durationStr;
const onLoad = (autoload = true) => {
const onLoad = this.onLoad = (autoload = true) => {
this.onLoad = undefined;
const audio = this.audio = appMediaPlaybackController.addMedia(this.message.peerId, this.message.media.document || this.message.media.webpage.document, this.message.mid, autoload);
this.onTypeDisconnect = onTypeLoad();
@ -393,10 +391,7 @@ export default class AudioElement extends HTMLElement {
const onPlaying = () => {
audioTimeDiv.innerText = getTimeStr();
if(!audio.paused) {
//toggle.classList.remove('tgico-largeplay');
toggle.classList.add('tgico-largepause');
}
toggle.classList.toggle('playing', !audio.paused);
};
if(!audio.paused || (audio.currentTime > 0 && audio.currentTime != audio.duration)) {
@ -410,8 +405,7 @@ export default class AudioElement extends HTMLElement {
});
this.addAudioListener('ended', () => {
//toggle.classList.add('tgico-largeplay');
toggle.classList.remove('tgico-largepause');
toggle.classList.remove('playing');
});
this.addAudioListener('timeupdate', () => {
@ -420,8 +414,7 @@ export default class AudioElement extends HTMLElement {
});
this.addAudioListener('pause', () => {
//toggle.classList.add('tgico-largeplay');
toggle.classList.remove('tgico-largepause');
toggle.classList.remove('playing');
});
this.addAudioListener('playing', onPlaying);
@ -430,49 +423,60 @@ export default class AudioElement extends HTMLElement {
if(!uploading) {
let preloader: ProgressivePreloader = this.preloader;
if(isRealVoice) {
let download: Download;
const getDownloadPromise = () => appDocsManager.downloadDoc(doc);
const onClick = (e: Event) => {
if(e) {
cancelEvent(e);
if(isVoice) {
if(!preloader) {
preloader = new ProgressivePreloader({
cancelable: true
});
}
const load = () => {
const download = getDownloadPromise();
preloader.attach(downloadDiv, false, download);
if(!downloadDiv.parentElement) {
this.append(downloadDiv);
}
if(!download) {
if(!preloader) {
preloader = new ProgressivePreloader(null, true);
}
download = appDocsManager.downloadDoc(doc);
preloader.attach(downloadDiv, true, download);
download.then(() => {
(download as Promise<any>).then(() => {
detachClickEvent(this, onClick);
onLoad();
downloadDiv.classList.add('downloaded');
setTimeout(() => {
downloadDiv.remove();
detachClickEvent(this, onClick);
onLoad();
}).catch(err => {
if(err.name === 'AbortError') {
download = null;
}
}).finally(() => {
downloadDiv.classList.remove('downloading');
});
downloadDiv.classList.add('downloading');
} else {
download.cancel();
}
}, 200);
});
return {download};
};
preloader.construct();
preloader.setManual();
preloader.attach(downloadDiv);
preloader.setDownloadFunction(load);
const onClick = (e?: Event) => {
preloader.onClick(e);
};
attachClickEvent(this, onClick);
onClick(null);
onClick();
} else {
onLoad(false);
if(doc.supportsStreaming) {
onLoad(false);
}
//if(appMediaPlaybackController.mediaExists(mid)) { // чтобы показать прогресс, если аудио уже было скачано
//onLoad();
//} else {
const r = (e: Event) => {
if(!this.audio) {
onLoad(false);
}
if(this.audio.src) {
return;
}
@ -483,22 +487,43 @@ export default class AudioElement extends HTMLElement {
appMediaPlaybackController.willBePlayed(this.audio); // prepare for loading audio
if(!preloader) {
preloader = new ProgressivePreloader(null, false);
if(doc.supportsStreaming) {
preloader = new ProgressivePreloader({
cancelable: false
});
preloader.attach(downloadDiv, false);
} else {
preloader = new ProgressivePreloader({
cancelable: true
});
const load = () => {
const download = getDownloadPromise();
preloader.attach(downloadDiv, false, download);
return {download};
};
preloader.setDownloadFunction(load);
load();
}
}
if(isSafari) {
this.audio.autoplay = true;
this.audio.play().catch(() => {});
}
preloader.attach(downloadDiv, false);
this.append(downloadDiv);
new Promise<void>((resolve) => {
if(this.audio.readyState >= 2) resolve();
else this.addAudioListener('canplay', resolve);
}).then(() => {
downloadDiv.remove();
downloadDiv.classList.add('downloaded');
setTimeout(() => {
downloadDiv.remove();
}, 200);
//setTimeout(() => {
// release loaded audio
@ -510,7 +535,7 @@ export default class AudioElement extends HTMLElement {
});
};
if(!this.audio.src) {
if(!this.audio?.src) {
attachClickEvent(this, r, {once: true, capture: true, passive: false});
}
//}
@ -533,6 +558,10 @@ export default class AudioElement extends HTMLElement {
}
disconnectedCallback() {
if(this.isConnected) {
return;
}
// браузер вызывает этот метод при удалении элемента из документа
// (может вызываться много раз, если элемент многократно добавляется/удаляется)
if(this.onTypeDisconnect) {

View File

@ -134,7 +134,9 @@ export default class ChatBubbles {
this.log = this.chat.log;
this.bubbleGroups = new BubbleGroups(this.chat);
this.preloader = new ProgressivePreloader(null, false);
this.preloader = new ProgressivePreloader({
cancelable: false
});
this.lazyLoadQueue = new LazyLoadQueue();
this.lazyLoadQueue.queueId = ++queueId;
@ -166,7 +168,7 @@ export default class ChatBubbles {
this.listenerSetter.add(rootScope, 'dialog_flush', (e) => {
let peerId: number = e.peerId;
if(this.peerId == peerId) {
if(this.peerId === peerId) {
this.deleteMessagesByIds(Object.keys(this.bubbles).map(m => +m));
}
});
@ -191,7 +193,7 @@ export default class ChatBubbles {
/////this.log('message_sent', bubble);
if(message.media?.document && !message.media.document.type) {
const div = bubble.querySelector(`.document-container[data-mid="${tempId}"] .document`) as AudioElement;
const div = bubble.querySelector(`.document-container[data-mid="${tempId}"] .document`);
if(div) {
div.replaceWith(wrapDocument({message}));
}
@ -219,6 +221,7 @@ export default class ChatBubbles {
audio.setAttribute('doc-id', message.media.document.id);
audio.setAttribute('message-id', '' + mid);
audio.message = message;
audio.onLoad(true);
}
}
@ -279,7 +282,7 @@ export default class ChatBubbles {
fastRaf(() => {
const {storage, peerId, mid} = e;
if(peerId != this.peerId || storage !== this.chat.getMessagesStorage()) return;
if(peerId !== this.peerId || storage !== this.chat.getMessagesStorage()) return;
const mounted = this.getMountedBubble(mid);
if(!mounted) return;
@ -298,7 +301,7 @@ export default class ChatBubbles {
fastRaf(() => {
const {peerId, groupId, deletedMids} = e;
if(peerId != this.peerId) return;
if(peerId !== this.peerId) return;
const mids = this.appMessagesManager.getMidsByAlbum(groupId);
const renderedId = mids.concat(deletedMids).find(mid => this.bubbles[mid]);
if(!renderedId) return;
@ -345,7 +348,7 @@ export default class ChatBubbles {
});
this.listenerSetter.add(rootScope, 'dialog_drop', (e) => {
if(e.peerId == this.peerId) {
if(e.peerId === this.peerId) {
this.chat.appImManager.setPeer(0);
}
});
@ -356,17 +359,17 @@ export default class ChatBubbles {
this.listenerSetter.add(this.bubblesContainer, 'dblclick', (e) => {
const bubble = findUpClassName(e.target, 'grouped-item') || findUpClassName(e.target, 'bubble');
if(bubble) {
const mid = +bubble.dataset.mid;
const mid = +bubble.dataset.mid
this.log('debug message:', this.chat.getMessage(mid));
this.chat.bubbles.highlightBubble(bubble);
}
});
}
this.stickyIntersector = new StickyIntersector(this.scrollable.container, (stuck, target) => {
/* if(false) */this.stickyIntersector = new StickyIntersector(this.scrollable.container, (stuck, target) => {
for(const timestamp in this.dateMessages) {
const dateMessage = this.dateMessages[timestamp];
if(dateMessage.container == target) {
if(dateMessage.container === target) {
dateMessage.div.classList.toggle('is-sticky', stuck);
break;
}
@ -459,34 +462,34 @@ export default class ChatBubbles {
const mid = +target.dataset.mid;
readed.push(mid);
this.unreadedObserver.unobserve(target);
this.unreaded.findAndSplice(id => id == mid);
}
});
if(readed.length) {
const max = Math.max(...readed);
let maxId = Math.max(...readed);
if(this.scrolledAllDown) {
const bubblesMaxId = Math.max(...Object.keys(this.bubbles).map(i => +i));
if(maxId >= bubblesMaxId) {
maxId = this.appMessagesManager.getHistoryStorage(this.peerId, this.chat.threadId).maxId || maxId;
}
}
let length = readed.length;
for(let i = this.unreaded.length - 1; i >= 0; --i) {
const mid = this.unreaded[i];
if(mid < max) {
if(this.unreaded[i] <= maxId) {
length++;
this.unreaded.splice(i, 1);
}
}
if(DEBUG) {
this.log('will readHistory by ids:', max, length);
this.log('will readHistory by ids:', maxId, length);
}
/* if(this.peerId < 0) {
max = appMessagesIDsManager.getMessageIdInfo(max)[0];
} */
//appMessagesManager.readMessages(readed);
/* false && */ this.appMessagesManager.readHistory(this.peerId, max, this.chat.threadId).catch((err: any) => {
/* false && */ this.appMessagesManager.readHistory(this.peerId, maxId, this.chat.threadId).catch((err: any) => {
this.log.error('readHistory err:', err);
this.appMessagesManager.readHistory(this.peerId, max, this.chat.threadId);
this.appMessagesManager.readHistory(this.peerId, maxId, this.chat.threadId);
});
}
});
@ -856,9 +859,9 @@ export default class ChatBubbles {
if(!history.length) return;
if(top && !this.scrolledAll) {
if(DEBUG) {
/* if(DEBUG) {
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);
@ -874,9 +877,9 @@ export default class ChatBubbles {
// if scroll down after search
if(!top && history.indexOf(historyStorage.maxId) === -1/* && this.chat.type == 'chat' */) {
if(DEBUG) {
/* if(DEBUG) {
this.log('Will load more (down) history by maxId:', history[history.length - 1], history);
}
} */
/* false && */this.getHistory(history[history.length - 1], false, true, undefined, justLoad);
}
@ -902,7 +905,7 @@ export default class ChatBubbles {
}, 1350);
}
if(this.scrollable.isScrolledDown && this.scrolledAllDown) {
if(this.scrollable.getDistanceToEnd() < 300 && this.scrolledAllDown) {
this.bubblesContainer.classList.add('scrolled-down');
this.scrolledDown = true;
} else if(this.bubblesContainer.classList.contains('scrolled-down')) {
@ -1045,6 +1048,16 @@ export default class ChatBubbles {
}
}
public scrollToBubble(
element: HTMLElement,
position: ScrollLogicalPosition,
forceDirection?: FocusDirection,
forceDuration?: number
) {
// * 4 = .25rem
return this.scrollable.scrollIntoViewNew(element, position, 4, undefined, forceDirection, forceDuration);
}
public scrollToNewLastBubble() {
const bubble = this.chatInner.lastElementChild.lastElementChild as HTMLElement;
@ -1054,7 +1067,7 @@ export default class ChatBubbles {
if(bubble) {
this.scrollingToNewBubble = bubble;
this.scrollable.scrollIntoViewNew(bubble, 'end').then(() => {
this.scrollToBubble(bubble, 'end').then(() => {
this.scrollingToNewBubble = null;
});
}
@ -1223,10 +1236,11 @@ export default class ChatBubbles {
topMessage = 0;
}
let readMaxId = 0;
if(!isTarget && topMessage) {
const isUnread = this.appMessagesManager.isHistoryUnread(peerId, this.chat.threadId);
if(/* dialog.unread_count */isUnread && !samePeer) {
lastMsgId = historyStorage.readMaxId;
readMaxId = this.appMessagesManager.getReadMaxIdIfUnread(peerId, this.chat.threadId);
if(/* dialog.unread_count */readMaxId && !samePeer) {
lastMsgId = readMaxId;
} else {
lastMsgId = topMessage;
//lastMsgID = topMessage;
@ -1239,7 +1253,7 @@ export default class ChatBubbles {
const mounted = this.getMountedBubble(lastMsgId);
if(mounted) {
if(isTarget) {
this.scrollable.scrollIntoViewNew(mounted.bubble, 'center');
this.scrollToBubble(mounted.bubble, 'center');
this.highlightBubble(mounted.bubble);
this.chat.setListenerResult('setPeer', lastMsgId, false);
} else if(topMessage && !isJump) {
@ -1337,23 +1351,23 @@ export default class ChatBubbles {
//if(dialog && lastMsgID && lastMsgID != topMessage && (this.bubbles[lastMsgID] || this.firstUnreadBubble)) {
if((topMessage && isJump) || isTarget) {
const fromUp = maxBubbleId > 0 && (maxBubbleId < lastMsgId || lastMsgId < 0);
const forwardingUnread = historyStorage.readMaxId === lastMsgId && !isTarget;
if(!fromUp && (samePeer || forwardingUnread)) {
const followingUnread = readMaxId === lastMsgId && !isTarget;
if(!fromUp && samePeer) {
this.scrollable.scrollTop = this.scrollable.scrollHeight;
} else if(fromUp/* && (samePeer || forwardingUnread) */) {
this.scrollable.scrollTop = 0;
}
const mountedByLastMsgId = this.getMountedBubble(lastMsgId);
let bubble: HTMLElement = (forwardingUnread && this.firstUnreadBubble) || mountedByLastMsgId?.bubble;
let bubble: HTMLElement = (followingUnread && this.firstUnreadBubble) || mountedByLastMsgId?.bubble;
if(!bubble?.parentElement) {
bubble = this.findNextMountedBubbleByMsgId(lastMsgId);
}
// ! sometimes there can be no bubble
if(bubble) {
this.scrollable.scrollIntoViewNew(bubble, forwardingUnread ? 'start' : 'center', undefined, undefined, !samePeer ? FocusDirection.Static : undefined);
if(!forwardingUnread) {
this.scrollToBubble(bubble, followingUnread ? 'start' : 'center', !samePeer ? FocusDirection.Static : undefined);
if(!followingUnread) {
this.highlightBubble(bubble);
}
}
@ -1372,7 +1386,7 @@ export default class ChatBubbles {
//if(!this.unreaded.length && dialog) { // lol
if(this.scrolledAllDown && topMessage) { // lol
this.appMessagesManager.readHistory(peerId, topMessage, this.chat.threadId);
this.onScrolledAllDown();
}
if(this.chat.type === 'chat') {
@ -1394,6 +1408,13 @@ export default class ChatBubbles {
return {cached, promise: setPeerPromise};
}
public onScrolledAllDown() {
if(this.chat.type === 'chat' || this.chat.type === 'discussion') {
const storage = this.appMessagesManager.getHistoryStorage(this.peerId, this.chat.threadId);
this.appMessagesManager.readHistory(this.peerId, storage.maxId, this.chat.threadId, true);
}
}
public finishPeerChange() {
const peerId = this.peerId;
const isChannel = this.appPeersManager.isChannel(peerId);
@ -1435,7 +1456,7 @@ export default class ChatBubbles {
// * если добавить этот промис - в таком случае нужно сделать, чтобы скроллило к последнему сообщению после рендера
// promises.push(getHeavyAnimationPromise());
//this.log('promises to call', promises, queue);
this.log('promises to call', promises, queue, this.isHeavyAnimationInProgress);
const middleware = this.getMiddleware();
Promise.all(promises).then(() => {
if(!middleware()) {
@ -1458,6 +1479,8 @@ export default class ChatBubbles {
if(this.messagesQueue.length) {
this.setMessagesQueuePromise();
}
this.setUnreadDelimiter(); // не нашёл места лучше
}).catch(reject);
}, 0);
});
@ -1561,13 +1584,14 @@ export default class ChatBubbles {
}
}
} else {
const save = ['is-highlighted', 'zoom-fade'];
const save = ['is-highlighted'];
const wasClassNames = bubble.className.split(' ');
const classNames = ['bubble'].concat(save.filter(c => wasClassNames.includes(c)));
bubble.className = classNames.join(' ');
contentWrapper = bubble.lastElementChild as HTMLElement;
bubbleContainer = contentWrapper.firstElementChild as HTMLDivElement;
bubbleContainer.innerHTML = '';
contentWrapper.innerHTML = '';
contentWrapper.appendChild(bubbleContainer);
//bubbleContainer.style.marginBottom = '';
@ -2277,12 +2301,16 @@ export default class ChatBubbles {
}
if(withReplies) {
MessageRender.renderReplies({
const isFooter = MessageRender.renderReplies({
bubble,
bubbleContainer,
message: messageWithReplies,
messageDiv
});
if(isFooter) {
canHaveTail = true;
}
}
if(canHaveTail) {
@ -2398,6 +2426,10 @@ export default class ChatBubbles {
//this.scrollable.scrollTop = this.scrollable.scrollHeight;
isTouchSupported && isApple && (this.scrollable.container.style.overflow = '');
this.scrollable.container.style.display = 'none';
void this.scrollable.container.offsetLeft; // reflow
this.scrollable.container.style.display = '';
/* if(DEBUG) {
this.log('performHistoryResult: have set up scrollTop:', newScrollTop, this.scrollable.scrollTop, this.isHeavyAnimationInProgress);
} */
@ -2408,6 +2440,7 @@ export default class ChatBubbles {
}).then(() => {
//console.timeEnd('appImManager render history');
//return new Promise<boolean>((resolve) => setTimeout(() => resolve(true), 300));
return true;
});
}
@ -2599,7 +2632,7 @@ export default class ChatBubbles {
const waitPromise = isAdditionRender ? processPromise(resultPromise) : promise;
if(isFirstMessageRender) {
if(isFirstMessageRender/* && false */) {
waitPromise.then(() => {
if(rootScope.settings.animationsEnabled && Object.keys(this.bubbles).length) {
let sortedMids = getObjectKeysAndSort(this.bubbles, 'desc');
@ -2623,9 +2656,9 @@ export default class ChatBubbles {
const middleIds = isAdditionRender ? [] : [targetMid];
const bottomIds = sortedMids.slice(0, sortedMids.findIndex(mid => targetMid >= mid)).reverse();
/* this.log('getHistory: targeting mid:', targetMid,
this.log('getHistory: targeting mid:', targetMid, maxId, additionMsgId,
topIds.map(m => this.appMessagesManager.getLocalMessageId(m)),
bottomIds.map(m => this.appMessagesManager.getLocalMessageId(m))); */
bottomIds.map(m => this.appMessagesManager.getLocalMessageId(m)));
const delay = isAdditionRender ? 10 : 40;
const offsetIndex = isAdditionRender ? 0 : 1;
@ -2633,6 +2666,11 @@ export default class ChatBubbles {
const animationPromise = deferredPromise<void>();
let lastMsDelay = 0;
mids.forEach((mid, idx) => {
if(!this.bubbles[mid]) {
this.log.warn('animateAsLadder: no bubble by mid:', mid);
return;
}
const contentWrapper = this.bubbles[mid].lastElementChild as HTMLElement;
lastMsDelay = ((idx + offsetIndex) || 0.1) * delay;
@ -2668,7 +2706,7 @@ export default class ChatBubbles {
const delays: number[] = [topRes.lastMsDelay, middleRes.lastMsDelay, bottomRes.lastMsDelay];
if(topIds.length || middleIds.length || bottomIds.length) {
dispatchHeavyAnimationEvent(Promise.all(promises), Math.max(...delays));
dispatchHeavyAnimationEvent(Promise.all(promises), Math.max(...delays) + 200); // * 200 - transition time
}
}
@ -2687,7 +2725,7 @@ export default class ChatBubbles {
return null;
}
/* false && */!isFirstMessageRender && promise.then(() => {
/* false && */!isFirstMessageRender && false && promise.then(() => {
if(reverse) {
this.loadedTopTimes++;
this.loadedBottomTimes = Math.max(0, --this.loadedBottomTimes);
@ -2727,9 +2765,9 @@ export default class ChatBubbles {
this.deleteMessagesByIds(ids, false);
}
});
this.setUnreadDelimiter(); // не нашёл места лучше
promise.then(() => {
// preload more
//if(!isFirstMessageRender) {
if(this.chat.type === 'chat'/* || this.chat.type === 'discussion' */) {
@ -2757,20 +2795,23 @@ export default class ChatBubbles {
}
const historyStorage = this.appMessagesManager.getHistoryStorage(this.peerId, this.chat.threadId);
const isUnread = this.appMessagesManager.isHistoryUnread(this.peerId, this.chat.threadId);
if(!isUnread) return;
let readMaxId = this.appMessagesManager.getReadMaxIdIfUnread(this.peerId, this.chat.threadId);
if(!readMaxId) return;
let maxId = historyStorage.readMaxId;
maxId = Object.keys(this.bubbles).filter(mid => !this.bubbles[mid].classList.contains('is-out')).map(i => +i).sort((a, b) => a - b).find(i => i > maxId);
readMaxId = Object.keys(this.bubbles)
.filter(mid => !this.bubbles[mid].classList.contains('is-out'))
.map(i => +i)
.sort((a, b) => a - b)
.find(i => i > readMaxId);
if(maxId && this.bubbles[maxId]) {
let bubble = this.bubbles[maxId];
if(readMaxId && this.bubbles[readMaxId]) {
let bubble = this.bubbles[readMaxId];
if(this.firstUnreadBubble && this.firstUnreadBubble != bubble) {
this.firstUnreadBubble.classList.remove('is-first-unread');
this.firstUnreadBubble = null;
}
if(maxId !== historyStorage.maxId) {
if(readMaxId !== historyStorage.maxId) {
bubble.classList.add('is-first-unread');
}

View File

@ -231,7 +231,7 @@ export default class ChatContextMenu {
icon: 'forward',
text: 'Forward',
onClick: this.onForwardClick,
verify: () => this.chat.type !== 'scheduled' && !this.message.pFlags.is_outgoing
verify: () => this.chat.type !== 'scheduled' && !this.message.pFlags.is_outgoing && this.message._ !== 'messageService'
}, {
icon: 'forward',
text: 'Forward selected',
@ -283,13 +283,7 @@ export default class ChatContextMenu {
};
private onReplyClick = () => {
const message = this.chat.getMessage(this.mid);
const chatInputC = this.chat.input;
const f = () => {
chatInputC.setTopInfo('reply', f, this.appPeersManager.getPeerTitle(message.fromId, true), message.message, undefined, message);
chatInputC.replyToMsgId = this.mid;
};
f();
this.chat.input.initMessageReply(this.mid);
};
private onEditClick = () => {

View File

@ -48,7 +48,6 @@ export default class ChatInput {
public messageInputField: InputField;
public fileInput: HTMLInputElement;
public inputMessageContainer: HTMLDivElement;
public inputScroll: Scrollable;
public btnSend = document.getElementById('btn-send') as HTMLButtonElement;
public btnCancelRecord: HTMLButtonElement;
public lastUrl = '';
@ -169,8 +168,6 @@ export default class ChatInput {
this.inputMessageContainer = document.createElement('div');
this.inputMessageContainer.classList.add('input-message-container');
this.inputScroll = new Scrollable(this.inputMessageContainer);
if(this.chat.type === 'chat') {
this.btnScheduled = ButtonIcon('scheduled', {noRipple: true});
this.btnScheduled.classList.add('btn-scheduled', 'hide');
@ -497,7 +494,7 @@ export default class ChatInput {
const str = getRichValue(this.messageInputField.input, entities);
let draft: DraftMessage.draftMessage;
if(str.length) {
if(str.length || this.replyToMsgId) {
draft = {
_: 'draftMessage',
date: tsNow(true) + this.serverTimeManager.serverTimeOffset,
@ -530,6 +527,8 @@ export default class ChatInput {
cancelSelection();
this.lastTimeType = 0;
if(this.messageInput) {
this.clearInput();
helperToo && this.clearHelper();
@ -547,6 +546,11 @@ export default class ChatInput {
}
}
this.noWebPage = draft.pFlags.no_webpage;
if(draft.reply_to_msg_id) {
this.initMessageReply(draft.reply_to_msg_id);
}
this.setInputValue(draft.rMessage, fromUpdate, fromUpdate);
return true;
}
@ -555,7 +559,7 @@ export default class ChatInput {
const peerId = this.chat.peerId;
this.chatInput.style.display = '';
const isBroadcast = this.appPeersManager.isBroadcast(peerId);
this.goDownBtn.classList.toggle('is-broadcast', isBroadcast);
this.goDownBtn.classList.remove('hide');
@ -600,6 +604,10 @@ export default class ChatInput {
} else {
this.messageInput.setAttribute('contenteditable', 'true');
this.setDraft(undefined, false);
if(!this.messageInput.innerHTML) {
this.messageInputField.onFakeInput();
}
}
this.attachMenu.toggleAttribute('disabled', !visible.length);
@ -610,21 +618,23 @@ export default class ChatInput {
}
private attachMessageInputField() {
const oldInput = this.messageInputField?.input;
const oldInputField = this.messageInputField;
this.messageInputField = new InputField({
placeholder: 'Message',
name: 'message'
name: 'message',
animate: true
});
this.messageInputField.input.className = 'input-message-input';
this.messageInputField.input.classList.replace('input-field-input', 'input-message-input');
this.messageInputField.inputFake.classList.replace('input-field-input', 'input-message-input');
this.messageInput = this.messageInputField.input;
this.attachMessageInputListeners();
const container = this.inputScroll.container;
if(oldInput) {
oldInput.replaceWith(this.messageInputField.input);
if(oldInputField) {
oldInputField.input.replaceWith(this.messageInputField.input);
oldInputField.inputFake.replaceWith(this.messageInputField.inputFake);
} else {
container.append(this.messageInputField.input);
this.inputMessageContainer.append(this.messageInputField.input, this.messageInputField.inputFake);
}
}
@ -674,14 +684,14 @@ export default class ChatInput {
cancelEvent(e); // cancel legacy event
let html = this.messageInput.innerHTML;
if(html && html != needHTML) {
if(html && html !== needHTML) {
this.lockRedo = true;
let sameHTMLTimes = 0;
do {
document.execCommand(type, false, null);
const currentHTML = this.messageInput.innerHTML;
if(html == currentHTML) {
if(html === currentHTML) {
if(++sameHTMLTimes > 2) { // * unlink, removeFormat (а может и нет, случай: заболдить подчёркнутый текст (выделить ровно его), попробовать отменить)
break;
}
@ -690,7 +700,7 @@ export default class ChatInput {
}
html = currentHTML;
} while(html != needHTML);
} while(html !== needHTML);
this.lockRedo = false;
}
@ -823,21 +833,22 @@ export default class ChatInput {
//return;
if(e.code == 'KeyZ') {
const html = this.messageInput.innerHTML;
let html = this.messageInput.innerHTML;
if(e.shiftKey) {
if(this.undoHistory.length) {
this.executedHistory.push(this.messageInput.innerHTML);
const html = this.undoHistory.pop();
this.executedHistory.push(html);
html = this.undoHistory.pop();
this.undoRedo(e, 'redo', html);
this.canRedoFromHTML = this.undoHistory.length ? this.messageInput.innerHTML : '';
this.canUndoFromHTML = this.messageInput.innerHTML;
html = this.messageInput.innerHTML;
this.canRedoFromHTML = this.undoHistory.length ? html : '';
this.canUndoFromHTML = html;
}
} else {
// * подождём, когда пользователь сам восстановит поле до нужного состояния, которое стало сразу после saveExecuted
if(this.executedHistory.length && (!this.canUndoFromHTML || html == this.canUndoFromHTML)) {
this.undoHistory.push(this.messageInput.innerHTML);
const html = this.executedHistory.pop();
this.undoHistory.push(html);
html = this.executedHistory.pop();
this.undoRedo(e, 'undo', html);
// * поставим новое состояние чтобы снова подождать, если пользователь изменит что-то, и потом попробует откатить до предыдущего состояния
@ -875,9 +886,9 @@ export default class ChatInput {
rootScope.settings.stickers.suggest &&
(this.chat.peerId > 0 || this.appChatsManager.hasRights(this.chat.peerId, 'send', 'send_stickers'))) {
let emoticon = '';
if(entities.length && entities[0]._ == 'messageEntityEmoji') {
if(entities.length && entities[0]._ === 'messageEntityEmoji') {
const entity = entities[0];
if(entity.length == richValue.length && !entity.offset) {
if(entity.length === richValue.length && !entity.offset) {
emoticon = richValue;
}
}
@ -890,12 +901,12 @@ export default class ChatInput {
}
const html = this.messageInput.innerHTML;
if(this.canRedoFromHTML && html != this.canRedoFromHTML && !this.lockRedo) {
if(this.canRedoFromHTML && html !== this.canRedoFromHTML && !this.lockRedo) {
this.canRedoFromHTML = '';
this.undoHistory.length = 0;
}
const urlEntities: Array<MessageEntity.messageEntityUrl | MessageEntity.messageEntityTextUrl> = entities.filter(e => e._ == 'messageEntityUrl' || e._ == 'messageEntityTextUrl') as any;
const urlEntities: Array<MessageEntity.messageEntityUrl | MessageEntity.messageEntityTextUrl> = entities.filter(e => e._ === 'messageEntityUrl' || e._ === 'messageEntityTextUrl') as any;
if(urlEntities.length) {
for(const entity of urlEntities) {
let url: string;
@ -946,7 +957,9 @@ export default class ChatInput {
}
if(this.isInputEmpty()) {
this.appMessagesManager.setTyping(this.chat.peerId, 'sendMessageCancelAction');
if(this.lastTimeType) {
this.appMessagesManager.setTyping(this.chat.peerId, 'sendMessageCancelAction');
}
} else {
const time = Date.now();
if(time - this.lastTimeType >= 6000) {
@ -1083,7 +1096,7 @@ export default class ChatInput {
cancelEvent(e);
if(!findUpClassName(e.target, 'reply-wrapper')) return;
if(this.helperType == 'forward') {
if(this.helperType === 'forward') {
if(this.helperWaitingForward) return;
this.helperWaitingForward = true;
@ -1101,18 +1114,20 @@ export default class ChatInput {
helperFunc();
}
});
} else if(this.helperType == 'reply') {
} else if(this.helperType === 'reply') {
this.chat.setMessageId(this.replyToMsgId);
} else if(this.helperType == 'edit') {
} else if(this.helperType === 'edit') {
this.chat.setMessageId(this.editMsgId);
}
};
public clearInput(canSetDraft = true) {
this.messageInputField.value = '';
if(isTouchSupported) {
this.messageInput.innerText = '';
//this.messageInput.innerText = '';
} else {
this.attachMessageInputField();
//this.attachMessageInputField();
//this.messageInput.innerText = '';
// clear executions
this.canRedoFromHTML = '';
@ -1126,9 +1141,9 @@ export default class ChatInput {
set = this.setDraft(undefined, false);
}
if(!set) {
/* if(!set) {
this.onMessageInput();
}
} */
}
public isInputEmpty() {
@ -1138,13 +1153,19 @@ export default class ChatInput {
public updateSendBtn() {
let icon: 'send' | 'record' | 'edit' | 'schedule';
const isInputEmpty = this.isInputEmpty();
if(this.editMsgId) icon = 'edit';
else if(!this.recorder || this.recording || !this.isInputEmpty() || this.forwardingMids.length) icon = this.chat.type === 'scheduled' ? 'schedule' : 'send';
else if(!this.recorder || this.recording || !isInputEmpty || this.forwardingMids.length) icon = this.chat.type === 'scheduled' ? 'schedule' : 'send';
else icon = 'record';
['send', 'record', 'edit', 'schedule'].forEach(i => {
this.btnSend.classList.toggle(i, icon === i);
});
if(this.btnScheduled) {
this.btnScheduled.classList.toggle('show', isInputEmpty);
}
}
public onMessageSent(clearInput = true, clearReply?: boolean) {
@ -1218,7 +1239,7 @@ export default class ChatInput {
this.onMessageSent();
}
public sendMessageWithDocument(document: MyDocument | string, force = false) {
public sendMessageWithDocument(document: MyDocument | string, force = false, clearDraft = false) {
document = this.appDocsManager.getDoc(document);
const flag = document.type === 'sticker' ? 'send_stickers' : (document.type === 'gif' ? 'send_gifs' : 'send_media');
@ -1238,9 +1259,10 @@ export default class ChatInput {
replyToMsgId: this.replyToMsgId,
threadId: this.chat.threadId,
silent: this.sendSilent,
scheduleDate: this.scheduleDate
scheduleDate: this.scheduleDate,
clearDraft: clearDraft || undefined
});
this.onMessageSent(false, true);
this.onMessageSent(clearDraft, true);
if(document.type == 'sticker') {
emoticonsDropdown.stickersTab?.pushRecentSticker(document);
@ -1321,8 +1343,17 @@ export default class ChatInput {
f();
}
public initMessageReply(mid: number) {
const message = this.chat.getMessage(mid);
const f = () => {
this.setTopInfo('reply', f, this.appPeersManager.getPeerTitle(message.fromId, true), message.message, undefined, message);
this.replyToMsgId = mid;
};
f();
}
public clearHelper(type?: ChatInputHelperType) {
if(this.helperType == 'edit' && type != 'edit') {
if(this.helperType === 'edit' && type !== 'edit') {
this.clearInput();
}
@ -1342,22 +1373,21 @@ export default class ChatInput {
public setInputValue(value: string, clear = true, focus = true) {
clear && this.clearInput();
this.messageInput.innerHTML = value || '';
this.onMessageInput();
this.messageInputField.value = value || '';
window.requestAnimationFrame(() => {
focus && placeCaretAtEnd(this.messageInput);
this.inputScroll.scrollTop = this.inputScroll.scrollHeight;
this.messageInput.scrollTop = this.messageInput.scrollHeight;
});
}
public setTopInfo(type: ChatInputHelperType, callerFunc: () => void, title = '', subtitle = '', input?: string, message?: any) {
if(type != 'webpage') {
if(type !== 'webpage') {
this.clearHelper(type);
this.helperType = type;
this.helperFunc = callerFunc;
}
if(this.replyElements.container.lastElementChild.tagName == 'DIV') {
if(this.replyElements.container.lastElementChild.tagName === 'DIV') {
this.replyElements.container.lastElementChild.remove();
this.replyElements.container.append(wrapReply(title, subtitle, message));
}

View File

@ -69,5 +69,6 @@ export namespace MessageRender {
repliesFooter.message = message;
repliesFooter.type = isFooter ? 'footer' : 'beside';
bubbleContainer.prepend(repliesFooter);
return isFooter;
};
}

View File

@ -35,13 +35,12 @@ export default class RepliesElement extends HTMLElement {
const replies = this.message.replies;
if(this.type === 'footer') {
let leftHTML = '', lastStyle = '';
let leftHTML = '';
if(replies?.recent_repliers) {
leftHTML += '<div class="replies-footer-avatars">'
let l: string[] = [];
replies.recent_repliers/* .slice().reverse() */.forEach((peer, idx) => {
lastStyle = idx == 0 ? '' : `style="transform: translateX(-${idx * 14}px);"`;
l.push(`<avatar-element class="avatar-34" dialog="0" peer="${appPeersManager.getPeerId(peer)}" ${lastStyle}></avatar-element>`);
replies.recent_repliers/* .slice().reverse() */.forEach((peer) => {
l.push(`<avatar-element class="avatar-34" dialog="0" peer="${appPeersManager.getPeerId(peer)}"></avatar-element>`);
});
leftHTML += l.reverse().join('') + '</div>';
} else {
@ -64,7 +63,7 @@ export default class RepliesElement extends HTMLElement {
this.classList.toggle('is-unread', replies.read_max_id < replies.max_id && (!historyStorage.readMaxId || historyStorage.readMaxId < replies.max_id));
}
this.innerHTML = `${leftHTML}<span class="replies-footer-text" ${lastStyle}>${text}</span><span class="tgico-next"></span>`;
this.innerHTML = `${leftHTML}<span class="replies-footer-text">${text}</span><span class="tgico-next"></span>`;
const rippleContainer = document.createElement('div');
this.append(rippleContainer);

View File

@ -75,8 +75,7 @@ export default class StickersHelper {
return;
}
appImManager.chat.input.clearInput();
EmoticonsDropdown.onMediaClick(e);
EmoticonsDropdown.onMediaClick(e, true);
});
this.container.append(this.stickersContainer);

View File

@ -362,7 +362,7 @@ export class EmoticonsDropdown {
return stickyIntersector;
};
public static onMediaClick = (e: MouseEvent) => {
public static onMediaClick = (e: MouseEvent, clearDraft = false) => {
let target = e.target as HTMLElement;
target = findUpTag(target, 'DIV');
@ -371,7 +371,7 @@ export class EmoticonsDropdown {
const fileId = target.dataset.docId;
if(!fileId) return;
if(appImManager.chat.input.sendMessageWithDocument(fileId)) {
if(appImManager.chat.input.sendMessageWithDocument(fileId, undefined, clearDraft)) {
/* dropdown.classList.remove('active');
toggleEl.classList.remove('active'); */
emoticonsDropdown.forceClose = true;

View File

@ -103,6 +103,8 @@ export default class EmojiTab implements EmoticonsTab {
this.appendEmoji(emoji, this.recentItemsDiv);
}
this.recentItemsDiv.parentElement.classList.toggle('hide', !this.recent.length);
categories.unshift('Recent');
categories.map(category => {
const div = divs[category];
@ -202,12 +204,14 @@ export default class EmojiTab implements EmoticonsTab {
const scrollHeight = this.recentItemsDiv.scrollHeight;
this.appendEmoji(emoji, this.recentItemsDiv, true);
this.recent.findAndSplice(e => e == emoji);
this.recent.findAndSplice(e => e === emoji);
this.recent.unshift(emoji);
if(this.recent.length > 36) {
this.recent.length = 36;
}
this.recentItemsDiv.parentElement.classList.toggle('hide', !this.recent.length);
appStateManager.pushToState('recentEmoji', this.recent);
// Append to input

View File

@ -72,7 +72,7 @@ export class SuperStickerRenderer {
const size = mediaSizes.active.esgSticker.width;
console.log('processVisibleDiv:', div);
//console.log('processVisibleDiv:', div);
const promise = wrapSticker({
doc,

View File

@ -1,4 +1,4 @@
import { findUpTag, whichChild } from "../helpers/dom";
import { findUpTag, whichChild, findUpAsChild } from "../helpers/dom";
import { TransitionSlider } from "./transition";
import { ScrollableX } from "./scrollable";
import rootScope from "../lib/rootScope";
@ -6,7 +6,7 @@ import { fastRaf } from "../helpers/schedulers";
import { FocusDirection } from "../helpers/fastSmoothScroll";
export function horizontalMenu(tabs: HTMLElement, content: HTMLElement, onClick?: (id: number, tabContent: HTMLDivElement) => void, onTransitionEnd?: () => void, transitionTime = 250, scrollableX?: ScrollableX) {
const selectTab = TransitionSlider(content, tabs || content.dataset.slider == 'tabs' ? 'tabs' : 'navigation', transitionTime, onTransitionEnd);
const selectTab = TransitionSlider(content, tabs || content.dataset.animation == 'tabs' ? 'tabs' : 'navigation', transitionTime, onTransitionEnd);
if(tabs) {
const proxy = new Proxy(selectTab, {
@ -77,13 +77,12 @@ export function horizontalMenu(tabs: HTMLElement, content: HTMLElement, onClick?
const useStripe = !tabs.classList.contains('no-stripe');
const tagName = tabs.classList.contains('menu-horizontal-div') ? 'BUTTON' : 'LI';//tabs.firstElementChild.tagName;
//const tagName = tabs.classList.contains('menu-horizontal-div') ? 'BUTTON' : 'LI';
const tagName = tabs.firstElementChild.tagName;
tabs.addEventListener('click', function(e) {
let target = e.target as HTMLElement;
if(target.tagName != tagName) {
target = findUpTag(target, tagName);
}
target = findUpAsChild(target, tabs);
//console.log('tabs click:', target);

View File

@ -53,6 +53,7 @@ const checkAndSetRTL = (input: HTMLElement) => {
class InputField {
public container: HTMLElement;
public input: HTMLElement;
public inputFake: HTMLElement;
//public onLengthChange: (length: number, isOverflow: boolean) => void;
@ -62,7 +63,8 @@ class InputField {
name?: string,
maxLength?: number,
showLengthOn?: number,
plainText?: true
plainText?: true,
animate?: true
} = {}) {
this.container = document.createElement('div');
this.container.classList.add('input-field');
@ -98,10 +100,22 @@ class InputField {
if(isInputEmpty(input)) {
input.innerHTML = '';
}
if(this.inputFake) {
this.inputFake.innerHTML = input.innerHTML;
this.onFakeInput();
}
});
// ! childList for paste first symbol
observer.observe(input, {characterData: true, childList: true, subtree: true});
if(options.animate) {
input.classList.add('scrollable', 'scrollable-y')
this.inputFake = document.createElement('div');
this.inputFake.setAttribute('contenteditable', 'true');
this.inputFake.className = input.className + ' input-field-input-fake';
}
} else {
this.container.innerHTML = `
<input type="text" ${name ? `name="${name}"` : ''} ${placeholder ? `placeholder="${placeholder}"` : ''} autocomplete="off" ${label ? 'required=""' : ''} class="input-field-input">
@ -142,23 +156,36 @@ class InputField {
this.input = input;
}
public onFakeInput() {
const scrollHeight = this.inputFake.scrollHeight;
this.input.style.height = scrollHeight ? scrollHeight + 'px' : '';
}
get value() {
return this.options.plainText ? (this.input as HTMLInputElement).value : getRichValue(this.input);
//return getRichValue(this.input);
}
set value(value: string) {
this.setValueSilently(value);
this.setValueSilently(value, false);
const event = new Event('input', {bubbles: true, cancelable: true});
this.input.dispatchEvent(event);
}
public setValueSilently(value: string) {
public setValueSilently(value: string, fireFakeInput = true) {
if(this.options.plainText) {
(this.input as HTMLInputElement).value = value;
} else {
this.input.innerHTML = value;
if(this.inputFake) {
this.inputFake.innerHTML = value;
if(fireFakeInput) {
this.onFakeInput();
}
}
}
}
}

31
src/components/loader.ts Normal file
View File

@ -0,0 +1,31 @@
import { CancellablePromise } from "../helpers/cancellablePromise";
import ProgressivePreloader from "./preloader";
export default class Loader {
public preloader: ProgressivePreloader;
constructor(private options: {
dialogType?: 'contact' | 'private' | 'group' | 'channel',
getPromise: () => CancellablePromise<any>,
middleware: () => boolean,
appendTo: HTMLElement,
preloader?: ProgressivePreloader,
isUpload?: boolean
}) {
this.preloader = options.preloader || new ProgressivePreloader({
cancelable: false,
attachMethod: 'prepend'
});
}
public init() {
}
public load() {
}
}

View File

@ -4,6 +4,7 @@ import ListenerSetter from "../helpers/listenerSetter";
import mediaSizes from "../helpers/mediaSizes";
import { isTouchSupported } from "../helpers/touchSupport";
import { isApple } from "../helpers/userAgent";
import { MOUNT_CLASS_TO } from "../lib/mtproto/mtproto_config";
export const loadedURLs: {[url: string]: boolean} = {};
const set = (elem: HTMLElement | HTMLImageElement | SVGImageElement | HTMLVideoElement, url: string) => {
@ -63,6 +64,8 @@ export function putPreloader(elem: Element, returnDiv = false) {
elem.innerHTML += html;
}
MOUNT_CLASS_TO && (MOUNT_CLASS_TO.putPreloader = putPreloader);
let sortedCountries: Country[];
export function formatPhoneNumber(str: string) {
str = str.replace(/\D/g, '');

View File

@ -1,6 +1,7 @@
import rootScope from "../../lib/rootScope";
import { blurActiveElement, cancelEvent, findUpClassName } from "../../helpers/dom";
import { ripple } from "../ripple";
import animationIntersector from "../animationIntersector";
export type PopupOptions = Partial<{closable: true, overlayClosable: true, withConfirm: string, body: true}>;
export default class PopupElement {
@ -98,7 +99,7 @@ export default class PopupElement {
}
private _onKeyDown = (e: KeyboardEvent) => {
if(e.key == 'Escape' && this.onEscape()) {
if(e.key === 'Escape' && this.onEscape()) {
cancelEvent(e);
this.destroy();
}
@ -110,10 +111,12 @@ export default class PopupElement {
void this.element.offsetWidth; // reflow
this.element.classList.add('active');
rootScope.overlayIsActive = true;
animationIntersector.checkAnimations(true);
}
public destroy = () => {
this.onClose && this.onClose();
this.element.classList.add('hiding');
this.element.classList.remove('active');
window.removeEventListener('keydown', this._onKeyDown, {capture: true});
@ -123,7 +126,8 @@ export default class PopupElement {
setTimeout(() => {
this.element.remove();
this.onCloseAfterTimeout && this.onCloseAfterTimeout();
}, 1000);
animationIntersector.checkAnimations(false);
}, 150);
};
}

View File

@ -34,15 +34,10 @@ export default class PopupStickers extends PopupElement {
this.onClose = () => {
animationIntersector.setOnlyOnePlayableGroup('');
animationIntersector.checkAnimations(false);
this.stickersFooter.removeEventListener('click', this.onFooterClick);
this.stickersDiv.removeEventListener('click', this.onStickersClick);
};
this.onCloseAfterTimeout = () => {
animationIntersector.checkAnimations(undefined, ANIMATION_GROUP);
};
const div = document.createElement('div');
div.classList.add('sticker-set');
@ -98,7 +93,6 @@ export default class PopupStickers extends PopupElement {
this.set = set.set;
animationIntersector.checkAnimations(true);
animationIntersector.setOnlyOnePlayableGroup(ANIMATION_GROUP);
this.h6.innerHTML = RichTextProcessor.wrapEmojiText(set.set.title);

View File

@ -7,88 +7,163 @@ const TRANSITION_TIME = 200;
export default class ProgressivePreloader {
public preloader: HTMLDivElement;
private circle: SVGCircleElement;
private cancelSvg: SVGSVGElement;
private downloadSvg: HTMLElement;
private tempId = 0;
private detached = true;
private promise: CancellablePromise<any> = null;
public onCancel: () => any = null;
constructor(elem?: Element, private cancelable = true, streamable = false, private attachMethod: 'append' | 'prepend' = 'append') {
this.preloader = document.createElement('div');
this.preloader.classList.add('preloader-container');
public isUpload = false;
private cancelable = true;
private streamable = false;
private tryAgainOnFail = true;
private attachMethod: 'append' | 'prepend' = 'append';
if(streamable) {
this.preloader.classList.add('preloader-streamable');
private loadFunc: () => {download: CancellablePromise<any>};
constructor(options?: Partial<{
isUpload: ProgressivePreloader['isUpload'],
cancelable: ProgressivePreloader['cancelable'],
streamable: ProgressivePreloader['streamable'],
tryAgainOnFail: ProgressivePreloader['tryAgainOnFail'],
attachMethod: ProgressivePreloader['attachMethod']
}>) {
if(options) {
for(let i in options) {
// @ts-ignore
this[i] = options[i];
}
}
}
public constructContainer(options: Partial<{
color: 'transparent',
bold: boolean
}> = {}) {
if(!this.preloader) {
this.preloader = document.createElement('div');
this.preloader.classList.add('preloader-container');
if(options.color) {
this.preloader.classList.add('preloader-' + options.color);
}
if(options.bold) {
this.preloader.classList.add('preloader-bold');
}
if(this.streamable) {
this.preloader.classList.add('preloader-streamable');
}
}
}
public constructDownloadIcon() {
this.constructContainer();
}
public construct() {
this.construct = null;
this.constructContainer();
this.preloader.innerHTML = `
<div class="you-spin-me-round">
<svg xmlns="http://www.w3.org/2000/svg" class="preloader-circular" viewBox="${streamable ? '25 25 50 50' : '27 27 54 54'}">
<circle class="preloader-path-new" cx="${streamable ? '50' : '54'}" cy="${streamable ? '50' : '54'}" r="${streamable ? 19 : 24}" fill="none" stroke-miterlimit="10"/>
<svg xmlns="http://www.w3.org/2000/svg" class="preloader-circular" viewBox="${this.streamable ? '25 25 50 50' : '27 27 54 54'}">
<circle class="preloader-path-new" cx="${this.streamable ? '50' : '54'}" cy="${this.streamable ? '50' : '54'}" r="${this.streamable ? 19 : 24}" fill="none" stroke-miterlimit="10"/>
</svg>
</div>`;
if(cancelable) {
if(this.cancelable) {
this.preloader.innerHTML += `
<svg xmlns="http://www.w3.org/2000/svg" class="preloader-close" viewBox="0 0 20 20">
<line x1="0" y1="20" x2="20" y2="0" stroke-width="2" stroke-linecap="round"></line>
<line x1="0" y1="0" x2="20" y2="20" stroke-width="2" stroke-linecap="round"></line>
<svg xmlns="http://www.w3.org/2000/svg" class="preloader-close" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<polygon points="0 0 24 0 24 24 0 24"/>
<path fill="#000" fill-rule="nonzero" d="M5.20970461,5.38710056 L5.29289322,5.29289322 C5.65337718,4.93240926 6.22060824,4.90467972 6.61289944,5.20970461 L6.70710678,5.29289322 L12,10.585 L17.2928932,5.29289322 C17.6834175,4.90236893 18.3165825,4.90236893 18.7071068,5.29289322 C19.0976311,5.68341751 19.0976311,6.31658249 18.7071068,6.70710678 L13.415,12 L18.7071068,17.2928932 C19.0675907,17.6533772 19.0953203,18.2206082 18.7902954,18.6128994 L18.7071068,18.7071068 C18.3466228,19.0675907 17.7793918,19.0953203 17.3871006,18.7902954 L17.2928932,18.7071068 L12,13.415 L6.70710678,18.7071068 C6.31658249,19.0976311 5.68341751,19.0976311 5.29289322,18.7071068 C4.90236893,18.3165825 4.90236893,17.6834175 5.29289322,17.2928932 L10.585,12 L5.29289322,6.70710678 C4.93240926,6.34662282 4.90467972,5.77939176 5.20970461,5.38710056 L5.29289322,5.29289322 L5.20970461,5.38710056 Z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" class="preloader-download" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<polygon points="0 0 24 0 24 24 0 24"/>
<path fill="#000" fill-rule="nonzero" d="M5,19 L19,19 C19.5522847,19 20,19.4477153 20,20 C20,20.5128358 19.6139598,20.9355072 19.1166211,20.9932723 L19,21 L5,21 C4.44771525,21 4,20.5522847 4,20 C4,19.4871642 4.38604019,19.0644928 4.88337887,19.0067277 L5,19 L19,19 L5,19 Z M11.8833789,3.00672773 L12,3 C12.5128358,3 12.9355072,3.38604019 12.9932723,3.88337887 L13,4 L13,13.585 L16.2928932,10.2928932 C16.6533772,9.93240926 17.2206082,9.90467972 17.6128994,10.2097046 L17.7071068,10.2928932 C18.0675907,10.6533772 18.0953203,11.2206082 17.7902954,11.6128994 L17.7071068,11.7071068 L12.7071068,16.7071068 C12.3466228,17.0675907 11.7793918,17.0953203 11.3871006,16.7902954 L11.2928932,16.7071068 L6.29289322,11.7071068 C5.90236893,11.3165825 5.90236893,10.6834175 6.29289322,10.2928932 C6.65337718,9.93240926 7.22060824,9.90467972 7.61289944,10.2097046 L7.70710678,10.2928932 L11,13.585 L11,4 C11,3.48716416 11.3860402,3.06449284 11.8833789,3.00672773 L12,3 L11.8833789,3.00672773 Z"/>
</g>
</svg>`;
this.downloadSvg = this.preloader.lastElementChild as HTMLElement;
this.cancelSvg = this.downloadSvg.previousElementSibling as any;
} else {
this.preloader.classList.add('preloader-swing');
}
this.circle = this.preloader.firstElementChild.firstElementChild.firstElementChild as SVGCircleElement;
if(elem) {
this.attach(elem);
}
if(this.cancelable) {
attachClickEvent(this.preloader, (e) => {
cancelEvent(e);
if(this.promise && this.promise.cancel) {
this.promise.cancel();
this.setProgress(0);
setTimeout(() => {
this.detach();
}, 100);
}
});
attachClickEvent(this.preloader, this.onClick);
}
}
public onClick = (e?: Event) => {
if(e) {
cancelEvent(e);
}
if(this.preloader.classList.contains('manual')) {
if(this.loadFunc) {
this.loadFunc();
}
} else {
if(this.promise && this.promise.cancel) {
this.promise.cancel();
}
}
};
public setDownloadFunction(func: ProgressivePreloader['loadFunc']) {
this.loadFunc = func;
}
public setManual() {
this.preloader.classList.add('manual');
this.setProgress(0);
}
public attachPromise(promise: CancellablePromise<any>) {
if(this.isUpload && this.promise) return;
this.promise = promise;
const tempId = --this.tempId;
const onEnd = (successfully: boolean) => {
const onEnd = (err: Error) => {
promise.notify = null;
if(tempId === this.tempId) {
if(successfully && this.cancelable) {
if(!err && this.cancelable) {
this.setProgress(100);
setTimeout(() => { // * wait for transition complete
if(tempId === this.tempId) {
this.detach();
}
}, TRANSITION_TIME * 1.25);
}, TRANSITION_TIME * 0.75);
} else {
this.detach();
if(this.tryAgainOnFail) {
this.setManual();
} else {
this.detach();
}
}
this.promise = promise = null;
}
};
//promise.catch(onEnd);
promise
.then(() => onEnd(true))
.catch(() => onEnd(false));
.then(() => onEnd(null))
.catch((err) => onEnd(err));
if(promise.addNotifyListener) {
promise.addNotifyListener((details: {done: number, total: number}) => {
@ -115,7 +190,17 @@ export default class ProgressivePreloader {
if(this.detached) return;
this.detached = false; */
elem[this.attachMethod](this.preloader);
if(this.construct) {
this.construct();
}
if(this.preloader.parentElement) {
this.preloader.classList.remove('manual');
}
if(this.preloader.parentElement !== elem) {
elem[this.attachMethod](this.preloader);
}
window.requestAnimationFrame(() => {
SetTransition(this.preloader, 'is-visible', true, TRANSITION_TIME);
@ -132,7 +217,7 @@ export default class ProgressivePreloader {
//return;
if(this.preloader.parentElement) {
if(this.preloader && this.preloader.parentElement) {
/* setTimeout(() => *///window.requestAnimationFrame(() => {
/* if(!this.detached) return;
this.detached = true; */

View File

@ -52,7 +52,6 @@ export class ScrollableBase {
protected onScroll: () => void;
public isHeavyAnimationInProgress = false;
public isHeavyScrolling = false;
constructor(public el: HTMLElement, logPrefix = '', public container: HTMLElement = document.createElement('div')) {
this.container.classList.add('scrollable');
@ -96,11 +95,7 @@ export class ScrollableBase {
forceDuration?: number,
axis?: 'x' | 'y'
) {
this.isHeavyScrolling = true;
return fastSmoothScroll(this.container, element, position, margin, maxDistance, forceDirection, forceDuration, axis)
.finally(() => {
this.isHeavyScrolling = false;
});
return fastSmoothScroll(this.container, element, position, margin, maxDistance, forceDirection, forceDuration, axis);
}
}
@ -205,8 +200,12 @@ export default class Scrollable extends ScrollableBase {
(this.splitUp || this.padding || this.container).append(...elements);
}
public getDistanceToEnd() {
return this.scrollHeight - Math.round(this.scrollTop + this.container.offsetHeight);
}
get isScrolledDown() {
return this.scrollHeight - Math.round(this.scrollTop + this.container.offsetHeight) <= 1;
return this.getDistanceToEnd() <= 1;
}
set scrollTop(y: number) {
@ -231,26 +230,12 @@ export class ScrollableX extends ScrollableBase {
if(!isTouchSupported) {
const scrollHorizontally = (e: any) => {
e = window.event || e;
if(e.which == 1) {
// maybe horizontal scroll is natively supports, works on macbook
return;
if(!e.deltaX) {
this.container!.scrollLeft += e.deltaY / 4;
}
const delta = Math.max(-1, Math.min(1, (e.wheelDelta || -e.detail)));
this.container.scrollLeft -= (delta * 20);
e.preventDefault();
};
if(this.container.addEventListener) {
// IE9, Chrome, Safari, Opera
this.container.addEventListener("mousewheel", scrollHorizontally, false);
// Firefox
this.container.addEventListener("DOMMouseScroll", scrollHorizontally, false);
} else {
// IE 6/7/8
// @ts-ignore
this.container.attachEvent("onmousewheel", scrollHorizontally);
}
this.container.addEventListener('wheel', scrollHorizontally, {passive: true});
}
}
}

View File

@ -10,6 +10,7 @@ import AppSharedMediaTab from "./tabs/sharedMedia";
import { MOUNT_CLASS_TO } from "../../lib/mtproto/mtproto_config";
import { pause } from "../../helpers/schedulers";
import rootScope from "../../lib/rootScope";
import { dispatchHeavyAnimationEvent } from "../../hooks/useHeavyAnimationCheck";
export const RIGHT_COLUMN_ACTIVE_CLASSNAME = 'is-right-column-shown';
@ -24,7 +25,6 @@ export class AppSidebarRight extends SidebarSlider {
public static SLIDERITEMSIDS = {
sharedMedia: 0,
search: 1,
//forward: 2,
stickers: 2,
pollResults: 3,
gifs: 4,
@ -32,7 +32,6 @@ export class AppSidebarRight extends SidebarSlider {
public sharedMediaTab: AppSharedMediaTab;
public searchTab: AppPrivateSearchTab;
//public forwardTab: AppForwardTab;
public stickersTab: AppStickersTab;
public pollResultsTab: AppPollResultsTab;
public gifsTab: AppGifsTab;
@ -41,23 +40,19 @@ export class AppSidebarRight extends SidebarSlider {
super(document.getElementById('column-right') as HTMLElement, {
[AppSidebarRight.SLIDERITEMSIDS.sharedMedia]: sharedMediaTab,
[AppSidebarRight.SLIDERITEMSIDS.search]: searchTab,
//[AppSidebarRight.SLIDERITEMSIDS.forward]: forwardTab,
[AppSidebarRight.SLIDERITEMSIDS.stickers]: stickersTab,
[AppSidebarRight.SLIDERITEMSIDS.pollResults]: pollResultsTab,
[AppSidebarRight.SLIDERITEMSIDS.gifs]: gifsTab
}, true);
//this._selectTab(3);
this.sharedMediaTab = sharedMediaTab;
this.searchTab = searchTab;
//this.forwardTab = forwardTab;
this.stickersTab = stickersTab;
this.pollResultsTab = pollResultsTab;
this.gifsTab = gifsTab;
mediaSizes.addListener('changeScreen', (from, to) => {
if(to == ScreenSize.medium && from !== ScreenSize.mobile) {
if(to === ScreenSize.medium && from !== ScreenSize.mobile) {
this.toggleSidebar(false);
}
});
@ -108,12 +103,18 @@ export class AppSidebarRight extends SidebarSlider {
this.selectTab(AppSidebarRight.SLIDERITEMSIDS.sharedMedia);
}
const transitionTime = rootScope.settings.animationsEnabled ? mediaSizes.isMobile ? 250 : 200 : 0;
const promise = pause(transitionTime);
if(transitionTime) {
dispatchHeavyAnimationEvent(promise, transitionTime);
}
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 pause(rootScope.settings.animationsEnabled ? mediaSizes.isMobile ? 250 : 200 : 0); // delay of slider animation
return promise; // delay of slider animation
//}
return pause(200); // delay for third column open

View File

@ -1,5 +1,7 @@
import { whichChild } from "../helpers/dom";
import rootScope from "../lib/rootScope";
import { CancellablePromise, deferredPromise } from "../helpers/cancellablePromise";
import { dispatchHeavyAnimationEvent } from "../hooks/useHeavyAnimationCheck";
function slideNavigation(tabContent: HTMLElement, prevTabContent: HTMLElement, toRight: boolean) {
const width = prevTabContent.getBoundingClientRect().width;
@ -21,39 +23,29 @@ function slideNavigation(tabContent: HTMLElement, prevTabContent: HTMLElement, t
}
function slideTabs(tabContent: HTMLElement, prevTabContent: HTMLElement, toRight: boolean) {
const width = prevTabContent.getBoundingClientRect().width;
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
tabContent.style.transform = '';
//window.requestAnimationFrame(() => {
const width = prevTabContent.getBoundingClientRect().width;
/* tabContent.style.setProperty('--width', width + 'px');
prevTabContent.style.setProperty('--width', width + 'px');
tabContent.classList.add('active'); */
//void tabContent.offsetWidth; // reflow
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
tabContent.style.transform = '';
//});
return () => {
prevTabContent.style.transform = '';
};
}
/* function slideTabsVertical(tabContent: HTMLElement, prevTabContent: HTMLElement, toRight: boolean) {
const height = prevTabContent.getBoundingClientRect().height;
const elements = [tabContent, prevTabContent];
if(toRight) elements.reverse();
elements[0].style.transform = `translate3d(0, ${-height}px, 0)`;
elements[1].style.transform = `translate3d(0, ${height}px, 0)`;
tabContent.classList.add('active');
void tabContent.offsetWidth; // reflow
tabContent.style.transform = '';
return () => {
prevTabContent.style.transform = '';
};
} */
export const TransitionSlider = (content: HTMLElement, type: 'tabs' | 'navigation' | 'zoom-fade' | 'none'/* | 'counter' */, transitionTime: number, onTransitionEnd?: (id: number) => void) => {
let animationFunction: TransitionFunction = null;
@ -64,12 +56,11 @@ export const TransitionSlider = (content: HTMLElement, type: 'tabs' | 'navigatio
case 'navigation':
animationFunction = slideNavigation;
break;
/* case 'counter':
animationFunction = slideTabsVertical;
break; */
/* default:
break; */
}
content.dataset.animation = type;
return Transition(content, animationFunction, transitionTime, onTransitionEnd);
};
@ -77,10 +68,33 @@ export const TransitionSlider = (content: HTMLElement, type: 'tabs' | 'navigatio
type TransitionFunction = (tabContent: HTMLElement, prevTabContent: HTMLElement, toRight: boolean) => void | (() => void);
const Transition = (content: HTMLElement, animationFunction: TransitionFunction, transitionTime: number, onTransitionEnd?: (id: number) => void) => {
const hideTimeouts: {[id: number]: number} = {};
//const deferred: (() => void)[] = [];
let transitionEndTimeout: number;
let prevTabContent: HTMLElement = null;
const onTransitionEndCallbacks: Map<HTMLElement, Function> = new Map();
let animationDeferred: CancellablePromise<void>;
let animationStarted = 0;
let from: HTMLElement = null;
// TODO: check for transition type (transform, etc) using by animationFunction
content.addEventListener(animationFunction ? 'transitionend' : 'animationend', (e) => {
if((e.target as HTMLElement).parentElement !== content) {
return;
}
//console.log('Transition: transitionend', /* content, */ e, selectTab.prevId, performance.now() - animationStarted);
const callback = onTransitionEndCallbacks.get(e.target as HTMLElement);
if(callback) callback();
if(!animationDeferred || e.target !== from) return;
animationDeferred.resolve();
animationDeferred = undefined;
if(onTransitionEnd) {
onTransitionEnd(selectTab.prevId);
}
content.classList.remove('animating', 'backwards', 'disable-hover');
});
function selectTab(id: number | HTMLElement, animate = true) {
const self = selectTab;
@ -89,96 +103,84 @@ const Transition = (content: HTMLElement, animationFunction: TransitionFunction,
id = whichChild(id);
}
if(id == self.prevId) return false;
if(id === self.prevId) return false;
//console.log('selectTab id:', id);
const p = prevTabContent;
const tabContent = content.children[id] as HTMLElement;
const _from = from;
const to = content.children[id] as HTMLElement;
if(!rootScope.settings.animationsEnabled) {
if(!rootScope.settings.animationsEnabled || self.prevId === -1) {
animate = false;
}
// * means animation isn't needed
if(/* content.dataset.slider == 'none' || */!animate) {
if(p) {
p.classList.remove('active');
if(!animate) {
if(_from) _from.classList.remove('active', 'to', 'from');
if(to) {
to.classList.remove('to', 'from');
to.classList.add('active');
}
if(tabContent) {
tabContent.classList.add('active');
}
content.classList.remove('animating', 'backwards', 'disable-hover');
self.prevId = id;
prevTabContent = tabContent;
from = to;
if(onTransitionEnd) onTransitionEnd(self.prevId);
return;
}
if(prevTabContent) {
prevTabContent.classList.remove('to');
prevTabContent.classList.add('from');
if(from) {
from.classList.remove('to');
from.classList.add('from');
}
content.classList.add('animating');
content.classList.add('animating', 'disable-hover');
const toRight = self.prevId < id;
content.classList.toggle('backwards', !toRight);
let afterTimeout: ReturnType<TransitionFunction>;
if(!tabContent) {
let onTransitionEndCallback: ReturnType<TransitionFunction>;
if(!to) {
//prevTabContent.classList.remove('active');
} else if(self.prevId != -1) {
} else {
if(animationFunction) {
afterTimeout = animationFunction(tabContent, prevTabContent, toRight);
onTransitionEndCallback = animationFunction(to, from, toRight);
} else {
to.classList.add('active');
}
tabContent.classList.add('to');
} else {
tabContent.classList.add('active');
to.classList.remove('from');
to.classList.add('to');
}
const _prevId = self.prevId;
if(hideTimeouts.hasOwnProperty(id)) clearTimeout(hideTimeouts[id]);
if(p/* && false */) {
hideTimeouts[_prevId] = window.setTimeout(() => {
if(afterTimeout) {
afterTimeout();
if(to) {
onTransitionEndCallbacks.set(to, () => {
to.classList.remove('to');
onTransitionEndCallbacks.delete(to);
});
}
if(_from/* && false */) {
onTransitionEndCallbacks.set(_from, () => {
_from.classList.remove('active', 'from');
if(onTransitionEndCallback) {
onTransitionEndCallback();
}
p.classList.remove('active', 'from');
onTransitionEndCallbacks.delete(_from);
});
if(tabContent) {
tabContent.classList.remove('to');
tabContent.classList.add('active');
}
if(!animationDeferred) {
animationDeferred = deferredPromise<void>();
animationStarted = performance.now();
}
delete hideTimeouts[_prevId];
}, transitionTime);
if(transitionEndTimeout) clearTimeout(transitionEndTimeout);
transitionEndTimeout = window.setTimeout(() => {
if(onTransitionEnd) {
onTransitionEnd(self.prevId);
}
content.classList.remove('animating', 'backwards');
transitionEndTimeout = 0;
}, transitionTime);
dispatchHeavyAnimationEvent(animationDeferred, transitionTime * 2);
}
self.prevId = id;
prevTabContent = tabContent;
/* if(p) {
return new Promise((resolve) => {
deferred.push(resolve);
});
} else {
return Promise.resolve();
} */
from = to;
}
selectTab.prevId = -1;

View File

@ -33,7 +33,7 @@ import rootScope from '../lib/rootScope';
const MAX_VIDEO_AUTOPLAY_SIZE = 50 * 1024 * 1024; // 50 MB
export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTail, isOut, middleware, lazyLoadQueue, noInfo, group, onlyPreview, withoutPreloader, loadPromises}: {
export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTail, isOut, middleware, lazyLoadQueue, noInfo, group, onlyPreview, withoutPreloader, loadPromises, noPlayButton}: {
doc: MyDocument,
container?: HTMLElement,
message?: any,
@ -44,6 +44,7 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
middleware?: () => boolean,
lazyLoadQueue?: LazyLoadQueue,
noInfo?: true,
noPlayButton?: boolean,
group?: string,
onlyPreview?: boolean,
withoutPreloader?: boolean,
@ -54,20 +55,22 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
let spanTime: HTMLElement;
if(!noInfo) {
if(doc.type != 'round') {
if(doc.type !== 'round') {
spanTime = document.createElement('span');
spanTime.classList.add('video-time');
container.append(spanTime);
if(doc.type != 'gif') {
if(doc.type !== 'gif') {
spanTime.innerText = (doc.duration + '').toHHMMSS(false);
if(canAutoplay) {
spanTime.classList.add('tgico', 'can-autoplay');
} else {
const spanPlay = document.createElement('span');
spanPlay.classList.add('video-play', 'tgico-largeplay', 'btn-circle', 'position-center');
container.append(spanPlay);
if(!noPlayButton) {
if(canAutoplay) {
spanTime.classList.add('tgico', 'can-autoplay');
} else {
const spanPlay = document.createElement('span');
spanPlay.classList.add('video-play', 'tgico-largeplay', 'btn-circle', 'position-center');
container.append(spanPlay);
}
}
} else {
spanTime.innerText = 'GIF';
@ -80,7 +83,7 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
loadPromise: Promise<any>
} = {} as any;
if(doc.mime_type == 'image/gif') {
if(doc.mime_type === 'image/gif') {
const photoRes = wrapPhoto({
photo: doc,
message,
@ -109,7 +112,7 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
video.classList.add('media-video');
video.muted = true;
video.setAttribute('playsinline', 'true');
if(doc.type == 'round') {
if(doc.type === 'round') {
//video.muted = true;
const globalVideo = appMediaPlaybackController.addMedia(message.peerId, doc, message.mid);
@ -224,23 +227,22 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
preloader = message.media.preloader as ProgressivePreloader;
preloader.attach(container, false);
} else if(!doc.downloaded && !doc.supportsStreaming) {
const promise = appDocsManager.downloadDoc(doc, /* undefined, */lazyLoadQueue?.queueId);
preloader = new ProgressivePreloader(null, true, false, 'prepend');
const promise = appDocsManager.downloadDoc(doc, lazyLoadQueue?.queueId);
preloader = new ProgressivePreloader({
attachMethod: 'prepend'
});
preloader.attach(container, true, promise);
/* video.addEventListener('canplay', () => {
if(preloader) {
preloader.detach();
}
}, {once: true}); */
await promise;
if(middleware && !middleware()) {
return;
}
} else if(doc.supportsStreaming) {
preloader = new ProgressivePreloader(null, false, false, 'prepend');
preloader = new ProgressivePreloader({
cancelable: false,
attachMethod: 'prepend'
});
preloader.attach(container, false, null);
video.addEventListener(isSafari ? 'timeupdate' : 'canplay', () => {
preloader.detach();
@ -267,7 +269,7 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
}, {once: true});
//}
if(doc.type == 'video') {
if(doc.type === 'video') {
video.addEventListener('timeupdate', () => {
spanTime.innerText = (video.duration - video.currentTime + '').toHHMMSS(false);
});
@ -275,23 +277,11 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
video.addEventListener('error', (e) => {
deferred.resolve();
/* console.error('video error', e, video.src);
if(video.src) { // if wasn't cleaned
deferred.reject(e);
} else {
deferred.resolve();
} */
});
//if(doc.type != 'round') {
renderImageFromUrl(video, doc.url);
//}
renderImageFromUrl(video, doc.url);
/* if(!container.parentElement) {
container.append(video);
} */
if(doc.type == 'round') {
if(doc.type === 'round') {
video.dataset.ckin = 'circle';
video.dataset.overlay = '1';
new VideoPlayer(video);
@ -356,7 +346,7 @@ export function wrapDocument({message, withTime, fontWeight, voiceAsMusic, showS
const uploading = message.pFlags.is_outgoing;
const doc = message.media.document || message.media.webpage.document;
if(doc.type == 'audio' || doc.type == 'voice') {
if(doc.type === 'audio' || doc.type === 'voice') {
const audioElement = new AudioElement();
audioElement.setAttribute('message-id', '' + message.mid);
audioElement.setAttribute('peer-id', '' + message.peerId);
@ -367,6 +357,11 @@ export function wrapDocument({message, withTime, fontWeight, voiceAsMusic, showS
if(searchContext) audioElement.searchContext = searchContext;
if(showSender) audioElement.showSender = showSender;
const isPending = message.pFlags.is_outgoing;
if(isPending) {
audioElement.preloader = message.media.preloader;
}
audioElement.dataset.fontWeight = '' + fontWeight;
audioElement.render();
return audioElement;
@ -394,7 +389,8 @@ export function wrapDocument({message, withTime, fontWeight, voiceAsMusic, showS
container: icoDiv,
boxWidth: 54,
boxHeight: 54,
loadPromises
loadPromises,
withoutPreloader: true
});
icoDiv.style.width = icoDiv.style.height = '';
}
@ -423,7 +419,7 @@ export function wrapDocument({message, withTime, fontWeight, voiceAsMusic, showS
}
docDiv.innerHTML = `
${!uploading ? `<div class="document-download"><div class="tgico-download"></div></div>` : ''}
${!uploading ? `<div class="document-download"></div>` : ''}
<div class="document-name"><middle-ellipsis-element data-font-weight="${fontWeight}">${fileName}</middle-ellipsis-element>${titleAdditionHTML}</div>
<div class="document-size">${size}</div>
`;
@ -431,40 +427,35 @@ export function wrapDocument({message, withTime, fontWeight, voiceAsMusic, showS
docDiv.prepend(icoDiv);
if(!uploading) {
let downloadDiv = docDiv.querySelector('.document-download') as HTMLDivElement;
let preloader: ProgressivePreloader;
let download: DownloadBlob;
attachClickEvent(docDiv, (e) => {
cancelEvent(e);
if(!download) {
if(downloadDiv.classList.contains('downloading')) {
return; // means not ready yet
}
if(!preloader) {
preloader = new ProgressivePreloader(null, true);
}
const downloadDiv = docDiv.querySelector('.document-download') as HTMLDivElement;
const preloader = new ProgressivePreloader();
//preloader.attach(downloadDiv, true);
download = appDocsManager.saveDocFile(doc, appImManager.chat.bubbles.lazyLoadQueue.queueId);
preloader.attach(downloadDiv, true, download);
download.then(() => {
const load = () => {
const download = appDocsManager.saveDocFile(doc, appImManager.chat.bubbles.lazyLoadQueue.queueId);
download.then(() => {
downloadDiv.classList.add('downloaded');
setTimeout(() => {
downloadDiv.remove();
}).catch(err => {
if(err.name === 'AbortError') {
download = null;
}
}).finally(() => {
downloadDiv.classList.remove('downloading');
});
downloadDiv.classList.add('downloading');
} else {
download.cancel();
}
}, 200);
});
preloader.attach(downloadDiv, true, download);
return {download};
};
preloader.construct();
preloader.setManual();
preloader.attach(downloadDiv);
preloader.setDownloadFunction(load);
attachClickEvent(docDiv, (e) => {
preloader.onClick(e);
});
} else if(message.media?.preloader) {
const icoDiv = docDiv.querySelector('.document-ico');
message.media.preloader.attach(icoDiv, false);
}
return docDiv;
@ -544,6 +535,10 @@ export function wrapPhoto({photo, message, container, boxWidth, boxHeight, withT
loadPromises?: Promise<any>[]
}) {
if(!((photo as MyPhoto).sizes || (photo as MyDocument).thumbs)) {
if(boxWidth && boxHeight && photo._ === 'document') {
size = appPhotosManager.setAttachmentSize(photo, container, boxWidth, boxHeight);
}
return {
loadPromises: {
thumb: Promise.resolve(),
@ -556,13 +551,8 @@ export function wrapPhoto({photo, message, container, boxWidth, boxHeight, withT
};
}
if(boxWidth === undefined) {
boxWidth = mediaSizes.active.regular.width;
}
if(boxHeight === undefined) {
boxHeight = mediaSizes.active.regular.height;
}
if(boxWidth === undefined) boxWidth = mediaSizes.active.regular.width;
if(boxHeight === undefined) boxHeight = mediaSizes.active.regular.height;
let loadThumbPromise: Promise<any>;
let thumbImage: HTMLImageElement;
@ -598,60 +588,68 @@ export function wrapPhoto({photo, message, container, boxWidth, boxHeight, withT
let preloader: ProgressivePreloader;
if(message?.media?.preloader) { // means upload
message.media.preloader.attach(container, false);
} else if(!cacheContext.downloaded && !withoutPreloader) {
preloader = new ProgressivePreloader(null, false, false, 'prepend');
preloader = message.media.preloader;
preloader.attach(container);
} else {
preloader = new ProgressivePreloader({
attachMethod: 'prepend'
});
}
let loadPromise: Promise<any>;
const load = () => {
if(loadPromise) return loadPromise;
const getDownloadPromise = () => {
const promise = photo._ === 'document' && photo.mime_type === 'image/gif' ?
appDocsManager.downloadDoc(photo, /* undefined, */lazyLoadQueue?.queueId) :
appPhotosManager.preloadPhoto(photo, size, lazyLoadQueue?.queueId);
if(preloader) {
preloader.attach(container, true, promise);
}
return promise;
};
return loadPromise = promise.then(() => {
if(middleware && !middleware()) return;
const onLoad = () => {
if(middleware && !middleware()) return Promise.resolve();
return new Promise((resolve) => {
renderImageFromUrl(image, cacheContext.url || photo.url, () => {
container.append(image);
return new Promise((resolve) => {
renderImageFromUrl(image, cacheContext.url || photo.url, () => {
container.append(image);
window.requestAnimationFrame(() => {
resolve();
});
//resolve();
if(needFadeIn) {
setTimeout(() => {
image.classList.remove('fade-in');
if(thumbImage) {
thumbImage.remove();
}
}, 200);
}
window.requestAnimationFrame(() => {
resolve();
});
//resolve();
if(needFadeIn) {
setTimeout(() => {
image.classList.remove('fade-in');
if(thumbImage) {
thumbImage.remove();
}
}, 200);
}
});
});
};
let loadPromise: Promise<any>;
const load = () => {
const promise = getDownloadPromise();
if(!cacheContext.downloaded && !withoutPreloader) {
preloader.attach(container, false, promise);
}
return {download: promise, render: promise.then(onLoad)};
};
preloader.setDownloadFunction(load);
if(cacheContext.downloaded) {
loadThumbPromise = load();
}
if(!lazyLoadQueue) {
loadPromise = load();
loadThumbPromise = loadPromise = load().render;
} else {
lazyLoadQueue.push({div: container, load});
if(!lazyLoadQueue) loadPromise = load().render;
else lazyLoadQueue.push({div: container, load: () => load().download});
}
if(loadPromises) {
if(loadPromises && loadThumbPromise) {
loadPromises.push(loadThumbPromise);
}
@ -1103,14 +1101,12 @@ export function wrapGroupedDocuments({albumMustBeRenderedFull, message, bubble,
}) {
let nameContainer: HTMLDivElement;
const mids = albumMustBeRenderedFull ? chat.getMidsByMid(message.mid) : [message.mid];
const isPending = message.pFlags.is_outgoing;
/* if(isPending) {
mids.reverse();
} */
mids.forEach((mid, idx) => {
const message = chat.getMessage(mid);
const doc = message.media.document;
const div = wrapDocument({
message,
loadPromises
@ -1147,15 +1143,6 @@ export function wrapGroupedDocuments({albumMustBeRenderedFull, message, bubble,
}
}
if(isPending) {
if(doc.type == 'audio' || doc.type == 'voice') {
(div as AudioElement).preloader = message.media.preloader;
} else {
const icoDiv = div.querySelector('.audio-download, .document-ico');
message.media.preloader.attach(icoDiv, false);
}
}
wrapper.append(div);
container.append(wrapper);
messageDiv.append(container);

View File

@ -1,34 +1,45 @@
import fastBlur from '../vendor/fastBlur';
import { fastRaf } from './schedulers';
import pushHeavyTask from './heavyQueue';
const RADIUS = 2;
const ITERATIONS = 2;
export default function blur(dataUri: string, delay?: number) {
function processBlur(dataUri: string) {
return new Promise<string>((resolve) => {
fastRaf(() => {
const img = new Image();
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
console.log('[blur] start');
const ctx = canvas.getContext('2d')!;
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
fastBlur(ctx, 0, 0, canvas.width, canvas.height, RADIUS, ITERATIONS);
const ctx = canvas.getContext('2d')!;
resolve(canvas.toDataURL());
};
ctx.drawImage(img, 0, 0);
fastBlur(ctx, 0, 0, canvas.width, canvas.height, RADIUS, ITERATIONS);
if(delay) {
setTimeout(() => {
img.src = dataUri;
}, delay);
} else {
img.src = dataUri;
}
//resolve(canvas.toDataURL());
canvas.toBlob(blob => {
resolve(URL.createObjectURL(blob));
console.log('[blur] end');
});
};
img.src = dataUri;
});
}
export default function blur(dataUri: string) {
return new Promise<string>((resolve) => {
//return resolve(dataUri);
pushHeavyTask({
items: [dataUri],
context: null,
process: processBlur
}).then(results => {
resolve(results[0]);
});
});
}

View File

@ -51,6 +51,12 @@ export function deferredPromise<T>() {
};
});
// @ts-ignore
/* deferred.then = (resolve: (value: T) => any, reject: (...args: any[]) => any) => {
const n = deferredPromise<ReturnType<typeof resolve>>();
}; */
deferred.finally(() => {
deferred.notify = null;
deferred.listeners.length = 0;

View File

@ -370,6 +370,19 @@ export function findUpAttribute(el: any, attribute: string): HTMLElement {
return null; */
}
export function findUpAsChild(el: any, parent: any) {
if(el.parentElement === parent) return el;
while(el.parentElement) {
el = el.parentElement;
if(el.parentElement === parent) {
return el;
}
}
return null;
}
export function whichChild(elem: Node) {
if(!elem.parentNode) {
return -1;

72
src/helpers/heavyQueue.ts Normal file
View File

@ -0,0 +1,72 @@
import { CancellablePromise, deferredPromise } from "./cancellablePromise";
import { getHeavyAnimationPromise } from "../hooks/useHeavyAnimationCheck";
import { fastRaf } from "./schedulers";
type HeavyQueue<T> = {
items: any[],
process: (...args: any[]) => T,
context: any,
promise?: CancellablePromise<ReturnType<HeavyQueue<T>['process']>[]>
};
const heavyQueue: HeavyQueue<any>[] = [];
let processingQueue = false;
export default function pushHeavyTask<T>(queue: HeavyQueue<T>) {
queue.promise = deferredPromise<T[]>();
heavyQueue.push(queue);
processHeavyQueue();
return queue.promise;
}
function processHeavyQueue() {
if(!processingQueue) {
const queue = heavyQueue.shift();
timedChunk(queue).finally(() => {
processingQueue = false;
if(heavyQueue.length) {
processHeavyQueue();
}
});
}
}
function timedChunk<T>(queue: HeavyQueue<T>) {
if(!queue.items.length) return Promise.resolve([]);
const todo = queue.items.slice();
const results: T[] = [];
return new Promise<T[]>((resolve, reject) => {
const f = async() => {
const start = performance.now();
do {
await getHeavyAnimationPromise();
const possiblePromise = queue.process.call(queue.context, todo.shift());
let realResult: T;
if(possiblePromise instanceof Promise) {
try {
realResult = await possiblePromise;
} catch(err) {
reject(err);
return;
}
} else {
realResult = possiblePromise;
}
results.push(realResult);
} while(todo.length > 0 && (performance.now() - start) < 6);
if(todo.length > 0) {
fastRaf(f);
//setTimeout(f, 25);
} else {
resolve(results);
}
};
fastRaf(f);
//setTimeout(f, 25);
}).then(queue.promise.resolve, queue.promise.reject);
}

View File

@ -23,7 +23,7 @@ export const dispatchHeavyAnimationEvent = (promise: Promise<any>, timeout?: num
}
++promisesInQueue;
console.log('dispatchHeavyAnimationEvent: attach promise, length:', promisesInQueue);
console.log('dispatchHeavyAnimationEvent: attach promise, length:', promisesInQueue, timeout);
const promises = [
timeout !== undefined ? pause(timeout) : undefined,

View File

@ -29,7 +29,7 @@
<div class="whole" id="auth-pages" style="display: none;">
<div class="scrollable scrollable-y">
<div class="tabs-container auth-pages__container" data-slider="tabs">
<div class="tabs-container auth-pages__container" data-animation="tabs">
<div class="page-sign">
<div class="container center-align">
<div class="auth-image">
@ -144,10 +144,8 @@
<div class="sidebar-content transition zoom-fade">
<div class="transition-item active" id="chatlist-container">
<div class="folders-tabs-scrollable menu-horizontal-scrollable hide">
<nav class="menu-horizontal" id="folders-tabs">
<ul>
<li class="rp"><span>All<div class="badge badge-20 badge-blue"></div><i></i></span></li>
</ul>
<nav class="menu-horizontal-div" id="folders-tabs">
<div class="menu-horizontal-div-item rp"><span>All<div class="badge badge-20 badge-blue"></div><i></i></span></div>
</nav>
</div>
<div class="tabs-container" id="folders-container">

View File

@ -22,9 +22,10 @@ import appUsersManager, { User } from "./appUsersManager";
import { App, DEBUG, MOUNT_CLASS_TO } from "../mtproto/mtproto_config";
import Button from "../../components/button";
import SetTransition from "../../components/singleTransition";
import AppStorage from '../storage';
import sessionStorage from '../sessionStorage';
import apiUpdatesManager from "./apiUpdatesManager";
import appDraftsManager, { MyDraftMessage } from "./appDraftsManager";
import ProgressivePreloader from "../../components/preloader";
type DialogDom = {
avatarEl: AvatarElement,
@ -46,7 +47,7 @@ let testTopSlice = 1;
class ConnectionStatusComponent {
private statusContainer: HTMLElement;
private statusEl: HTMLElement;
private statusPreloader: HTMLElement;
private statusPreloader: ProgressivePreloader;
private currentText = '';
@ -62,7 +63,8 @@ class ConnectionStatusComponent {
this.statusContainer.classList.add('connection-status');
this.statusEl = Button('btn-primary bg-warning connection-status-button', {noRipple: true});
this.statusPreloader = putPreloader(null, true).firstElementChild as HTMLElement;
this.statusPreloader = new ProgressivePreloader({cancelable: false});
this.statusPreloader.constructContainer({color: 'transparent', bold: true});
this.statusContainer.append(this.statusEl);
chatsContainer.prepend(this.statusContainer);
@ -94,7 +96,7 @@ class ConnectionStatusComponent {
});
const setConnectionStatus = () => {
AppStorage.get<number>('dc').then(baseDcId => {
sessionStorage.get('dc').then(baseDcId => {
if(!baseDcId) {
baseDcId = App.baseDcId;
}
@ -136,7 +138,7 @@ class ConnectionStatusComponent {
private setStatusText = (text: string) => {
if(this.currentText == text) return;
this.statusEl.innerText = this.currentText = text;
this.statusEl.appendChild(this.statusPreloader);
this.statusPreloader.attach(this.statusEl);
};
private setState = () => {
@ -389,7 +391,7 @@ export class AppDialogsManager {
// set tab
//(this.folders.menu.firstElementChild.children[Math.max(0, filter.id - 2)] as HTMLElement).click();
(this.folders.menu.firstElementChild.children[0] as HTMLElement).click();
(this.folders.menu.firstElementChild as HTMLElement).click();
elements.container.remove();
elements.menu.remove();
@ -405,7 +407,7 @@ export class AppDialogsManager {
rootScope.on('filter_order', (e) => {
const order = e;
const containerToAppend = this.folders.menu.firstElementChild as HTMLUListElement;
const containerToAppend = this.folders.menu as HTMLElement;
order.forEach((filterId) => {
const filter = appMessagesManager.filtersStorage.filters[filterId];
const renderedFilter = this.filtersRendered[filterId];
@ -450,14 +452,14 @@ export class AppDialogsManager {
this.onTabChange();
}, () => {
for(const folderId in this.chatLists) {
if(+folderId != this.filterId) {
if(+folderId !== this.filterId) {
this.chatLists[folderId].innerHTML = '';
}
}
}, undefined, foldersScrollable);
//selectTab(0);
(this.folders.menu.firstElementChild.firstElementChild as HTMLElement).click();
(this.folders.menu.firstElementChild as HTMLElement).click();
appStateManager.getState().then((state) => {
const getFiltersPromise = appMessagesManager.filtersStorage.getDialogFilters();
getFiltersPromise.then((filters) => {
@ -580,7 +582,8 @@ export class AppDialogsManager {
private addFilter(filter: DialogFilter) {
if(this.filtersRendered[filter.id]) return;
const li = document.createElement('li');
const menuTab = document.createElement('div');
menuTab.classList.add('menu-horizontal-div-item');
const span = document.createElement('span');
const titleSpan = document.createElement('span');
titleSpan.innerHTML = RichTextProcessor.wrapEmojiText(filter.title);
@ -588,11 +591,11 @@ export class AppDialogsManager {
unreadSpan.classList.add('badge', 'badge-20', 'badge-blue');
const i = document.createElement('i');
span.append(titleSpan, unreadSpan, i);
li.append(span);
ripple(li);
menuTab.append(span);
ripple(menuTab);
const containerToAppend = this.folders.menu.firstElementChild as HTMLUListElement;
positionElementByIndex(li, containerToAppend, filter.orderIndex);
const containerToAppend = this.folders.menu as HTMLElement;
positionElementByIndex(menuTab, containerToAppend, filter.orderIndex);
//containerToAppend.append(li);
const ul = document.createElement('ul');
@ -614,7 +617,7 @@ export class AppDialogsManager {
}
this.filtersRendered[filter.id] = {
menu: li,
menu: menuTab,
container: div,
unread: unreadSpan,
title: titleSpan
@ -1036,18 +1039,10 @@ export class AppDialogsManager {
return;
}
const isMuted = (dialog.notify_settings?.mute_until * 1000) > Date.now();
const isMuted = appMessagesManager.isDialogMuted(dialog);
const wasMuted = dom.listEl.classList.contains('is-muted');
if(!isMuted && wasMuted) {
dom.listEl.classList.add('backwards');
if(dom.muteAnimationTimeout) clearTimeout(dom.muteAnimationTimeout);
dom.muteAnimationTimeout = window.setTimeout(() => {
delete dom.muteAnimationTimeout;
dom.listEl.classList.remove('backwards', 'is-muted');
}, 200);
} else {
dom.listEl.classList.toggle('is-muted', isMuted);
if(isMuted !== wasMuted) {
SetTransition(dom.listEl, 'is-muted', isMuted, 200);
}
const lastMessage = dialog.draft && dialog.draft._ === 'draftMessage' ?
@ -1276,6 +1271,11 @@ export class AppDialogsManager {
container.append(li);
} */
const isMuted = appMessagesManager.isDialogMuted(dialog);
if(isMuted) {
li.classList.add('is-muted');
}
this.setLastMessage(dialog);
} else {
container[method](li);

View File

@ -9,6 +9,7 @@ import webpWorkerController from '../webp/webpWorkerController';
import appDownloadManager, { DownloadBlob } from './appDownloadManager';
import appPhotosManager from './appPhotosManager';
import blur from '../../helpers/blur';
import apiManager from '../mtproto/mtprotoworker';
export type MyDocument = Document.document;
@ -18,8 +19,16 @@ export class AppDocsManager {
private docs: {[docId: string]: MyDocument} = {};
private savingLottiePreview: {[docId: string]: true} = {};
public onServiceWorkerFail() {
for(const id in this.docs) {
const doc = this.docs[id];
delete doc.supportsStreaming;
delete doc.url;
}
}
public saveDoc(doc: Document, context?: ReferenceContext): MyDocument {
if(doc._ == 'documentEmpty') {
if(doc._ === 'documentEmpty') {
return undefined;
}
@ -147,8 +156,8 @@ export class AppDocsManager {
}
}
if('serviceWorker' in navigator) {
if((doc.type == 'gif' && doc.size > 8e6) || doc.type == 'audio' || doc.type == 'video') {
if(apiManager.isServiceWorkerOnline()) {
if((doc.type === 'gif' && doc.size > 8e6) || doc.type === 'audio' || doc.type === 'video') {
doc.supportsStreaming = true;
if(!doc.url) {
@ -159,13 +168,13 @@ export class AppDocsManager {
// for testing purposes
// doc.supportsStreaming = false;
// doc.url = '';
// doc.url = ''; // * this will break upload urls
if(!doc.file_name) {
doc.file_name = '';
}
if(doc.mime_type == 'application/x-tgsticker' && doc.file_name == "AnimatedSticker.tgs") {
if(doc.mime_type === 'application/x-tgsticker' && doc.file_name === "AnimatedSticker.tgs") {
doc.type = 'sticker';
doc.animated = true;
doc.sticker = 2;

View File

@ -112,6 +112,10 @@ export class AppDownloadManager {
return apiManager.downloadFile(options).then(deferred.resolve, onError);
});
} else {
/* return apiManager.downloadFile(options).then(res => {
setTimeout(() => deferred.resolve(res), 5e3);
}, onError); */
return apiManager.downloadFile(options).then(deferred.resolve, onError);
}
};

View File

@ -10,6 +10,7 @@ import apiManager from "../mtproto/mtprotoworker";
import { tsNow } from "../../helpers/date";
import { deepEqual } from "../../helpers/object";
import appStateManager from "./appStateManager";
import { isObject } from "../mtproto/bin_utils";
export type MyDraftMessage = DraftMessage.draftMessage;
@ -99,7 +100,37 @@ export class AppDraftsManager {
}
public draftsAreEqual(draft1: DraftMessage, draft2: DraftMessage) {
return deepEqual(draft1, draft2);
if(typeof(draft1) !== typeof(draft2)) {
return false;
}
if(!isObject(draft1)) {
return true;
}
if(draft1._ !== draft2._) {
return false;
}
if(draft1._ === 'draftMessage' && draft2._ === draft1._) {
if(draft1.reply_to_msg_id !== draft2.reply_to_msg_id) {
return false;
}
if(!deepEqual(draft1.entities, draft2.entities)) {
return false;
}
if(draft1.message !== draft2.message) {
return false;
}
if(draft1.pFlags.no_webpage !== draft2.pFlags.no_webpage) {
return false;
}
}
return true;
}
public isEmptyDraft(draft: DraftMessage) {
@ -136,12 +167,12 @@ export class AppDraftsManager {
return draft;
}
public syncDraft(peerId: number, threadId: number, localDraft?: MyDraftMessage, saveOnServer = true) {
public async syncDraft(peerId: number, threadId: number, localDraft?: MyDraftMessage, saveOnServer = true) {
// console.warn(dT(), 'sync draft', peerID)
const serverDraft = this.getDraft(peerId, threadId);
if(this.draftsAreEqual(serverDraft, localDraft)) {
// console.warn(dT(), 'equal drafts', localDraft, serverDraft)
return;
return true;
}
// console.warn(dT(), 'changed draft', localDraft, serverDraft)
@ -154,29 +185,34 @@ export class AppDraftsManager {
if(this.isEmptyDraft(localDraft)) {
draftObj = {_: 'draftMessageEmpty'};
} else {
draftObj = {_: 'draftMessage'} as any as DraftMessage.draftMessage;
let message = localDraft.message;
let entities: MessageEntity[] = localDraft.entities;
if(localDraft.reply_to_msg_id) {
params.reply_to_msg_id = draftObj.reply_to_msg_id = localDraft.reply_to_msg_id;
params.reply_to_msg_id = appMessagesManager.getLocalMessageId(localDraft.reply_to_msg_id);
}
if(entities?.length) {
params.entities = draftObj.entities = entities;
params.entities = entities;
}
params.message = draftObj.message = message;
if(localDraft.pFlags.no_webpage) {
params.no_webpage = localDraft.pFlags.no_webpage;
}
params.message = message;
}
draftObj.date = tsNow(true) + serverTimeManager.serverTimeOffset;
this.saveDraft(peerId, threadId, draftObj, {notify: true});
const saveLocalDraft = draftObj || localDraft;
saveLocalDraft.date = tsNow(true) + serverTimeManager.serverTimeOffset;
this.saveDraft(peerId, threadId, saveLocalDraft, {notify: true});
if(saveOnServer && !threadId) {
apiManager.invokeApi('messages.saveDraft', params).then(() => {
});
return apiManager.invokeApi('messages.saveDraft', params);
}
return true;
}
}

View File

@ -27,9 +27,9 @@ import { isTouchSupported } from '../../helpers/touchSupport';
import appPollsManager from './appPollsManager';
import SetTransition from '../../components/singleTransition';
import ChatDragAndDrop from '../../components/chat/dragAndDrop';
import { debounce } from '../../helpers/schedulers';
import { debounce, pause } from '../../helpers/schedulers';
import lottieLoader from '../lottieLoader';
import useHeavyAnimationCheck from '../../hooks/useHeavyAnimationCheck';
import useHeavyAnimationCheck, { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
import appDraftsManager from './appDraftsManager';
import serverTimeManager from '../mtproto/serverTimeManager';
@ -102,6 +102,7 @@ export class AppImManager {
this.chatsContainer = document.createElement('div');
this.chatsContainer.classList.add('chats-container', 'tabs-container');
this.chatsContainer.dataset.animation = 'navigation';
this.columnEl.append(this.chatsContainer);
@ -173,6 +174,8 @@ export class AppImManager {
animationIntersector.checkAnimations(false);
}
// * не могу использовать тут TransitionSlider, так как мне нужен отрисованный блок рядом
// * (или под текущим чатом) чтобы правильно отрендерить чат (напр. scrollTop)
private chatsSelectTab(tab: HTMLElement) {
if(this.prevTab === tab) {
return;
@ -181,6 +184,10 @@ export class AppImManager {
if(this.prevTab) {
this.prevTab.classList.remove('active');
this.chatsSelectTabDebounced();
if(rootScope.settings.animationsEnabled) { // ! нужно переделать на animation, так как при лаге анимация будет длиться не 250мс
dispatchHeavyAnimationEvent(pause(250), 250);
}
}
tab.classList.add('active');
@ -434,6 +441,14 @@ export class AppImManager {
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) {
const transitionTime = mediaSizes.isMobile ? 250 : 200;
dispatchHeavyAnimationEvent(pause(transitionTime), transitionTime);
}
this.tabId = id;
if(mediaSizes.isMobile && prevTabId === 2 && id === 1) {
//appSidebarRight.toggleSidebar(false);

View File

@ -19,7 +19,7 @@ import serverTimeManager from "../mtproto/serverTimeManager";
import { RichTextProcessor } from "../richtextprocessor";
import rootScope from "../rootScope";
import searchIndexManager from '../searchIndexManager';
import AppStorage from '../storage';
import sessionStorage from '../sessionStorage';
import DialogsStorage from "../storages/dialogs";
import FiltersStorage from "../storages/filters";
//import { telegramMeWebService } from "../mtproto/mtproto";
@ -34,6 +34,7 @@ import appStateManager from "./appStateManager";
import appUsersManager from "./appUsersManager";
import appWebPagesManager from "./appWebPagesManager";
import appDraftsManager from "./appDraftsManager";
import pushHeavyTask from "../../helpers/heavyQueue";
//console.trace('include');
// TODO: если удалить сообщение в непрогруженном диалоге, то при обновлении, из-за стейта, последнего сообщения в чатлисте не будет
@ -223,27 +224,6 @@ export class AppMessagesManager {
}
});
function timedChunk(items: any[], process: (...args: any[]) => any, context: any, callback: (...args: any[]) => void) {
if(!items.length) return callback(items);
const todo = items.slice();
const f = () => {
const start = +new Date();
do {
process.call(context, todo.shift());
} while(todo.length > 0 && (+new Date() - start < 50));
if(todo.length > 0) {
setTimeout(f, 25);
} else {
callback(items);
}
};
setTimeout(f, 25);
}
appStateManager.addListener('save', () => {
const messages: any[] = [];
const dialogs: Dialog[] = [];
@ -288,7 +268,11 @@ export class AppMessagesManager {
}
}
return new Promise((resolve => timedChunk(items, processDialog, this, resolve))).then(() => {
return pushHeavyTask({
items,
process: processDialog,
context: this
}).then(() => {
appStateManager.pushToState('dialogs', dialogs);
appStateManager.pushToState('messages', messages);
appStateManager.pushToState('filters', this.filtersStorage.filters);
@ -792,7 +776,11 @@ export class AppMessagesManager {
this.log('sendFile', attachType, apiFileName, file.type, options);
const preloader = new ProgressivePreloader(null, true, false, 'prepend');
const preloader = new ProgressivePreloader({
attachMethod: 'prepend',
tryAgainOnFail: false,
isUpload: true
});
const media = {
_: photo ? 'messageMediaPhoto' : 'messageMediaDocument',
@ -1578,16 +1566,17 @@ export class AppMessagesManager {
});
}
public isHistoryUnread(peerId: number, threadId?: number) {
public getReadMaxIdIfUnread(peerId: number, threadId?: number) {
const historyStorage = this.getHistoryStorage(peerId, threadId);
if(threadId) {
const chatHistoryStorage = this.getHistoryStorage(peerId);
const readMaxId = Math.max(chatHistoryStorage.readMaxId, historyStorage.readMaxId);
const message = this.getMessageByPeer(peerId, historyStorage.maxId);
return !message.pFlags.out && readMaxId < historyStorage.maxId;
return !message.pFlags.out && readMaxId < historyStorage.maxId ? readMaxId : 0;
} else {
const message = this.getMessageByPeer(peerId, historyStorage.maxId);
return !message.pFlags.out && (peerId > 0 ? Math.max(historyStorage.readMaxId, historyStorage.readOutboxMaxId) : historyStorage.readMaxId) < historyStorage.maxId;
const maxId = peerId > 0 ? Math.max(historyStorage.readMaxId, historyStorage.readOutboxMaxId) : historyStorage.readMaxId;
return !message.pFlags.out && maxId < historyStorage.maxId ? maxId : 0;
}
}
@ -2064,7 +2053,7 @@ export class AppMessagesManager {
message.pFlags = {};
}
if(message._ == 'messageEmpty') {
if(message._ === 'messageEmpty') {
return;
}
@ -2367,15 +2356,15 @@ export class AppMessagesManager {
case 'messageMediaDocument':
let document = media.document;
if(document.type == 'video') {
if(document.type === 'video') {
messageText = '<i>Video' + (message.message ? ', ' : '') + '</i>';
} else if(document.type == 'voice') {
} else if(document.type === 'voice') {
messageText = '<i>Voice message</i>';
} else if(document.type == 'gif') {
} else if(document.type === 'gif') {
messageText = '<i>GIF' + (message.message ? ', ' : '') + '</i>';
} else if(document.type == 'round') {
} else if(document.type === 'round') {
messageText = '<i>Video message' + (message.message ? ', ' : '') + '</i>';
} else if(document.type == 'sticker') {
} else if(document.type === 'sticker') {
messageText = (document.stickerEmoji || '') + '<i>Sticker</i>';
text = '';
} else {
@ -3359,83 +3348,91 @@ export class AppMessagesManager {
return promise;
}
public readHistory(peerId: number, maxId = 0, threadId?: number) {
public readHistory(peerId: number, maxId = 0, threadId?: number, force = false) {
// return Promise.resolve();
// console.trace('start read')
if(!this.isHistoryUnread(peerId, threadId)) return Promise.resolve();
this.log('readHistory:', peerId, maxId, threadId);
if(!this.getReadMaxIdIfUnread(peerId, threadId) && !force) {
this.log('readHistory: isn\'t unread');
return Promise.resolve();
}
const isChannel = appPeersManager.isChannel(peerId);
const historyStorage = this.getHistoryStorage(peerId, threadId);
if(!historyStorage.readMaxId || maxId > historyStorage.readMaxId) {
historyStorage.readMaxId = maxId;
let apiPromise: Promise<any>;
if(threadId) {
if(!historyStorage.readPromise) {
apiPromise = apiManager.invokeApi('messages.readDiscussion', {
peer: appPeersManager.getInputPeerById(peerId),
msg_id: this.getLocalMessageId(threadId),
read_max_id: this.getLocalMessageId(maxId)
});
}
apiUpdatesManager.processUpdateMessage({
_: 'updateShort',
update: {
_: 'updateReadChannelDiscussionInbox',
channel_id: -peerId,
top_msg_id: threadId,
read_max_id: maxId
} as Update.updateReadChannelDiscussionInbox
});
} else if(isChannel) {
if(!historyStorage.readPromise) {
apiPromise = apiManager.invokeApi('channels.readHistory', {
channel: appChatsManager.getChannelInput(-peerId),
max_id: this.getLocalMessageId(maxId)
});
}
apiUpdatesManager.processUpdateMessage({
_: 'updateShort',
update: {
_: 'updateReadChannelInbox',
max_id: maxId,
channel_id: -peerId
}
});
} else {
if(!historyStorage.readPromise) {
apiPromise = apiManager.invokeApi('messages.readHistory', {
peer: appPeersManager.getInputPeerById(peerId),
max_id: this.getLocalMessageId(maxId)
}).then((affectedMessages) => {
apiUpdatesManager.processUpdateMessage({
_: 'updateShort',
update: {
_: 'updatePts',
pts: affectedMessages.pts,
pts_count: affectedMessages.pts_count
}
});
});
}
apiUpdatesManager.processUpdateMessage({
_: 'updateShort',
update: {
_: 'updateReadHistoryInbox',
max_id: maxId,
peer: appPeersManager.getOutputPeer(peerId)
}
});
}
if(historyStorage.readPromise) {
return historyStorage.readPromise;
}
let apiPromise: Promise<void>;
if(threadId) {
apiPromise = apiManager.invokeApi('messages.readDiscussion', {
peer: appPeersManager.getInputPeerById(peerId),
msg_id: this.getLocalMessageId(threadId),
read_max_id: this.getLocalMessageId(maxId)
}).then((res) => {
apiUpdatesManager.processUpdateMessage({
_: 'updateShort',
update: {
_: 'updateReadChannelDiscussionInbox',
channel_id: -peerId,
top_msg_id: threadId,
read_max_id: maxId
} as Update.updateReadChannelDiscussionInbox
});
});
} else if(isChannel) {
apiPromise = apiManager.invokeApi('channels.readHistory', {
channel: appChatsManager.getChannelInput(-peerId),
max_id: this.getLocalMessageId(maxId)
}).then((res) => {
apiUpdatesManager.processUpdateMessage({
_: 'updateShort',
update: {
_: 'updateReadChannelInbox',
max_id: maxId,
channel_id: -peerId
}
});
});
} else {
apiPromise = apiManager.invokeApi('messages.readHistory', {
peer: appPeersManager.getInputPeerById(peerId),
max_id: this.getLocalMessageId(maxId)
}).then((affectedMessages) => {
apiUpdatesManager.processUpdateMessage({
_: 'updateShort',
update: {
_: 'updatePts',
pts: affectedMessages.pts,
pts_count: affectedMessages.pts_count
}
});
apiUpdatesManager.processUpdateMessage({
_: 'updateShort',
update: {
_: 'updateReadHistoryInbox',
max_id: maxId,
peer: appPeersManager.getOutputPeer(peerId)
}
});
});
}
apiPromise.finally(() => {
delete historyStorage.readPromise;
this.log('readHistory: promise finally', maxId, historyStorage.readMaxId);
if(historyStorage.readMaxId > maxId) {
this.readHistory(peerId, historyStorage.readMaxId);
this.readHistory(peerId, historyStorage.readMaxId, threadId, true);
}
});
@ -3880,7 +3877,7 @@ export class AppMessagesManager {
else foundDialog.read_inbox_max_id = maxId;
if(!isOut) {
if(newUnreadCount < 0 || !this.isHistoryUnread(peerId)) {
if(newUnreadCount < 0 || !this.getReadMaxIdIfUnread(peerId)) {
foundDialog.unread_count = 0;
} else if(newUnreadCount && foundDialog.top_message > maxId) {
foundDialog.unread_count = newUnreadCount;
@ -4242,10 +4239,7 @@ export class AppMessagesManager {
return pendingMessage;
}
public isPeerMuted(peerId: number) {
if(peerId == rootScope.myId) return false;
const dialog = this.getDialogByPeerId(peerId)[0];
public isDialogMuted(dialog: MTDialog.dialog) {
let muted = false;
if(dialog && dialog.notify_settings && dialog.notify_settings.mute_until) {
muted = new Date(dialog.notify_settings.mute_until * 1000) > new Date();
@ -4254,6 +4248,12 @@ export class AppMessagesManager {
return muted;
}
public isPeerMuted(peerId: number) {
if(peerId === rootScope.myId) return false;
return this.isDialogMuted(this.getDialogByPeerId(peerId)[0]);
}
public mutePeer(peerId: number) {
let inputPeer = appPeersManager.getInputPeerById(peerId);
let inputNotifyPeer: InputNotifyPeer.inputNotifyPeer = {
@ -4391,7 +4391,7 @@ export class AppMessagesManager {
this.maxSeenId = maxId;
AppStorage.set({max_seen_msg: maxId});
sessionStorage.set({max_seen_msg: maxId});
apiManager.invokeApi('messages.receivedMessages', {
max_id: this.getLocalMessageId(maxId)

View File

@ -3,15 +3,16 @@ import type { AppStickersManager } from './appStickersManager';
import { App, MOUNT_CLASS_TO, UserAuth } from '../mtproto/mtproto_config';
import EventListenerBase from '../../helpers/eventListenerBase';
import rootScope from '../rootScope';
import AppStorage from '../storage';
import sessionStorage from '../sessionStorage';
import { logger } from '../logger';
import type { AppUsersManager } from './appUsersManager';
import type { AppChatsManager } from './appChatsManager';
import type { AuthState } from '../../types';
import type FiltersStorage from '../storages/filters';
import type DialogsStorage from '../storages/dialogs';
import { copy, setDeepProperty, isObject, validateInitObject } from '../../helpers/object';
import { AppDraftsManager } from './appDraftsManager';
import type { AppDraftsManager } from './appDraftsManager';
import { copy, setDeepProperty, validateInitObject } from '../../helpers/object';
import { getHeavyAnimationPromise } from '../../hooks/useHeavyAnimationCheck';
const REFRESH_EVERY = 24 * 60 * 60 * 1000; // 1 day
const STATE_VERSION = App.version;
@ -34,7 +35,6 @@ export type State = Partial<{
recentEmoji: string[],
topPeers: number[],
recentSearch: number[],
stickerSets: AppStickersManager['stickerSets'],
version: typeof STATE_VERSION,
authState: AuthState,
hiddenPinnedMessages: {[peerId: string]: number},
@ -74,7 +74,6 @@ const STATE_INIT: State = {
recentEmoji: [],
topPeers: [],
recentSearch: [],
stickerSets: {},
version: STATE_VERSION,
authState: {
_: 'authStateSignIn'
@ -126,13 +125,13 @@ export class AppStateManager extends EventListenerBase<{
if(this.loaded) return this.loaded;
//console.time('load state');
return this.loaded = new Promise((resolve) => {
AppStorage.get<any>(...ALL_KEYS, 'user_auth').then((arr) => {
Promise.all(ALL_KEYS.concat('user_auth' as any).map(key => sessionStorage.get(key))).then((arr) => {
let state: State = {};
// ! then can't store false values
ALL_KEYS.forEach((key, idx) => {
const value = arr[idx];
if(value !== false) {
if(value !== undefined) {
// @ts-ignore
state[key] = value;
} else {
@ -166,7 +165,7 @@ export class AppStateManager extends EventListenerBase<{
//return resolve();
const auth: UserAuth = arr[arr.length - 1];
const auth: UserAuth = arr[arr.length - 1] as any;
if(auth) {
// ! Warning ! DON'T delete this
this.state.authState = {_: 'authStateSignedIn'};
@ -191,15 +190,20 @@ export class AppStateManager extends EventListenerBase<{
public saveState() {
if(this.state === undefined || this.savePromise) return;
const tempId = this.tempId;
this.savePromise = Promise.all(this.setListenerResult('save', this.state)).then(() => {
return AppStorage.set(this.state);
}).then(() => {
this.savePromise = null;
//return;
if(this.tempId !== tempId) {
this.saveState();
}
const tempId = this.tempId;
this.savePromise = getHeavyAnimationPromise().then(() => {
return Promise.all(this.setListenerResult('save', this.state))
.then(() => getHeavyAnimationPromise())
.then(() => sessionStorage.set(this.state))
.then(() => {
this.savePromise = null;
if(this.tempId !== tempId) {
this.saveState();
}
});
});
//let perf = performance.now();
@ -232,7 +236,7 @@ export class AppStateManager extends EventListenerBase<{
// @ts-ignore
this.state[i] = false;
}
AppStorage.set(this.state).then(() => {
sessionStorage.set(this.state).then(() => {
location.reload();
});
}

View File

@ -4,35 +4,20 @@ import apiManager from '../mtproto/mtprotoworker';
import { MOUNT_CLASS_TO } from '../mtproto/mtproto_config';
import rootScope from '../rootScope';
import appDocsManager from './appDocsManager';
import appStateManager from './appStateManager';
import AppStorage from '../storage';
// TODO: если пак будет сохранён и потом обновлён, то недостающие стикеры не подгрузит
export class AppStickersManager {
private stickerSets: {
[stickerSetId: string]: MessagesStickerSet
} = {};
private saveSetsTimeout: number;
private storage = new AppStorage<Record<string, MessagesStickerSet>>({
storeName: 'stickerSets'
});
private getStickerSetPromises: {[setId: string]: Promise<MessagesStickerSet>} = {};
private getStickersByEmoticonsPromises: {[emoticon: string]: Promise<Document[]>} = {};
constructor() {
appStateManager.getState().then(({stickerSets}) => {
if(stickerSets) {
for(let id in stickerSets) {
let set = stickerSets[id];
this.saveStickers(set.documents);
}
this.stickerSets = stickerSets;
}
//if(!this.stickerSets['emoji']) {
this.getStickerSet({id: 'emoji', access_hash: ''}, {overwrite: true});
//}
});
this.getStickerSet({id: 'emoji', access_hash: ''}, {overwrite: true});
rootScope.on('apiUpdate', (e) => {
const update = e;
@ -62,21 +47,29 @@ export class AppStickersManager {
}, params: Partial<{
overwrite: boolean
}> = {}): Promise<MessagesStickerSet> {
if(this.stickerSets[set.id] && !params.overwrite && this.stickerSets[set.id].documents?.length) return this.stickerSets[set.id];
if(this.getStickerSetPromises[set.id]) {
return this.getStickerSetPromises[set.id];
}
const promise = this.getStickerSetPromises[set.id] = apiManager.invokeApi('messages.getStickerSet', {
stickerset: this.getStickerSetInput(set)
});
const stickerSet = await promise;
delete this.getStickerSetPromises[set.id];
this.saveStickerSet(stickerSet, set.id);
return this.getStickerSetPromises[set.id] = new Promise(async(resolve, reject) => {
if(!params.overwrite) {
const cachedSet = await this.storage.get(set.id);
if(cachedSet && cachedSet.documents?.length) {
this.saveStickers(cachedSet.documents);
resolve(cachedSet);
return;
}
}
return stickerSet;
const stickerSet = await apiManager.invokeApi('messages.getStickerSet', {
stickerset: this.getStickerSetInput(set)
});
delete this.getStickerSetPromises[set.id];
this.saveStickerSet(stickerSet, set.id);
resolve(stickerSet);
});
}
public async getRecentStickers(): Promise<Modify<MessagesRecentStickers.messagesRecentStickers, {
@ -90,7 +83,7 @@ export class AppStickersManager {
}
public getAnimatedEmojiSticker(emoji: string) {
let stickerSet = this.stickerSets.emoji;
const stickerSet = this.storage.getFromCache('emoji');
if(!stickerSet || !stickerSet.documents) return undefined;
emoji = emoji.replace(/\ufe0f/g, '').replace(/🏻|🏼|🏽|🏾|🏿/g, '');
@ -108,30 +101,19 @@ export class AppStickersManager {
documents: res.documents as Document[]
};
if(this.stickerSets[id]) {
Object.assign(this.stickerSets[id], newSet);
let stickerSet = this.storage.getFromCache(id);
if(stickerSet) {
Object.assign(stickerSet, newSet);
} else {
this.stickerSets[id] = newSet;
stickerSet = this.storage.setToCache(id, newSet);
}
this.saveStickers(res.documents);
//console.log('stickers wrote', this.stickerSets);
if(this.saveSetsTimeout) return;
this.saveSetsTimeout = window.setTimeout(() => {
const savedSets: {[id: string]: MessagesStickerSet} = {};
for(const id in this.stickerSets) {
const set = this.stickerSets[id];
if(set.set.installed_date || id == 'emoji') {
savedSets[id] = set;
}
}
appStateManager.pushToState('stickerSets', savedSets);
appStateManager.saveState();
this.saveSetsTimeout = 0;
}, 100);
const needSave = stickerSet.set.installed_date || id === 'emoji';
if(needSave) this.storage.set({[id]: stickerSet});
else this.storage.remove(id);
}
public getStickerSetThumbDownloadOptions(stickerSet: StickerSet.stickerSet) {
@ -229,8 +211,9 @@ export class AppStickersManager {
});
const foundSaved: StickerSetCovered[] = [];
for(let id in this.stickerSets) {
const {set} = this.stickerSets[id];
const cache = this.storage.getCache();
for(let id in cache) {
const {set} = cache[id];
if(set.title.toLowerCase().includes(query.toLowerCase()) && !res.sets.find(c => c.set.id == set.id)) {
foundSaved.push({_: 'stickerSetCovered', set, cover: null});

439
src/lib/idb.ts Normal file
View File

@ -0,0 +1,439 @@
import { blobConstruct } from '../helpers/blob';
import { logger } from './logger';
import { Database } from './mtproto/mtproto_config';
/**
* https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/createIndex
*/
export type IDBIndex = {
indexName: string,
keyPath: string,
objectParameters: IDBIndexParameters
};
export type IDBStore = {
name: string,
indexes?: IDBIndex[]
};
export type IDBOptions = {
name?: string,
storeName: string,
stores?: IDBStore[],
version?: number
};
export default class IDBStorage {
public openDbPromise: Promise<IDBDatabase>;
public storageIsAvailable = true;
private log: ReturnType<typeof logger> = logger('IDB');
public name: string = Database.name;
public version: number = Database.version;
public stores: IDBStore[] = Database.stores;
public storeName: string;
constructor(options: IDBOptions) {
for(let i in options) {
// @ts-ignore
this[i] = options[i];
}
this.openDatabase(true);
}
public isAvailable() {
return this.storageIsAvailable;
}
public openDatabase(createNew = false): Promise<IDBDatabase> {
if(this.openDbPromise && !createNew) {
return this.openDbPromise;
}
const createObjectStore = (db: IDBDatabase, store: IDBStore) => {
const os = db.createObjectStore(store.name);
if(store.indexes?.length) {
for(const index of store.indexes) {
os.createIndex(index.indexName, index.keyPath, index.objectParameters);
}
}
};
try {
var request = indexedDB.open(this.name, this.version);
if(!request) {
throw new Error();
}
} catch(error) {
this.log.error('error opening db', error.message)
this.storageIsAvailable = false;
return Promise.reject(error);
}
let finished = false;
setTimeout(() => {
if(!finished) {
request.onerror({type: 'IDB_CREATE_TIMEOUT'} as Event);
}
}, 3000);
return this.openDbPromise = new Promise<IDBDatabase>((resolve, reject) => {
request.onsuccess = (event) => {
finished = true;
const db = request.result;
let calledNew = false;
this.log('Opened');
db.onerror = (error) => {
this.storageIsAvailable = false;
this.log.error('Error creating/accessing IndexedDB database', error);
reject(error);
};
db.onclose = (e) => {
this.log.error('closed:', e);
!calledNew && this.openDatabase();
};
db.onabort = (e) => {
this.log.error('abort:', e);
const transaction = e.target as IDBTransaction;
this.openDatabase(calledNew = true);
if(transaction.onerror) {
transaction.onerror(e);
}
db.close();
};
db.onversionchange = (e) => {
this.log.error('onversionchange, lol?');
};
resolve(db);
};
request.onerror = (event) => {
finished = true;
this.storageIsAvailable = false;
this.log.error('Error creating/accessing IndexedDB database', event);
reject(event);
};
request.onupgradeneeded = (event) => {
finished = true;
this.log.warn('performing idb upgrade from', event.oldVersion, 'to', event.newVersion);
// @ts-ignore
var db = event.target.result as IDBDatabase;
this.stores.forEach((store) => {
/* if(db.objectStoreNames.contains(store.name)) {
//if(event.oldVersion === 1) {
db.deleteObjectStore(store.name);
//}
} */
if(!db.objectStoreNames.contains(store.name)) {
createObjectStore(db, store);
}
});
};
});
}
public delete(entryName: string): Promise<void> {
//return Promise.resolve();
return this.openDatabase().then((db) => {
try {
//this.log('delete: `' + entryName + '`');
var objectStore = db.transaction([this.storeName], 'readwrite')
.objectStore(this.storeName);
var request = objectStore.delete(entryName);
} catch(error) {
return Promise.reject(error);
}
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
this.log.error('delete: request not finished!', entryName, request);
resolve();
}, 3000);
request.onsuccess = (event) => {
//this.log('delete: deleted file', event);
resolve();
clearTimeout(timeout);
};
request.onerror = (error) => {
reject(error);
clearTimeout(timeout);
};
});
});
}
public deleteAll() {
return this.openDatabase().then((db) => {
//this.log('deleteAll');
try {
const transaction = db.transaction([this.storeName], 'readwrite');
const objectStore = transaction.objectStore(this.storeName);
var request = objectStore.clear();
} catch(error) {
return Promise.reject(error);
}
return new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
this.log.error('deleteAll: request not finished', request);
}, 3000);
request.onsuccess = (event) => {
resolve();
clearTimeout(timeout);
};
request.onerror = (error) => {
reject(error);
clearTimeout(timeout);
};
});
});
}
public save(entryName: string, value: any) {
return this.openDatabase().then((db) => {
//this.log('save:', entryName, value);
const handleError = (error: Error) => {
this.log.error('save: transaction error:', entryName, value, db, error, error && error.name);
if((!error || error.name === 'InvalidStateError')/* && false */) {
setTimeout(() => {
this.save(entryName, value);
}, 2e3);
} else {
//console.error('IndexedDB saveFile transaction error:', error, error && error.name);
}
};
try {
const transaction = db.transaction([this.storeName], 'readwrite');
transaction.onerror = (e) => {
handleError(transaction.error);
};
/* transaction.oncomplete = (e) => {
this.log('save: transaction complete:', entryName);
}; */
/* transaction.addEventListener('abort', (e) => {
//handleError();
this.log.error('IndexedDB: save transaction abort!', transaction.error);
}); */
const objectStore = transaction.objectStore(this.storeName);
var request = objectStore.put(value, entryName);
} catch(error) {
handleError(error);
return Promise.reject(error);
/* this.storageIsAvailable = false;
throw error; */
}
return new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
this.log.error('save: request not finished', entryName, request);
}, 10000);
request.onsuccess = (event) => {
resolve();
clearTimeout(timeout);
};
request.onerror = (error) => {
reject(error);
clearTimeout(timeout);
};
});
});
}
public saveFile(fileName: string, blob: Blob | Uint8Array) {
//return Promise.resolve(blobConstruct([blob]));
if(!(blob instanceof Blob)) {
blob = blobConstruct([blob]) as Blob;
}
return this.save(fileName, blob);
}
/* public saveFileBase64(db: IDBDatabase, fileName: string, blob: Blob | any): Promise<Blob> {
if(this.getBlobSize(blob) > 10 * 1024 * 1024) {
return Promise.reject();
}
if(!(blob instanceof Blob)) {
var safeMimeType = blobSafeMimeType(blob.type || 'image/jpeg');
var address = 'data:' + safeMimeType + ';base64,' + bytesToBase64(blob);
return this.storagePutB64String(db, fileName, address).then(() => {
return blob;
});
}
try {
var reader = new FileReader();
} catch (e) {
this.storageIsAvailable = false;
return Promise.reject();
}
let promise = new Promise<Blob>((resolve, reject) => {
reader.onloadend = () => {
this.storagePutB64String(db, fileName, reader.result as string).then(() => {
resolve(blob);
}, reject);
}
reader.onerror = reject;
});
try {
reader.readAsDataURL(blob);
} catch (e) {
this.storageIsAvailable = false;
return Promise.reject();
}
return promise;
}
public storagePutB64String(db: IDBDatabase, fileName: string, b64string: string) {
try {
var objectStore = db.transaction([this.storeName], 'readwrite')
.objectStore(this.storeName);
var request = objectStore.put(b64string, fileName);
} catch(error) {
this.storageIsAvailable = false;
return Promise.reject(error);
}
return new Promise((resolve, reject) => {
request.onsuccess = function(event) {
resolve();
};
request.onerror = reject;
});
}
public getBlobSize(blob: any) {
return blob.size || blob.byteLength || blob.length;
} */
public get<T>(entryName: string): Promise<T> {
//return Promise.reject();
return this.openDatabase().then((db) => {
//this.log('get pre:', fileName);
try {
const transaction = db.transaction([this.storeName], 'readonly');
/* transaction.onabort = (e) => {
this.log.error('get transaction onabort?', e);
}; */
const objectStore = transaction.objectStore(this.storeName);
var request = objectStore.get(entryName);
//this.log.log('IDB get:', fileName, request);
} catch(err) {
this.log.error('get error:', err, entryName, request, request.error);
}
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
this.log.error('get request not finished!', entryName, request);
reject();
}, 3000);
request.onsuccess = function(event) {
const result = request.result;
if(result === undefined) {
reject('NO_ENTRY_FOUND');
} /* else if(typeof result === 'string' &&
result.substr(0, 5) === 'data:') {
resolve(dataUrlToBlob(result));
} */else {
resolve(result);
}
clearTimeout(timeout);
}
request.onerror = () => {
clearTimeout(timeout);
reject();
};
});
});
}
/* public getAllKeys(): Promise<Array<string>> {
console.time('getAllEntries');
return this.openDatabase().then((db) => {
var objectStore = db.transaction([this.storeName], 'readonly')
.objectStore(this.storeName);
var request = objectStore.getAllKeys();
return new Promise((resolve, reject) => {
request.onsuccess = function(event) {
// @ts-ignore
var result = event.target.result;
resolve(result);
console.timeEnd('getAllEntries');
}
request.onerror = reject;
});
});
} */
/* public isFileExists(fileName: string): Promise<boolean> {
console.time('isFileExists');
return this.openDatabase().then((db) => {
var objectStore = db.transaction([this.storeName], 'readonly')
.objectStore(this.storeName);
var request = objectStore.openCursor(fileName);
return new Promise((resolve, reject) => {
request.onsuccess = function(event) {
// @ts-ignore
var cursor = event.target.result;
resolve(!!cursor);
console.timeEnd('isFileExists');
}
request.onerror = reject;
});
});
} */
/* public getFileWriter(fileName: string, mimeType: string) {
var fakeWriter = FileManager.getFakeFileWriter(mimeType, (blob) => {
return this.saveFile(fileName, blob);
});
return Promise.resolve(fakeWriter);
} */
}

View File

@ -371,6 +371,13 @@ export class RLottiePlayer extends EventListenerBase<{
this.curFrame = this.direction == 1 ? 0 : frameCount - 1;
this.frameCount = frameCount;
this.fps = fps;
// * Handle 30fps stickers if 30fps set
if(this.fps < 60 && this.skipDelta !== 1) {
const diff = 60 / fps;
this.skipDelta = this.skipDelta / diff | 0;
}
this.frInterval = 1000 / this.fps / this.speed * this.skipDelta;
this.frThen = Date.now() - this.frInterval;
//this.sendQuery('renderFrame', 0);

View File

@ -118,7 +118,7 @@ export class MediaProgressLine extends RangeSelector {
}
protected setSeekMax() {
this.max = this.media.duration;
this.max = this.media.duration || 0;
if(this.max > 0) {
this.onLoadedData();
} else {

View File

@ -1,4 +1,4 @@
import AppStorage from '../storage';
import sessionStorage from '../sessionStorage';
import MTPNetworker, { MTMessage } from './networker';
import { isObject } from './bin_utils';
@ -92,7 +92,7 @@ export class ApiManager {
// mtpSetUserAuth
public setUserAuth(userId: number) {
AppStorage.set({
sessionStorage.set({
user_auth: userId
});
@ -106,26 +106,25 @@ export class ApiManager {
public setBaseDcId(dcId: number) {
this.baseDcId = dcId;
AppStorage.set({
sessionStorage.set({
dc: this.baseDcId
});
}
// mtpLogOut
public async logOut() {
let storageKeys: Array<string> = [];
let prefix = Modes.test ? 't_dc' : 'dc';
const storageKeys: Array<string> = [];
const prefix = 'dc';
for(let dcId = 1; dcId <= 5; dcId++) {
storageKeys.push(prefix + dcId + '_auth_key');
//storageKeys.push(prefix + dcId + '_auth_keyId');
}
// WebPushApiManager.forceUnsubscribe(); // WARNING
let storageResult = await AppStorage.get<string[]|boolean[]>(...storageKeys);
const storageResult = await Promise.all(storageKeys.map(key => sessionStorage.get(key as any)));
let logoutPromises = [];
const logoutPromises = [];
for(let i = 0; i < storageResult.length; i++) {
if(storageResult[i]) {
logoutPromises.push(this.invokeApi('auth.logOut', {}, {dcId: i + 1, ignoreErrors: true}));
@ -137,7 +136,7 @@ export class ApiManager {
this.baseDcId = 0;
//this.telegramMeNotify(false);
const promise = AppStorage.clear();
const promise = sessionStorage.clear();
promise.finally(() => {
self.postMessage({type: 'reload'});
});
@ -198,7 +197,7 @@ export class ApiManager {
const akId = 'dc' + dcId + '_auth_keyId';
const ss = 'dc' + dcId + '_server_salt';
return this.gettingNetworkers[getKey] = AppStorage.get<string[]>(ak, akId, ss)
return this.gettingNetworkers[getKey] = Promise.all([ak, akId, ss].map(key => sessionStorage.get(key as any)))
.then(async([authKeyHex, authKeyIdHex, serverSaltHex]) => {
const transport = dcConfigurator.chooseServer(dcId, connectionType, transportType, false);
let networker: MTPNetworker;
@ -222,7 +221,7 @@ export class ApiManager {
[ss]: bytesToHex(auth.serverSalt)
};
AppStorage.set(storeObj);
sessionStorage.set(storeObj);
networker = networkerFactory.getNetworker(dcId, auth.authKey, auth.authKeyId, auth.serverSalt, transport, options);
} catch(error) {
@ -331,7 +330,8 @@ export class ApiManager {
if(error.code == 401 && this.baseDcId == dcId) {
if(error.type != 'SESSION_PASSWORD_NEEDED') {
AppStorage.remove('dc', 'user_auth'); // ! возможно тут вообще не нужно это делать, но нужно проверить случай с USER_DEACTIVATED (https://core.telegram.org/api/errors)
sessionStorage.remove('dc')
sessionStorage.remove('user_auth'); // ! возможно тут вообще не нужно это делать, но нужно проверить случай с USER_DEACTIVATED (https://core.telegram.org/api/errors)
//this.telegramMeNotify(false);
}
@ -406,7 +406,7 @@ export class ApiManager {
if(dcId = (options.dcId || this.baseDcId)) {
this.getNetworker(dcId, options).then(performRequest, rejectPromise);
} else {
AppStorage.get<number>('dc').then((baseDcId) => {
sessionStorage.get('dc').then((baseDcId) => {
this.getNetworker(this.baseDcId = dcId = baseDcId || App.baseDcId, options).then(performRequest, rejectPromise);
});
}

View File

@ -1,4 +1,4 @@
import AppStorage from '../storage';
import sessionStorage from '../sessionStorage';
import { Modes, App } from './mtproto_config';
/* import PasswordManager from './passwordManager';
@ -11,7 +11,7 @@ import NetworkerFactory from './networkerFactory';
import ApiManager from './apiManager';
import ApiFileManager from './apiFileManager'; */
export class TelegramMeWebService {
/* export class TelegramMeWebService {
public disabled = Modes.test ||
App.domains.indexOf(location.hostname) == -1 ||
location.protocol != 'http:' && location.protocol != 'https:' ||
@ -22,7 +22,7 @@ export class TelegramMeWebService {
return false;
}
AppStorage.get<any>('tgme_sync').then((curValue) => {
sessionStorage.get('tgme_sync').then((curValue) => {
var ts = Date.now() / 1000;
if(canRedirect &&
curValue &&
@ -31,7 +31,7 @@ export class TelegramMeWebService {
return false;
}
AppStorage.set({tgme_sync: {canRedirect: canRedirect, ts: ts}});
sessionStorage.set({tgme_sync: {canRedirect: canRedirect, ts: ts}});
var urls = [
'//telegram.me/_websync_?authed=' + (canRedirect ? '1' : '0'),
@ -50,7 +50,7 @@ export class TelegramMeWebService {
}
}
export const telegramMeWebService = new TelegramMeWebService();
export const telegramMeWebService = new TelegramMeWebService(); */
/* export namespace MTProto {
//$($window).on('click keydown', rng_seed_time); // WARNING!

View File

@ -1,3 +1,5 @@
import { IDBIndex, IDBStore } from "../idb";
export type UserAuth = number;
export const REPLIES_PEER_ID = 1271266957;
@ -10,6 +12,18 @@ export const App = {
baseDcId: 2
};
export type DatabaseStoreName = 'session' | 'stickerSets';
export type DatabaseStore = Omit<IDBStore, 'name'> & {name: DatabaseStoreName};
export const Database = {
name: 'tweb',
version: 5,
stores: [{
name: 'session'
}, {
name: 'stickerSets'
}] as DatabaseStore[],
};
export const Modes = {
test: location.search.indexOf('test=1') > 0/* || true */,
debug: location.search.indexOf('debug=1') > 0,

View File

@ -5,7 +5,6 @@ import type { InvokeApiOptions } from '../../types';
import CryptoWorkerMethods from '../crypto/crypto_methods';
import { logger } from '../logger';
import rootScope from '../rootScope';
import AppStorage from '../storage';
import webpWorkerController from '../webp/webpWorkerController';
import type { DownloadOptions } from './apiFileManager';
import { ApiError } from './apiManager';
@ -13,6 +12,7 @@ import type { ServiceWorkerTask, ServiceWorkerTaskResponse } from './mtproto.ser
import { MOUNT_CLASS_TO, UserAuth } from './mtproto_config';
import type { MTMessage } from './networker';
import referenceDatabase from './referenceDatabase';
import appDocsManager from '../appManagers/appDocsManager';
type Task = {
taskId: number,
@ -52,6 +52,8 @@ export class ApiManagerProxy extends CryptoWorkerMethods {
private hashes: {[method: string]: HashOptions} = {};
private isSWRegistered = true;
constructor() {
super();
this.log('constructor');
@ -60,13 +62,19 @@ export class ApiManagerProxy extends CryptoWorkerMethods {
this.registerWorker();
}
public isServiceWorkerOnline() {
return this.isSWRegistered;
}
private registerServiceWorker() {
if(!('serviceWorker' in navigator)) return;
navigator.serviceWorker.register('./sw.js', {scope: './'}).then(registration => {
this.isSWRegistered = true;
}, (err) => {
this.isSWRegistered = false;
this.log.error('SW registration failed!', err);
appDocsManager.onServiceWorkerFail();
});
navigator.serviceWorker.ready.then((registration) => {
@ -107,6 +115,8 @@ export class ApiManagerProxy extends CryptoWorkerMethods {
}
private registerWorker() {
//return;
const worker = new MTProtoWorker();
worker.addEventListener('message', (e) => {
if(!this.worker) {

View File

@ -2,7 +2,7 @@ import {isObject} from './bin_utils';
import { bigStringInt} from './bin_utils';
import {TLDeserialization, TLSerialization} from './tl_utils';
import CryptoWorker from '../crypto/cryptoworker';
import AppStorage from '../storage';
import sessionStorage from '../sessionStorage';
import Schema from './schema';
import timeManager from './timeManager';
import NetworkerFactory from './networkerFactory';
@ -331,7 +331,7 @@ export default class MTPNetworker {
return false;
}
AppStorage.get<number>('dc').then((baseDcId: number) => {
sessionStorage.get('dc').then((baseDcId) => {
if(isClean && (
baseDcId != this.dcId ||
this.isFileNetworker ||
@ -1030,7 +1030,7 @@ export default class MTPNetworker {
public applyServerSalt(newServerSalt: string) {
const serverSalt = longToBytes(newServerSalt);
AppStorage.set({
sessionStorage.set({
['dc' + this.dcId + '_server_salt']: bytesToHex(serverSalt)
});
@ -1260,7 +1260,7 @@ export default class MTPNetworker {
this.processMessageAck(message.first_msg_id);
this.applyServerSalt(message.server_salt);
AppStorage.get<number>('dc').then((baseDcId: number) => {
sessionStorage.get('dc').then((baseDcId) => {
if(baseDcId == this.dcId && !this.isFileNetworker && NetworkerFactory.updatesProcessor) {
NetworkerFactory.updatesProcessor(message);
}

View File

@ -1,5 +1,5 @@
import { tsNow } from '../../helpers/date';
import AppStorage from '../storage';
import sessionStorage from '../sessionStorage';
export class ServerTimeManager {
public timestampNow = tsNow(true);
@ -17,7 +17,7 @@ export class ServerTimeManager {
constructor() {
this.midnightOffseted.setHours(0, 0, 0, 0);
AppStorage.get<number>('server_time_offset').then((to) => {
sessionStorage.get('server_time_offset').then((to) => {
if(to) {
this.serverTimeOffset = to;
this.timeParams.serverTimeOffset = to;

View File

@ -1,4 +1,4 @@
import AppStorage from '../storage';
import sessionStorage from '../sessionStorage';
import { longFromInts } from './bin_utils';
import { nextRandomInt } from '../../helpers/random';
@ -7,7 +7,7 @@ export class TimeManager {
private timeOffset = 0;
constructor() {
AppStorage.get('server_time_offset').then((to: any) => {
sessionStorage.get('server_time_offset').then((to: any) => {
if(to) {
this.timeOffset = to;
}
@ -39,7 +39,7 @@ export class TimeManager {
localTime = (localTime || Date.now()) / 1000 | 0;
const newTimeOffset = serverTime - localTime;
const changed = Math.abs(this.timeOffset - newTimeOffset) > 10;
AppStorage.set({
sessionStorage.set({
server_time_offset: newTimeOffset
});

19
src/lib/sessionStorage.ts Normal file
View File

@ -0,0 +1,19 @@
import { MOUNT_CLASS_TO } from './mtproto/mtproto_config';
import AppStorage from './storage';
import { State } from './appManagers/appStateManager';
const sessionStorage = new AppStorage<{
dc: number,
user_auth: number,
dc1_auth_key: any,
dc2_auth_key: any,
dc3_auth_key: any,
dc4_auth_key: any,
dc5_auth_key: any,
max_seen_msg: number,
server_time_offset: number
} & State>({
storeName: 'session'
});
MOUNT_CLASS_TO && (MOUNT_CLASS_TO.appStorage = sessionStorage);
export default sessionStorage;

View File

@ -1,95 +1,80 @@
import CacheStorageController from './cacheStorage';
import { DEBUG, MOUNT_CLASS_TO } from './mtproto/mtproto_config';
//import { stringify } from '../helpers/json';
import IDBStorage, { IDBOptions } from "./idb";
import { DatabaseStore, DatabaseStoreName } from "./mtproto/mtproto_config";
class AppStorage {
private cacheStorage = new CacheStorageController('session');
export default class AppStorage<Storage extends Record<string, any>/* Storage extends {[name: string]: any} *//* Storage extends Record<string, any> */> {
private storage: IDBStorage;//new CacheStorageController('session');
//public noPrefix = false;
private cache: {[key: string]: any} = {};
private useCS = true;
constructor() {
//private cache: Partial<{[key: string]: Storage[typeof key]}> = {};
private cache: Partial<Storage> = {};
private useStorage = true;
constructor(storageOptions: Omit<IDBOptions, 'storeName' | 'stores'> & {stores?: DatabaseStore[], storeName: DatabaseStoreName}) {
this.storage = new IDBStorage(storageOptions);
}
public storageGetPrefix() {
/* if(this.noPrefix) {
this.noPrefix = false;
return '';
} */
return '';
//return this.keyPrefix;
public getCache() {
return this.cache;
}
public async get<T>(...keys: string[]): Promise<T> {
const single = keys.length === 1;
const result: any[] = [];
const prefix = this.storageGetPrefix();
for(let key of keys) {
key = prefix + key;
if(this.cache.hasOwnProperty(key)) {
result.push(this.cache[key]);
} else if(this.useCS) {
let value: any;
try {
value = await this.cacheStorage.getFile(key, 'text');
value = JSON.parse(value);
} catch(e) {
if(e !== 'NO_ENTRY_FOUND') {
this.useCS = false;
value = undefined;
console.error('[AS]: get error:', e, key, value);
}
public getFromCache(key: keyof Storage) {
return this.cache[key];
}
public setToCache(key: keyof Storage, value: Storage[typeof key]) {
return this.cache[key] = value;
}
public async get(key: keyof Storage): Promise<Storage[typeof key]> {
if(this.cache.hasOwnProperty(key)) {
return this.getFromCache(key);
} else if(this.useStorage) {
let value: any;
try {
value = await this.storage.get(key as string);
//console.log('[AS]: get result:', key, value);
//value = JSON.parse(value);
} catch(e) {
if(e !== 'NO_ENTRY_FOUND') {
this.useStorage = false;
value = undefined;
console.error('[AS]: get error:', e, key, value);
}
// const str = `[get] ${keys.join(', ')}`;
// console.time(str);
try {
value = (value === undefined || value === null) ? false : value;
} catch(e) {
value = false;
}
//console.timeEnd(str);
result.push(this.cache[key] = value);
} else {
throw 'something went wrong';
}
return this.cache[key] = value;
} else {
throw 'something went wrong';
}
return single ? result[0] : result;
}
public async set(obj: any) {
const prefix = this.storageGetPrefix();
public async set(obj: Partial<Storage>) {
//console.log('storageSetValue', obj, callback, arguments);
for(let key in obj) {
if(obj.hasOwnProperty(key)) {
let value = obj[key];
key = prefix + key;
this.cache[key] = value;
this.setToCache(key, value);
let perf = /* DEBUG */false ? performance.now() : 0;
value = JSON.stringify(value);
// let perf = /* DEBUG */false ? performance.now() : 0;
// value = JSON.stringify(value);
if(perf) {
let elapsedTime = performance.now() - perf;
if(elapsedTime > 10) {
console.warn('LocalStorage set: stringify time by JSON.stringify:', elapsedTime, key);
}
}
// if(perf) {
// let elapsedTime = performance.now() - perf;
// if(elapsedTime > 10) {
// console.warn('LocalStorage set: stringify time by JSON.stringify:', elapsedTime, key);
// }
// }
/* perf = performance.now();
value = stringify(value);
console.log('LocalStorage set: stringify time by own stringify:', performance.now() - perf); */
if(this.useCS) {
if(this.useStorage) {
try {
//console.log('setItem: will set', key/* , value */);
//await this.cacheStorage.delete(key); // * try to prevent memory leak in Chrome leading to 'Unexpected internal error.'
await this.cacheStorage.save(key, new Response(value, {headers: {'Content-Type': 'application/json'}}));
//await this.storage.save(key, new Response(value, {headers: {'Content-Type': 'application/json'}}));
await this.storage.save(key, value);
//console.log('setItem: have set', key/* , value */);
} catch(e) {
//this.useCS = false;
@ -100,31 +85,19 @@ class AppStorage {
}
}
public async remove(...keys: any[]) {
if(!Array.isArray(keys)) {
keys = Array.prototype.slice.call(arguments);
}
const prefix = this.storageGetPrefix();
for(let i = 0; i < keys.length; i++) {
const key = keys[i] = prefix + keys[i];
delete this.cache[key];
if(this.useCS) {
try {
await this.cacheStorage.delete(key);
} catch(e) {
this.useCS = false;
console.error('[AS]: remove error:', e);
}
public async remove(key: keyof Storage) {
delete this.cache[key];
if(this.useStorage) {
try {
await this.storage.delete(key as string);
} catch(e) {
this.useStorage = false;
console.error('[AS]: remove error:', e);
}
}
}
public clear() {
return this.cacheStorage.deleteAll();
return this.storage.deleteAll();
}
}
const appStorage = new AppStorage();
MOUNT_CLASS_TO && (MOUNT_CLASS_TO.appStorage = appStorage);
export default appStorage;

View File

@ -157,6 +157,10 @@ let onFirstMount = (): Promise<any> => {
err.handled = true;
await pagePassword.mount();
break;
case 'PHONE_CODE_EXPIRED':
codeInput.classList.add('error');
codeInputLabel.innerText = 'Code expired';
break;
case 'PHONE_CODE_EMPTY':
case 'PHONE_CODE_INVALID':
codeInput.classList.add('error');

View File

@ -14,7 +14,7 @@ class PagesManager {
constructor() {
this.pagesDiv = document.getElementById('auth-pages') as HTMLDivElement;
this.selectTab = horizontalMenu(null, this.pagesDiv.firstElementChild.firstElementChild as HTMLDivElement, null, () => {
if(this.page.onShown) {
if(this.page?.onShown) {
this.page.onShown();
}
});

View File

@ -13,178 +13,17 @@
overflow: hidden;
border-radius: 50%;
background-color: $color-blue;
font-size: 0;
align-items: center;
}
&-toggle {
transform: rotate(-119deg);
transition: transform .25s;
}
.tgico-largeplay {
.part {
position: absolute;
background-color: white;
@include respond-to(not-handhelds) {
height: 136px;
width: 136px;
}
@include respond-to(handhelds) {
height: 92px;
width: 92px;
}
&.one {
clip-path: polygon(
43.77666% 55.85251%,
43.77874% 55.46331%,
43.7795% 55.09177%,
43.77934% 54.74844%,
43.77855% 54.44389%,
43.77741% 54.18863%,
43.77625% 53.99325%,
43.77533% 53.86828%,
43.77495% 53.82429%,
43.77518% 53.55329%,
43.7754% 53.2823%,
43.77563% 53.01131%,
43.77585% 52.74031%,
43.77608% 52.46932%,
43.7763% 52.19832%,
43.77653% 51.92733%,
43.77675% 51.65633%,
43.77653% 51.38533%,
43.7763% 51.11434%,
43.77608% 50.84334%,
43.77585% 50.57235%,
43.77563% 50.30136%,
43.7754% 50.03036%,
43.77518% 49.75936%,
43.77495% 49.48837%,
44.48391% 49.4885%,
45.19287% 49.48865%,
45.90183% 49.48878%,
46.61079% 49.48892%,
47.31975% 49.48906%,
48.0287% 49.4892%,
48.73766% 49.48934%,
49.44662% 49.48948%,
50.72252% 49.48934%,
51.99842% 49.4892%,
53.27432% 49.48906%,
54.55022% 49.48892%,
55.82611% 49.48878%,
57.10201% 49.48865%,
58.3779% 49.4885%,
59.6538% 49.48837%,
59.57598% 49.89151%,
59.31883% 50.28598%,
58.84686% 50.70884%,
58.12456% 51.19714%,
57.11643% 51.78793%,
55.78697% 52.51828%,
54.10066% 53.42522%,
52.02202% 54.54581%,
49.96525% 55.66916%,
48.3319% 56.57212%,
47.06745% 57.27347%,
46.11739% 57.79191%,
45.42719% 58.14619%,
44.94235% 58.35507%,
44.60834% 58.43725%,
44.37066% 58.41149%,
44.15383% 58.27711%,
43.99617% 58.0603%,
43.88847% 57.77578%,
43.82151% 57.43825%,
43.78608% 57.06245%,
43.77304% 56.66309%,
43.773% 56.25486%
);
transition: clip-path 250ms;
}
&.two {
clip-path: polygon(
43.77666% 43.83035%,
43.77874% 44.21955%,
43.7795% 44.59109%,
43.77934% 44.93442%,
43.77855% 45.23898%,
43.77741% 45.49423%,
43.77625% 45.68961%,
43.77533% 45.81458%,
43.77495% 45.85858%,
43.77518% 46.12957%,
43.7754% 46.40056%,
43.77563% 46.67156%,
43.77585% 46.94255%,
43.77608% 47.21355%,
43.7763% 47.48454%,
43.77653% 47.75554%,
43.77675% 48.02654%,
43.77653% 48.29753%,
43.7763% 48.56852%,
43.77608% 48.83952%,
43.77585% 49.11051%,
43.77563% 49.38151%,
43.7754% 49.65251%,
43.77518% 49.9235%,
43.77495% 50.1945%,
44.48391% 50.19436%,
45.19287% 50.19422%,
45.90183% 50.19408%,
46.61079% 50.19394%,
47.31975% 50.1938%,
48.0287% 50.19366%,
48.73766% 50.19353%,
49.44662% 50.19338%,
50.72252% 50.19353%,
51.99842% 50.19366%,
53.27432% 50.1938%,
54.55022% 50.19394%,
55.82611% 50.19408%,
57.10201% 50.19422%,
58.3779% 50.19436%,
59.6538% 50.1945%,
59.57598% 49.79136%,
59.31883% 49.39688%,
58.84686% 48.97402%,
58.12456% 48.48572%,
57.11643% 47.89493%,
55.78697% 47.16458%,
54.10066% 46.25764%,
52.02202% 45.13705%,
49.96525% 44.01371%,
48.3319% 43.11074%,
47.06745% 42.4094%,
46.11739% 41.89096%,
45.42719% 41.53667%,
44.94235% 41.3278%,
44.60834% 41.24561%,
44.37066% 41.27137%,
44.15383% 41.40575%,
43.99617% 41.62256%,
43.88847% 41.90709%,
43.82151% 42.24461%,
43.78608% 42.62041%,
43.77304% 43.01978%,
43.773% 43.428%
);
transition: clip-path 250ms;
}
}
}
.tgico-largepause {
transform: rotate(-90deg);
.part {
position: absolute;
background-color: white;
@include respond-to(not-handhelds) {
height: 140px;
width: 140px;
@ -332,6 +171,166 @@
56.87342% 42.46916%
);
}
&.one, &.two {
transition: clip-path .25s;
}
}
}
&-toggle.playing {
transform: rotate(-90deg);
}
&-toggle:not(.playing) {
.part {
@include respond-to(not-handhelds) {
height: 136px;
width: 136px;
}
@include respond-to(handhelds) {
height: 92px;
width: 92px;
}
&.one {
clip-path: polygon(
43.77666% 55.85251%,
43.77874% 55.46331%,
43.7795% 55.09177%,
43.77934% 54.74844%,
43.77855% 54.44389%,
43.77741% 54.18863%,
43.77625% 53.99325%,
43.77533% 53.86828%,
43.77495% 53.82429%,
43.77518% 53.55329%,
43.7754% 53.2823%,
43.77563% 53.01131%,
43.77585% 52.74031%,
43.77608% 52.46932%,
43.7763% 52.19832%,
43.77653% 51.92733%,
43.77675% 51.65633%,
43.77653% 51.38533%,
43.7763% 51.11434%,
43.77608% 50.84334%,
43.77585% 50.57235%,
43.77563% 50.30136%,
43.7754% 50.03036%,
43.77518% 49.75936%,
43.77495% 49.48837%,
44.48391% 49.4885%,
45.19287% 49.48865%,
45.90183% 49.48878%,
46.61079% 49.48892%,
47.31975% 49.48906%,
48.0287% 49.4892%,
48.73766% 49.48934%,
49.44662% 49.48948%,
50.72252% 49.48934%,
51.99842% 49.4892%,
53.27432% 49.48906%,
54.55022% 49.48892%,
55.82611% 49.48878%,
57.10201% 49.48865%,
58.3779% 49.4885%,
59.6538% 49.48837%,
59.57598% 49.89151%,
59.31883% 50.28598%,
58.84686% 50.70884%,
58.12456% 51.19714%,
57.11643% 51.78793%,
55.78697% 52.51828%,
54.10066% 53.42522%,
52.02202% 54.54581%,
49.96525% 55.66916%,
48.3319% 56.57212%,
47.06745% 57.27347%,
46.11739% 57.79191%,
45.42719% 58.14619%,
44.94235% 58.35507%,
44.60834% 58.43725%,
44.37066% 58.41149%,
44.15383% 58.27711%,
43.99617% 58.0603%,
43.88847% 57.77578%,
43.82151% 57.43825%,
43.78608% 57.06245%,
43.77304% 56.66309%,
43.773% 56.25486%
);
}
&.two {
clip-path: polygon(
43.77666% 43.83035%,
43.77874% 44.21955%,
43.7795% 44.59109%,
43.77934% 44.93442%,
43.77855% 45.23898%,
43.77741% 45.49423%,
43.77625% 45.68961%,
43.77533% 45.81458%,
43.77495% 45.85858%,
43.77518% 46.12957%,
43.7754% 46.40056%,
43.77563% 46.67156%,
43.77585% 46.94255%,
43.77608% 47.21355%,
43.7763% 47.48454%,
43.77653% 47.75554%,
43.77675% 48.02654%,
43.77653% 48.29753%,
43.7763% 48.56852%,
43.77608% 48.83952%,
43.77585% 49.11051%,
43.77563% 49.38151%,
43.7754% 49.65251%,
43.77518% 49.9235%,
43.77495% 50.1945%,
44.48391% 50.19436%,
45.19287% 50.19422%,
45.90183% 50.19408%,
46.61079% 50.19394%,
47.31975% 50.1938%,
48.0287% 50.19366%,
48.73766% 50.19353%,
49.44662% 50.19338%,
50.72252% 50.19353%,
51.99842% 50.19366%,
53.27432% 50.1938%,
54.55022% 50.19394%,
55.82611% 50.19408%,
57.10201% 50.19422%,
58.3779% 50.19436%,
59.6538% 50.1945%,
59.57598% 49.79136%,
59.31883% 49.39688%,
58.84686% 48.97402%,
58.12456% 48.48572%,
57.11643% 47.89493%,
55.78697% 47.16458%,
54.10066% 46.25764%,
52.02202% 45.13705%,
49.96525% 44.01371%,
48.3319% 43.11074%,
47.06745% 42.4094%,
46.11739% 41.89096%,
45.42719% 41.53667%,
44.94235% 41.3278%,
44.60834% 41.24561%,
44.37066% 41.27137%,
44.15383% 41.40575%,
43.99617% 41.62256%,
43.88847% 41.90709%,
43.82151% 42.24461%,
43.78608% 42.62041%,
43.77304% 43.01978%,
43.773% 43.428%
);
}
}
}
@ -450,7 +449,6 @@
--border-radius: 4px;
flex: 1 1 auto;
margin-left: 5px;
margin-top: -1px;
&__filled {
background-color: #0089ff;

View File

@ -6,13 +6,12 @@ avatar-element {
color: #fff;
width: var(--size);
height: var(--size);
line-height: var(--size);
line-height: var(--size) !important;
border-radius: 50%;
background: linear-gradient(var(--color-top), var(--color-bottom));
text-align: center;
font-size: calc(1.25rem / var(--multiplier));
/* overflow: hidden; */
position: relative;
user-select: none;
text-transform: uppercase;
font-weight: 700;
@ -52,17 +51,16 @@ avatar-element {
}
&.tgico-avatar_deletedaccount:before {
font-size: calc(56px / var(--multiplier));
font-size: calc(54px / var(--multiplier));
}
&.tgico-calendarfilter:before {
font-size: calc(32px / var(--multiplier));
font-size: calc(36px / var(--multiplier));
}
&:before {
line-height: inherit !important;
}
/* kostil */
display: flex;
align-items: center;
justify-content: center;
img {
width: 100%;
@ -79,17 +77,21 @@ avatar-element {
fill: white;
}
&.is-online:after {
position: absolute;
content: " ";
display: block;
border-radius: 50%;
border: 2px solid white;
background-color: #0ac630;
left: 74%;
top: 73%;
width: 14px;
height: 14px;
&.is-online {
position: relative;
&:after {
position: absolute;
content: " ";
display: block;
border-radius: 50%;
border: 2px solid white;
background-color: #0ac630;
width: 14px;
height: 14px;
left: 2.4375rem;
top: 2.4375rem;
}
}
&[clickable] {

View File

@ -4,6 +4,7 @@
color: white;
font-size: .875rem;
transition: background-color .2s ease-in-out;
text-align: center;
&:not(.tgico):empty {
display: none;

View File

@ -1,7 +1,7 @@
.btn-icon {
text-align: center;
font-size: 1.5rem;
line-height: 1.5rem;
line-height: 1;
border-radius: 50% !important;
transition: background-color .15s ease-in-out, opacity .15s ease-in-out;
color: $color-gray;
@ -37,10 +37,11 @@
}
.btn-corner {
--translateY: calc(100% + 20px);
--offset: 1.25rem;
--translateY: calc(54px + var(--offset));
position: absolute !important;
bottom: 20px;
right: 20px;
bottom: var(--offset);
right: var(--offset);
//transition: .2s ease;
transition: transform var(--btn-corner-transition) !important;
transform: translate3d(0, var(--translateY), 0);
@ -235,7 +236,7 @@
transition: none;
}
svg, use {
> svg, use {
height: calc(100% - 20px);
right: 15px;
left: auto;

View File

@ -113,7 +113,7 @@ $chat-helper-size: 39px;
background: none;
border: none;
width: 100%;
padding: 9px 8px 9px 8px;
padding: 9px 8px;
/* height: 100%; */
max-height: calc(30rem - var(--padding-vertical) * 2);
overflow-y: none;
@ -123,6 +123,7 @@ $chat-helper-size: 39px;
white-space: pre-wrap;
font-size: var(--messages-text-size);
line-height: 1.3125;
transition: height .1s;
@media only screen and (max-height: 30rem) {
max-height: unquote('max(39px, calc(100vh - 10rem))');
@ -131,10 +132,6 @@ $chat-helper-size: 39px;
@include respond-to(handhelds) {
max-height: 10rem;
}
/* span.emoji {
font-size: .95rem;
} */
}
.toggle-emoticons {
@ -170,19 +167,28 @@ $chat-helper-size: 39px;
}
.btn-scheduled {
margin-right: .75rem !important; // * maybe this is correct margin
position: absolute;
right: 3.625rem;
animation: grow-icon .4s forwards ease-in-out !important;
align-self: center;
display: none !important;
margin: 0 !important;
&:after {
content: "";
position: absolute;
top: 5px;
right: 5px;
//border: .1875rem solid #fff;
box-sizing: initial;
width: .5rem;
height: .5rem;
border-radius: 50%;
background: #61c642;
//box-shadow: -0.375rem -0.25rem 0 -0.1875rem #fff;
}
}
&:not(.is-recording) {
.btn-scheduled.show:not(.hide) {
display: flex !important;
}
}
@ -797,10 +803,6 @@ $chat-helper-size: 39px;
//padding-bottom: 4.5px;
align-items: flex-end;
min-height: var(--chat-input-size);
.btn-icon:before {
vertical-align: bottom;
}
}
.input-message-container {
@ -814,10 +816,6 @@ $chat-helper-size: 39px;
> .scrollable {
position: relative;
}
& ~ .btn-icon {
}
}
.selection-container {
@ -898,29 +896,16 @@ $chat-helper-size: 39px;
}
.btn-icon {
display: block;
flex: 0 0 auto;
font-size: 24px;
line-height: 24px;
color: #8d969c;
//margin-bottom: 1px;
// ! EXPERIMENTAL
margin: 0 .125rem 5px;
padding: 0.25rem;
padding: 0;
width: 34px;
height: 34px;
//По макету ниже....
// display: block;
// flex: 0 0 auto;
// font-size: 24px;
// line-height: 24px;
// color: #8d969c;
// padding: 0.25rem;
// margin-left: -1px;
// width: 40px;
// height: 40px;
&.active {
color: $color-blue;
}
@ -1040,13 +1025,13 @@ $chat-helper-size: 39px;
//> .bubbles-transform-helper {
// ! these lines will blur messages if chat input helper is active
//> .scrollable {
-webkit-mask-image: -webkit-linear-gradient(bottom, transparent, #000 28px);
mask-image: linear-gradient(0deg, transparent 0, #000 28px);
//-webkit-mask-image: -webkit-linear-gradient(bottom, transparent, #000 28px);
//mask-image: linear-gradient(0deg, transparent 0, #000 28px);
//}
& + .chat-input .bubbles-go-down {
cursor: pointer;
--translateY: 0;
//--translateY: 0;
opacity: 1;
/* &.is-broadcast {
@ -1163,10 +1148,12 @@ $chat-helper-size: 39px;
cursor: default;
opacity: 0;
z-index: 2;
transition: transform var(--layer-transition), opacity var(--layer-transition) !important;
//transition: transform var(--layer-transition), opacity var(--layer-transition) !important;
overflow: visible;
--translateY: calc(var(--chat-input-size) + 10px);
//--translateY: calc(var(--chat-input-size) + 10px);
//--translateY: calc(100% + 10px);
transition: opacity var(--layer-transition) !important;
transform: none !important;
body.animation-level-0 & {
transition: none !important;

View File

@ -63,10 +63,13 @@ $bubble-margin: .25rem;
margin: 0 auto $bubble-margin;
user-select: none;
display: flex;
flex-wrap: wrap;
//flex-direction: column; // fix 'Unread messages', still need to refactor it
--background-color: #fff;
--accent-color: $color-blue;
--secondary-color: $color-gray;
--highlightning-color: rgba(77, 142, 80, .4);
&.is-highlighted, &.is-selected, /* .bubbles.is-selecting */ & {
&:after {
@ -103,7 +106,7 @@ $bubble-margin: .25rem;
&.is-highlighted:after {
//background-color: rgba(0, 132, 255, .3);
background-color: rgba(77, 142, 80, .4);
background-color: var(--highlightning-color);
body:not(.animation-level-0) & {
animation: bubbleSelected 2s linear;
@ -116,16 +119,17 @@ $bubble-margin: .25rem;
}
&:before {
content: "Unread messages";
height: 30px;
margin-bottom: $bubble-margin;
margin-left: -50%;
text-align: center;
color: #538BCC;
line-height: 2.1;
font-weight: 500;
font-size: 15px;
background-color: rgba(255, 255, 255, .95);
content: "Unread messages";
height: 30px;
margin-bottom: $bubble-margin;
margin-left: -50%;
margin-right: -50%;
text-align: center;
color: #538BCC;
line-height: 2.1;
font-weight: 500;
font-size: 15px;
background-color: rgba(255, 255, 255, .95);
z-index: 2;
position: relative;
}
@ -995,7 +999,7 @@ $bubble-margin: .25rem;
&.is-highlighted {
.document-selection {
background-color: rgba(0, 132, 255, .3);
background-color: var(--highlightning-color);
}
body:not(.animation-level-0) & {
@ -1458,6 +1462,11 @@ $bubble-margin: .25rem;
avatar-element {
border: 2px solid #fff;
cursor: pointer;
z-index: 0; // * fix border blinking
&:not(:first-child) {
margin-right: -14px;
}
}
}
@ -1486,7 +1495,7 @@ $bubble-margin: .25rem;
display: none;
}
&.is-in .bubble-content {
&.is-in .bubble-content-wrapper {
margin-left: 0;
}
}
@ -1505,6 +1514,7 @@ $bubble-margin: .25rem;
.bubble-content-wrapper {
transition: transform var(--layer-transition);
transform: scale(1) translateX(0);
opacity: 1;
&.zoom-fade /* .bubble-content */ {
//transform: scale(.8) translateZ(0);
@ -1512,7 +1522,7 @@ $bubble-margin: .25rem;
//transform: scale(.8);
opacity: 0;
transform-origin: center;
animation: zoom-opacity-fade-in .2s linear forwards;
animation: zoom-opacity-fade-in .2s ease-in-out forwards;
animation-delay: 0;
}
@ -1553,6 +1563,7 @@ $bubble-margin: .25rem;
.bubble.service {
align-self: center;
justify-content: center;
.bubble-content {
background-color: transparent;

View File

@ -11,6 +11,7 @@
margin: 0;
display: flex;
flex-direction: column;
position: relative;
width: 100%;
user-select: none;
@ -33,12 +34,21 @@
color: #a3a3a3;
font-size: 1.125rem;
margin-left: .125rem;
animation: fade-in-opacity .2s ease-in-out forwards;
}
}
&.backwards .user-title:after {
animation: fade-in-backwards-opacity .2s ease-in-out forwards;
body:not(.animation-level-0) & {
&.animating {
&:not(.backwards) {
.user-title:after {
animation: fade-in-opacity .2s ease-in-out forwards;
}
}
&.backwards .user-title:after {
animation: fade-in-backwards-opacity .2s ease-in-out forwards;
}
}
}
}
}
@ -70,6 +80,16 @@
html.no-touch &:hover {
background: var(--color-gray-hover);
}
span {
display: inline-block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
//margin: .1rem 0;
line-height: 27px;
}
}
li.menu-open {
@ -112,16 +132,6 @@
}
}
span {
display: inline-block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
//margin: .1rem 0;
line-height: 27px;
}
.dialog-avatar {
flex: 0 0 auto;
}
@ -141,7 +151,7 @@
}
.user-title {
display: flex;
display: flex !important;
align-items: center;
img.emoji {
@ -151,14 +161,11 @@
height: 18px;
}
span.emoji {
overflow: visible;
margin: 0;
width: auto;
font-size: 14px;
//vertical-align: unset;
margin-top: -1.5px;
}
/* span.emoji {
&:first-of-type:not(:first-child) {
margin-left: .125rem;
}
} */
.verified-icon {
flex: 0 0 auto;

View File

@ -104,6 +104,16 @@
//padding-right: 32px;
line-height: 1.3;
}
.preloader-container {
width: 2.375rem;
height: 2.375rem;
@include respond-to(handhelds) {
width: 26px;
height: 26px;
}
}
}
.document, .audio {
@ -134,32 +144,24 @@
cursor: pointer;
display: flex;
justify-content: center;
.tgico-download {
transform: scale(1);
transition: .2s scale;
transition: opacity .2s ease-in-out/* , transform .2s ease-in-out */;
opacity: 1;
//transform: scale(1);
@include respond-to(handhelds) {
margin-top: -1px;
font-size: 20px;
}
}
&.downloading {
.tgico-download {
transform: scale(0);
}
&.downloaded {
opacity: 0;
//transform: scale(0);
}
}
.preloader-container {
width: 42px;
height: 42px;
.preloader-container:not(.preloader-streamable) {
width: 100%;
height: 100%;
transform: scale(1) !important;
}
@include respond-to(handhelds) {
width: 26px;
height: 26px;
}
.preloader-circular {
background-color: transparent !important;
}
}

View File

@ -143,6 +143,21 @@
font-weight: 500;
}
}
&-input-fake {
opacity: 0;
pointer-events: none;
position: absolute !important;
top: 0;
left: 0;
// * override scrollable styles
bottom: auto!important;
right: auto!important;
height: auto!important;
z-index: -3;
}
}
.input-wrapper > * + * {

View File

@ -62,16 +62,14 @@
position: relative;
}
.menu-horizontal {
.menu-horizontal-div {
border-bottom: none;
position: relative !important;
ul {
justify-content: flex-start;
z-index: 0;
}
justify-content: flex-start;
z-index: 0;
li {
&-item {
height: 43px;
padding: 0 1rem;
display: flex;
@ -84,7 +82,7 @@
overflow: visible;
i {
bottom: calc(-.625rem - -1.75px); // * 1.75px will fix for high DPR
bottom: calc(-.6875rem);
padding-right: 1rem !important;
margin-left: -.5rem !important;
}
@ -106,7 +104,7 @@
}
}
.folders-tabs-scrollable li:first-child {
.folders-tabs-scrollable .menu-horizontal-div-item:first-child {
margin-left: .6875rem;
}
@ -173,7 +171,7 @@
.search-super-tabs-scrollable {
position: relative !important;
.menu-horizontal li {
.menu-horizontal-div-item {
flex: 1 0 auto !important;
}
}
@ -405,13 +403,14 @@
height: 3.5rem;
transform: translateY(-100%);
svg {
.preloader-container {
right: auto;
left: 15px;
height: calc(100% - 30px);
.preloader-path {
left: 1rem;
width: 1.5rem;
height: 1.5rem;
.preloader-path-new {
stroke: #2e3939;
}
}

View File

@ -15,9 +15,10 @@ $transition: .2s ease-in-out;
}
&-path {
stroke-dasharray: 1, 200;
//stroke-dasharray: 1, 200;
stroke-dasharray: 93.6375, 124.85; // 75%
stroke-dashoffset: 0;
animation: dash 1.5s ease-in-out infinite/* , color 6s ease-in-out infinite */;
//animation: dash 1.5s ease-in-out infinite/* , color 6s ease-in-out infinite */;
stroke-linecap: round;
stroke: white;
stroke-width: 3;
@ -33,7 +34,7 @@ $transition: .2s ease-in-out;
width: 54px;
height: 54px;
display: flex;
/* cursor: pointer; */
cursor: pointer;
opacity: 0;
transform: scale(0);
@ -60,7 +61,6 @@ $transition: .2s ease-in-out;
.preloader-circular {
animation: none;
cursor: pointer;
background-color: rgba(0, 0, 0, .7);
border-radius: 50%;
width: 100%;
@ -69,8 +69,9 @@ $transition: .2s ease-in-out;
.preloader-path-new {
stroke-dasharray: 5, 149.82;
//stroke-dasharray: 112.36, 149.82;
stroke-dashoffset: 0;
transition: stroke-dasharray $transition;
transition: stroke-dasharray $transition, stroke-width $transition;
stroke-linecap: round;
stroke: white;
stroke-width: 2;
@ -79,17 +80,13 @@ $transition: .2s ease-in-out;
&.preloader-swing {
cursor: default;
.preloader-circular {
cursor: default;
}
.preloader-path-new {
animation: dashNew 1.5s ease-in-out infinite;
//animation: dashNew 1.5s ease-in-out infinite;
stroke-dasharray: 112.36, 149.82;
}
}
.preloader-close {
cursor: pointer;
.preloader-close, .preloader-download {
position: absolute;
top: 0;
left: 0;
@ -97,15 +94,36 @@ $transition: .2s ease-in-out;
right: 0;
margin: auto;
color: #fff;
stroke: #fff;
width: 34%;
height: 34%;
width: 56%;
height: 56%;
transition: opacity .2s ease-in-out/* , transform .2s ease-in-out */;
//transform: scale(1);
opacity: 1;
path {
fill: #fff;
}
html.no-touch &:hover {
background: none;
}
}
&:not(.manual) .preloader-download,
&.manual .preloader-close {
opacity: 0;
//transform: scale(.5);
}
&.manual .preloader-path-new {
stroke-width: 0;
}
.preloader-download {
width: 1.5rem;
height: 1.5rem;
}
&.preloader-streamable {
&, svg {
cursor: pointer !important;
@ -113,7 +131,7 @@ $transition: .2s ease-in-out;
circle {
stroke-width: 2.5 !important;
animation: dashNewStreamable 1.5s ease-in-out infinite !important;
//animation: dashNewStreamable 1.5s ease-in-out infinite !important;
}
&:after {
@ -128,6 +146,18 @@ $transition: .2s ease-in-out;
transform: translate3d(-50%, -50%, 0);
}
}
&.preloader-transparent {
.preloader-circular {
background-color: transparent;
}
}
&.preloader-bold {
.preloader-path-new {
stroke-width: 3.5;
}
}
}
@keyframes rotate {

View File

@ -495,7 +495,6 @@
}
&-content-music, &-content-voice {
.search-super-month-items {
padding: 20px 15px 0px 20px;
@ -504,15 +503,6 @@
}
}
.preloader-container {
.preloader-circular {
background-color: rgba(0, 0, 0, .35);
}
width: 36px !important;
height: 36px !important;
}
.audio {
padding-left: 61px;
/* min-height: 58px; */

View File

@ -3,7 +3,7 @@
user-select: none;
}
.rp-overflow, .btn-menu-toggle.rp, .menu-horizontal li.rp, .btn-corner.rp/* , html.is-safari .c-ripple */ {
.rp-overflow, .btn-menu-toggle.rp, .menu-horizontal-div-item.rp, .btn-corner.rp/* , html.is-safari .c-ripple */ {
.c-ripple {
width: 100%;
height: 100%;

View File

@ -48,7 +48,7 @@ html:not(.is-safari):not(.is-ios) {
overflow-y: hidden;
overflow-x: hidden;
max-height: 100%;
transform: translateZ(0);
//transform: translateZ(0);
//@include respond-to(not-handhelds) {
position: absolute;
@ -72,9 +72,9 @@ html:not(.is-safari):not(.is-ios) {
scrollbar-width: none;
-ms-overflow-style: none;
html.is-safari & {
/* html.is-safari & {
overflow-y: scroll;
}
} */
}
&-padding {

View File

@ -34,6 +34,7 @@
padding: 0 24px 0 24px;
display: flex;
flex-flow: wrap;
position: relative;
&-input {
border: none;
@ -91,6 +92,7 @@
float: left;
margin-right: 8px;
overflow: hidden;
position: relative;
html.is-safari & {
-webkit-mask-image: -webkit-radial-gradient(circle, white 100%, black 100%); // fix safari overflow

View File

@ -26,6 +26,7 @@
position: relative;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
transition: none !important;
html.no-touch & {
background-color: transparent;
@ -35,9 +36,9 @@
}
}
html.no-touch body:not(.animation-level-0) & {
/* html.no-touch body:not(.animation-level-0) & {
transition: background-color .15s ease-in-out;
}
} */
> span {
position: relative;
@ -77,88 +78,6 @@
}
}
.menu-horizontal {
color: $color-gray;
border-bottom: 1px solid $lightgrey;
position: relative;
ul {
width: 100%;
height: 100%;
margin: 0;
display: flex;
justify-content: space-around;
align-items: center;
position: relative;
z-index: 2;
flex-direction: row;
}
li {
display: inline-block;
padding: .75rem 1rem;
cursor: pointer;
text-align: center;
flex: 1 1 auto;
//flex: 0 0 auto;
//overflow: hidden;
user-select: none;
// font-size: 1rem;
font-size: 14px;
font-weight: 500;
position: relative;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
html.no-touch & {
background-color: transparent;
&:hover {
background-color: var(--color-gray-hover);
}
}
html.no-touch body:not(.animation-level-0) & {
transition: background-color .15s ease-in-out;
}
> span {
position: relative;
display: inline-flex;
align-items: center;
overflow: visible;
}
&.active {
color: $color-blue;
i {
opacity: 1;
}
}
}
i {
position: absolute;
bottom: calc(-.625rem - 2.4px);
left: 0;
opacity: 0;
background-color: $color-blue;
height: .1875rem;
width: 100%;
border-radius: .1875rem .1875rem 0 0;
pointer-events: none;
padding-right: .5rem;
margin-left: -.25rem;
box-sizing: content-box;
transform-origin: left;
z-index: 1;
&.animate {
transition: transform var(--tabs-transition);
}
}
}
.tabs-container {
min-width: 100%;
width: 100%;
@ -179,8 +98,6 @@
max-height: 100%;
width: 100%;
max-width: 100%;
//transition: transform .42s, filter .42s;
transition: transform var(--tabs-transition), filter var(--tabs-transition);
display: none;
flex-direction: column;
position: relative;
@ -190,7 +107,7 @@
//z-index: 1;
body.animation-level-0 & {
transition: none;
transition: none !important;
}
//@include respond-to(not-handhelds) {
@ -204,16 +121,68 @@
> div:not(.scroll-padding) {
width: 100%;
max-width: 100%;
/* overflow: hidden; */
//overflow: hidden;
position: relative;
}
}
&[data-slider="tabs"] {
transition: transform var(--tabs-transition);
/* &[data-animation="tabs"] {
& > div {
--width: 100%;
transition: transform var(--tabs-transition);
transform: translateZ(0);
body.animation-level-0 & {
transition: none;
&.from {
animation: slide-tabs-from var(--tabs-transition) forwards;
}
&.to {
transform: translate3d(var(--width), 0, 0);
animation: slide-tabs-to var(--tabs-transition) forwards;
}
}
&.backwards > div {
&.from {
animation: slide-tabs-backwards-from var(--tabs-transition) forwards;
}
&.to {
transform: translate3d(calc(var(--width) * -1), 0, 0);
animation: slide-tabs-backwards-to var(--tabs-transition) forwards;
}
}
} */
&[data-animation="tabs"] > div {
transition: transform var(--tabs-transition);
}
}
&[data-animation="navigation"] > div {
transition: transform var(--tabs-transition), filter var(--tabs-transition);
}
}
/* @keyframes slide-tabs-from {
to {
transform: translate3d(calc(var(--width) * -1), 0, 0);
}
}
@keyframes slide-tabs-to {
to {
transform: translateZ(0);
}
}
@keyframes slide-tabs-backwards-from {
to {
transform: translate3d(var(--width), 0, 0);
}
}
@keyframes slide-tabs-backwards-to {
to {
transform: translateZ(0);
}
} */

View File

@ -1,4 +1,5 @@
.popup {
--transition-time: .15s;
position: fixed!important;
left: 0;
top: 0;
@ -12,12 +13,9 @@
box-shadow: none;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s 0s, visibility 0s 0.3s;
transition: opacity var(--transition-time) ease-in-out, visibility 0s var(--transition-time) ease-in-out;
overflow: auto;
/* text-align: center; */
display: flex;
/* align-items: center;
justify-content: center; */
body.animation-level-0 & {
transition: none;
@ -26,7 +24,7 @@
&.active {
opacity: 1;
visibility: visible;
transition: opacity 0.3s 0s, visibility 0s 0s;
transition: opacity var(--transition-time) ease-in-out, visibility 0s 0s ease-in-out;
z-index: 4;
.popup-container {
@ -34,16 +32,21 @@
}
}
&.hiding {
.popup-container {
transform: translate3d(0, 0, 0);
}
}
&-container {
position: relative;
/* max-width: 400px; */
border-radius: $border-radius-medium;
background-color: #fff;
padding: 1rem;
transform: translate3d(0, -40px, 0);
transform: translate3d(0, 3rem, 0);
backface-visibility: hidden;
transition-property: transform;
transition-duration: 0.3s;
transition: transform var(--transition-time) ease-in-out;
display: flex;
flex-direction: column;
overflow: hidden;

View File

@ -84,7 +84,7 @@ $chat-padding-handhelds: .5rem;
--color-gray-hover: rgba(112, 117, 121, .08);
--pm-transition: .2s ease-in-out;
--layer-transition: .2s ease-in-out;
--tabs-transition: .25s;
--tabs-transition: .25s ease-in-out;
//--layer-transition: .3s cubic-bezier(.33, 1, .68, 1);
//--layer-transition: none;
--btn-corner-transition: .2s cubic-bezier(.34, 1.56, .64, 1);
@ -634,6 +634,7 @@ span.emoji {
//font-size: 1em;
font-family: apple color emoji,segoe ui emoji,noto color emoji,android emoji,emojisymbols,emojione mozilla,twemoji mozilla,segoe ui symbol;
line-height: 1 !important;
}
img.emoji {
@ -955,7 +956,6 @@ img.emoji {
&:not(.active):not(.from):not(.to) {
display: none !important; // Best performance when animating container
//transform: scale(0); // Shortest initial delay
}
}
@ -972,8 +972,6 @@ img.emoji {
> .to {
transform-origin: center;
opacity: 0;
// We can omit `transform: scale(1.1);` here because `opacity` is 0.
// We need to for proper position calculation in `InfiniteScroll`.
}
&.animating {