Chat background image

Fix chat jumping scroll after animation in Chrome
Refactor bubble reply rendering
Fix reply in discussions
Changed subscribers formatting
Fix chat resizing again
This commit is contained in:
Eduard Kuzmenko 2021-01-31 03:52:14 +02:00
parent b2126e385f
commit 43cfa9ee12
29 changed files with 713 additions and 285 deletions

View File

@ -386,8 +386,9 @@ export default class AppSearchSuper {
});
}
wrapped.images.thumb && wrapped.images.thumb.classList.add('grid-item-media');
wrapped.images.full && wrapped.images.full.classList.add('grid-item-media');
[wrapped.images.thumb, wrapped.images.full].filter(Boolean).forEach(image => {
image.classList.add('grid-item-media');
});
promises.push(wrapped.loadPromises.thumb);

View File

@ -1,4 +1,4 @@
import { AppImManager, CHAT_ANIMATION_GROUP } from "../../lib/appManagers/appImManager";
import { CHAT_ANIMATION_GROUP } from "../../lib/appManagers/appImManager";
import type { AppMessagesManager, HistoryResult, MyMessage } from "../../lib/appManagers/appMessagesManager";
import type { AppStickersManager } from "../../lib/appManagers/appStickersManager";
import type { AppUsersManager } from "../../lib/appManagers/appUsersManager";
@ -7,7 +7,8 @@ import type { AppPhotosManager } from "../../lib/appManagers/appPhotosManager";
import type { AppDocsManager } from "../../lib/appManagers/appDocsManager";
import type { AppPeersManager } from "../../lib/appManagers/appPeersManager";
import type sessionStorage from '../../lib/sessionStorage';
import { findUpClassName, cancelEvent, findUpTag, whichChild, getElementByPoint, attachClickEvent, positionElementByIndex } from "../../helpers/dom";
import type Chat from "./chat";
import { findUpClassName, cancelEvent, findUpTag, whichChild, getElementByPoint, attachClickEvent, positionElementByIndex, reflowScrollableElement } from "../../helpers/dom";
import { getObjectKeysAndSort } from "../../helpers/object";
import { isTouchSupported } from "../../helpers/touchSupport";
import { logger } from "../../lib/logger";
@ -33,7 +34,6 @@ import { wrapAlbum, wrapPhoto, wrapVideo, wrapDocument, wrapSticker, wrapPoll, w
import { MessageRender } from "./messageRender";
import LazyLoadQueue from "../lazyLoadQueue";
import { AppChatsManager } from "../../lib/appManagers/appChatsManager";
import Chat from "./chat";
import ListenerSetter from "../../helpers/listenerSetter";
import PollElement from "../poll";
import AudioElement from "../audio";
@ -87,9 +87,6 @@ export default class ChatBubbles {
private preloader: ProgressivePreloader = null;
private scrolledAll: boolean;
public scrolledAllDown: boolean;
private loadedTopTimes = 0;
private loadedBottomTimes = 0;
@ -115,6 +112,7 @@ export default class ChatBubbles {
public scrollingToNewBubble: HTMLElement;
public isFirstLoad = true;
private needReflowScroll: boolean;
constructor(private chat: Chat, private appMessagesManager: AppMessagesManager, private appStickersManager: AppStickersManager, private appUsersManager: AppUsersManager, private appInlineBotsManager: AppInlineBotsManager, private appPhotosManager: AppPhotosManager, private appDocsManager: AppDocsManager, private appPeersManager: AppPeersManager, private appChatsManager: AppChatsManager, private storage: typeof sessionStorage) {
//this.chat.log.error('Bubbles construction');
@ -326,7 +324,7 @@ export default class ChatBubbles {
}); */
this.needUpdate.forEachReverse((obj, idx) => {
if(obj.replyMid === mid, obj.replyToPeerId === peerId) {
if(obj.replyMid === mid && obj.replyToPeerId === peerId) {
const {mid, replyMid} = this.needUpdate.splice(idx, 1)[0];
//this.log('messages_downloaded', mid, replyMid, i, this.needUpdate, this.needUpdate.length, mids, this.bubbles[mid]);
@ -340,8 +338,11 @@ export default class ChatBubbles {
delete message.reply_to_mid; // ! WARNING!
}
this.renderMessage(message, true, false, bubble, false);
//this.renderMessage(message, true, true, bubble, false);
MessageRender.setReply({
chat: this.chat,
bubble,
message
});
}
});
});
@ -400,7 +401,7 @@ export default class ChatBubbles {
this.listenerSetter.add(rootScope, 'history_append', (e) => {
let details = e;
if(!this.scrolledAllDown) {
if(!this.scrollable.loadedAll.bottom) {
this.chat.setMessageId();
} else {
this.renderNewMessagesByIds([details.messageId], true);
@ -469,7 +470,7 @@ export default class ChatBubbles {
if(readed.length) {
let maxId = Math.max(...readed);
if(this.scrolledAllDown) {
if(this.scrollable.loadedAll.bottom) {
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;
@ -505,12 +506,13 @@ export default class ChatBubbles {
const onResizeEnd = () => {
const height = this.scrollable.container.offsetHeight;
if(height !== wasHeight) { // * fix opening keyboard while ESG is active, offsetHeight will change right between 'start' and this first frame
const isScrolledDown = this.scrollable.isScrolledDown;
if(height !== wasHeight && (!skip || !isScrolledDown)) { // * fix opening keyboard while ESG is active, offsetHeight will change right between 'start' and this first frame
part += wasHeight - height;
}
/* if(DEBUG) {
this.log('resize end', scrolled, this.scrollable.scrollTop, height, this.scrollable.isScrolledDown);
this.log('resize end', scrolled, part, this.scrollable.scrollTop, height, wasHeight, this.scrollable.isScrolledDown);
} */
if(part) {
@ -972,8 +974,8 @@ export default class ChatBubbles {
/* TEST_SCROLL || */
this.chat.setPeerPromise ||
this.isHeavyAnimationInProgress ||
(top && this.getHistoryTopPromise) ||
(!top && this.getHistoryBottomPromise)
(top && (this.getHistoryTopPromise || this.scrollable.loadedAll.top)) ||
(!top && (this.getHistoryBottomPromise || this.scrollable.loadedAll.bottom))
) {
return;
}
@ -982,28 +984,28 @@ export default class ChatBubbles {
const history = Object.keys(this.bubbles).map(id => +id).sort((a, b) => a - b);
if(!history.length) return;
if(top && !this.scrolledAll) {
/* if(DEBUG) {
this.log('Will load more (up) history by id:', history[0], 'maxId:', history[history.length - 1], history);
} */
if(top) {
if(DEBUG) {
this.log('Will load more (up) history by id:', history[0], 'maxId:', history[history.length - 1], justLoad/* , history */);
}
/* if(history.length == 75) {
this.log('load more', this.scrollable.scrollHeight, this.scrollable.scrollTop, this.scrollable);
return;
} */
/* false && */this.getHistory(history[0], true, undefined, undefined, justLoad);
}
} else {
//let dialog = this.appMessagesManager.getDialogByPeerId(this.peerId)[0];
const historyStorage = this.appMessagesManager.getHistoryStorage(this.peerId, this.chat.threadId);
// if scroll down after search
if(history.indexOf(historyStorage.maxId) !== -1) {
return;
}
if(this.scrolledAllDown) return;
//let dialog = this.appMessagesManager.getDialogByPeerId(this.peerId)[0];
const historyStorage = this.appMessagesManager.getHistoryStorage(this.peerId, this.chat.threadId);
// if scroll down after search
if(!top && history.indexOf(historyStorage.maxId) === -1/* && this.chat.type == 'chat' */) {
/* if(DEBUG) {
this.log('Will load more (down) history by maxId:', history[history.length - 1], history);
} */
if(DEBUG) {
this.log('Will load more (down) history by id:', history[history.length - 1], justLoad/* , history */);
}
/* false && */this.getHistory(history[history.length - 1], false, true, undefined, justLoad);
}
@ -1029,7 +1031,7 @@ export default class ChatBubbles {
}, 1350);
}
if(this.scrollable.getDistanceToEnd() < 300 && this.scrolledAllDown) {
if(this.scrollable.getDistanceToEnd() < 300 && this.scrollable.loadedAll.bottom) {
this.bubblesContainer.classList.add('scrolled-down');
this.scrolledDown = true;
} else if(this.bubblesContainer.classList.contains('scrolled-down')) {
@ -1044,6 +1046,8 @@ export default class ChatBubbles {
public setScroll() {
this.scrollable = new Scrollable(this.bubblesContainer/* .firstElementChild */ as HTMLElement, 'IM', /* 10300 */300);
this.scrollable.loadedAll.top = false;
this.scrollable.loadedAll.bottom = false;
/* const getScrollOffset = () => {
//return Math.round(Math.max(300, appPhotosManager.windowH / 1.5));
@ -1141,7 +1145,7 @@ export default class ChatBubbles {
}
public renderNewMessagesByIds(mids: number[], scrolledDown = this.scrolledDown) {
if(!this.scrolledAllDown) { // seems search active or sliced
if(!this.scrollable.loadedAll.bottom) { // seems search active or sliced
//this.log('renderNewMessagesByIds: seems search is active, skipping render:', mids);
return;
}
@ -1295,8 +1299,8 @@ export default class ChatBubbles {
public cleanup(bubblesToo = false) {
////console.time('appImManager cleanup');
this.scrolledAll = false;
this.scrolledAllDown = false;
this.scrollable.loadedAll.top = false;
this.scrollable.loadedAll.bottom = false;
if(TEST_SCROLL !== undefined) {
TEST_SCROLL = TEST_SCROLL_TIMES;
@ -1520,13 +1524,13 @@ export default class ChatBubbles {
// warning
if(!lastMsgId || this.bubbles[topMessage] || lastMsgId == topMessage) {
this.scrolledAllDown = true;
this.scrollable.loadedAll.bottom = true;
}
this.log('scrolledAllDown:', this.scrolledAllDown);
this.log('scrolledAllDown:', this.scrollable.loadedAll.bottom);
//if(!this.unreaded.length && dialog) { // lol
if(this.scrolledAllDown && topMessage) { // lol
if(this.scrollable.loadedAll.bottom && topMessage) { // lol
this.onScrolledAllDown();
}
@ -2375,28 +2379,12 @@ export default class ChatBubbles {
}
if(message.reply_to_mid && message.reply_to_mid !== this.chat.threadId) {
const replyToPeerId = message.reply_to.reply_to_peer_id ? this.appPeersManager.getPeerId(message.reply_to.reply_to_peer_id) : this.peerId;
let originalMessage = this.appMessagesManager.getMessageByPeer(replyToPeerId, message.reply_to_mid);
let originalPeerTitle: string;
/////////this.log('message to render reply', originalMessage, originalPeerTitle, bubble, message);
// need to download separately
if(originalMessage._ == 'messageEmpty') {
//////////this.log('message to render reply empty, need download', message, message.reply_to_mid);
this.appMessagesManager.wrapSingleMessage(replyToPeerId, message.reply_to_mid);
this.needUpdate.push({replyToPeerId, replyMid: message.reply_to_mid, mid: message.mid});
originalPeerTitle = 'Loading...';
} else {
originalPeerTitle = this.appPeersManager.getPeerTitle(originalMessage.fromId || originalMessage.fwdFromId, true) || '';
}
const wrapped = wrapReply(originalPeerTitle, originalMessage.message || '', originalMessage);
bubbleContainer.append(wrapped);
//bubbleContainer.insertBefore(, nameContainer);
bubble.classList.add('is-reply');
MessageRender.setReply({
chat: this.chat,
bubble,
bubbleContainer,
message
});
}
const needAvatar = this.chat.isAnyGroup() && !isOut;
@ -2474,7 +2462,7 @@ export default class ChatBubbles {
// commented bot getProfile in getHistory!
if(!history/* .filter((id: number) => id > 0) */.length) {
if(!isBackLimit) {
this.scrolledAll = true;
this.scrollable.loadedAll.top = true;
/* if(this.chat.type === 'discussion') {
const serviceStartMessageId = this.appMessagesManager.threadsServiceMessagesIdsStorage[this.peerId + '_' + this.chat.threadId];
@ -2482,7 +2470,7 @@ export default class ChatBubbles {
history.push(this.chat.threadId);
} */
} else {
this.scrolledAllDown = true;
this.scrollable.loadedAll.bottom = true;
}
}
@ -2503,7 +2491,7 @@ export default class ChatBubbles {
const historyStorage = this.appMessagesManager.getHistoryStorage(this.peerId, this.chat.threadId);
if(history.includes(historyStorage.maxId)) {
this.scrolledAllDown = true;
this.scrollable.loadedAll.bottom = true;
}
//console.time('appImManager render history');
@ -2511,7 +2499,9 @@ export default class ChatBubbles {
return new Promise<boolean>((resolve, reject) => {
//await new Promise((resolve) => setTimeout(resolve, 1e3));
//this.log('performHistoryResult: will render some messages:', history.length, this.isHeavyAnimationInProgress);
/* if(DEBUG) {
this.log('performHistoryResult: will render some messages:', history.length, this.isHeavyAnimationInProgress, this.messagesQueuePromise);
} */
const method = (reverse ? history.shift : history.pop).bind(history);
@ -2534,12 +2524,19 @@ export default class ChatBubbles {
previousScrollHeightMinusTop = scrollTop;
} */
//this.log('performHistoryResult: messagesQueueOnRender, scrollTop:', scrollTop, scrollHeight, previousScrollHeightMinusTop);
/* if(DEBUG) {
this.log('performHistoryResult: messagesQueueOnRender, scrollTop:', scrollTop, scrollHeight, previousScrollHeightMinusTop);
} */
this.messagesQueueOnRender = undefined;
};
//}
//}
if(this.needReflowScroll) {
reflowScrollableElement(this.scrollable.container);
this.needReflowScroll = false;
}
while(history.length) {
let message = this.chat.getMessage(method());
this.renderMessage(message, reverse, true);
@ -2577,13 +2574,11 @@ export default class ChatBubbles {
//isTouchSupported && isApple && (this.scrollable.container.style.overflow = '');
if(isSafari/* && !isAppleMobile */) { // * fix blinking and jumping
this.scrollable.container.style.display = 'none';
void this.scrollable.container.offsetLeft; // reflow
this.scrollable.container.style.display = '';
reflowScrollableElement(this.scrollable.container);
}
/* if(DEBUG) {
this.log('performHistoryResult: have set up scrollTop:', newScrollTop, this.scrollable.scrollTop, this.isHeavyAnimationInProgress);
this.log('performHistoryResult: have set up scrollTop:', newScrollTop, this.scrollable.scrollTop, this.scrollable.scrollHeight, this.isHeavyAnimationInProgress);
} */
}
@ -2629,8 +2624,8 @@ export default class ChatBubbles {
return promise;
} else if(this.chat.type === 'scheduled') {
return this.appMessagesManager.getScheduledMessages(this.peerId).then(mids => {
this.scrolledAll = true;
this.scrolledAllDown = true;
this.scrollable.loadedAll.top = true;
this.scrollable.loadedAll.bottom = true;
return {history: mids.slice().reverse()};
});
}
@ -2650,8 +2645,8 @@ export default class ChatBubbles {
//console.time('appImManager call getHistory');
const pageCount = this.appPhotosManager.windowH / 38/* * 1.25 */ | 0;
//const loadCount = Object.keys(this.bubbles).length > 0 ? 50 : pageCount;
//const realLoadCount = Object.keys(this.bubbles).length > 0 || additionMsgId ? Math.max(40, pageCount) : pageCount;//const realLoadCount = 50;
const realLoadCount = pageCount;//const realLoadCount = 50;
const realLoadCount = Object.keys(this.bubbles).length > 0 || additionMsgId ? Math.max(40, pageCount) : pageCount;//const realLoadCount = 50;
//const realLoadCount = pageCount;//const realLoadCount = 50;
let loadCount = realLoadCount;
/* if(TEST_SCROLL) {
@ -2729,7 +2724,7 @@ export default class ChatBubbles {
const serviceStartMessageId = this.appMessagesManager.threadsServiceMessagesIdsStorage[this.peerId + '_' + this.chat.threadId];
if(serviceStartMessageId) historyResult.history.push(serviceStartMessageId);
historyResult.history.push(...this.chat.getMidsByMid(this.chat.threadId).reverse());
this.scrolledAll = true;
this.scrollable.loadedAll.top = true;
}
}
};
@ -2785,123 +2780,127 @@ export default class ChatBubbles {
const waitPromise = isAdditionRender ? processPromise(resultPromise) : promise;
if(isFirstMessageRender && rootScope.settings.animationsEnabled/* && false */) {
let times = isAdditionRender ? 2 : 1;
this.messagesQueueOnRenderAdditional = () => {
if(Object.keys(this.bubbles).length > 1) {
let sortedMids = getObjectKeysAndSort(this.bubbles, 'desc');
this.log('ship went past rocks of magnets');
if(isAdditionRender && additionMsgIds.length) {
sortedMids = sortedMids.filter(mid => !additionMsgIds.includes(mid));
if(--times) return;
this.messagesQueueOnRenderAdditional = undefined;
if(!Object.keys(this.bubbles).length) {
return;
}
let sortedMids = getObjectKeysAndSort(this.bubbles, 'desc');
if(isAdditionRender && additionMsgIds.length) {
sortedMids = sortedMids.filter(mid => !additionMsgIds.includes(mid));
}
let targetMid: number;
if(backLimit) {
targetMid = maxId;
} else {
if(additionMsgId) {
targetMid = additionMsgId;
} else { // * if maxId === 0
targetMid = Math.max(...sortedMids);
}
}
let targetMid: number;
if(backLimit) {
targetMid = maxId;
} else {
if(additionMsgId) {
targetMid = additionMsgId;
} else { // * if maxId === 0
targetMid = Math.max(...sortedMids);
}
}
const topIds = sortedMids.slice(sortedMids.findIndex(mid => targetMid > mid));
const middleIds = isAdditionRender ? [] : [targetMid];
const bottomIds = isAdditionRender ? [] : sortedMids.slice(0, sortedMids.findIndex(mid => targetMid >= mid)).reverse();
const topIds = sortedMids.slice(sortedMids.findIndex(mid => targetMid > mid));
const middleIds = isAdditionRender ? [] : [targetMid];
const bottomIds = isAdditionRender ? [] : sortedMids.slice(0, sortedMids.findIndex(mid => targetMid >= mid)).reverse();
if(DEBUG) {
this.log('getHistory: targeting mid:', targetMid, maxId, additionMsgId,
topIds.map(m => this.appMessagesManager.getServerMessageId(m)),
bottomIds.map(m => this.appMessagesManager.getServerMessageId(m)));
}
const setBubbles: HTMLElement[] = [];
const setBubbles: HTMLElement[] = [];
const delay = isAdditionRender ? 10 : 40;
const offsetIndex = isAdditionRender ? 0 : 1;
const animateAsLadder = (mids: number[], offsetIndex = 0) => {
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;
//lastMsDelay = (idx + offsetIndex) * delay;
//lastMsDelay = (idx || 0.1) * 1000;
//if(idx || isSafari) {
// ! 0.1 = 1ms задержка для Safari, без этого первое сообщение над самым нижним может появиться позже другого с animation-delay, LOL !
//contentWrapper.style.animationDelay = lastMsDelay + 'ms';
//}
contentWrapper.classList.add('zoom-fade');
contentWrapper.style.transitionDelay = lastMsDelay + 'ms';
if(idx === (mids.length - 1)) {
const onTransitionEnd = (e: TransitionEvent) => {
if(e.target !== contentWrapper) {
return;
}
//contentWrapper.style.animationDelay = '';
//contentWrapper.classList.remove('zoom-fade');
//this.log('onTransitionEnd', e);
animationPromise.resolve();
contentWrapper.removeEventListener('transitionend', onTransitionEnd);
};
contentWrapper.addEventListener('transitionend', onTransitionEnd);
}
//this.log('supa', bubble);
setBubbles.push(contentWrapper);
fastRaf(() => {
contentWrapper.classList.remove('zoom-fade');
});
});
if(!mids.length) {
animationPromise.resolve();
const delay = isAdditionRender ? 10 : 40;
const offsetIndex = isAdditionRender ? 0 : 1;
const animateAsLadder = (mids: number[], offsetIndex = 0) => {
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;
}
return {lastMsDelay, animationPromise};
};
const contentWrapper = this.bubbles[mid].lastElementChild as HTMLElement;
const topRes = animateAsLadder(topIds, offsetIndex);
const middleRes = animateAsLadder(middleIds);
const bottomRes = animateAsLadder(bottomIds, offsetIndex);
const promises = [topRes.animationPromise, middleRes.animationPromise, bottomRes.animationPromise];
const delays: number[] = [topRes.lastMsDelay, middleRes.lastMsDelay, bottomRes.lastMsDelay];
lastMsDelay = ((idx + offsetIndex) || 0.1) * delay;
//lastMsDelay = (idx + offsetIndex) * delay;
//lastMsDelay = (idx || 0.1) * 1000;
contentWrapper.classList.add('zoom-fade');
contentWrapper.style.transitionDelay = lastMsDelay + 'ms';
let promise: Promise<any>;
if(topIds.length || middleIds.length || bottomIds.length) {
promise = Promise.all(promises);
promise.then(() => {
fastRaf(() => {
setBubbles.forEach(contentWrapper => {
contentWrapper.style.transitionDelay = '';
});
});
});
dispatchHeavyAnimationEvent(promise, Math.max(...delays) + 200); // * 200 - transition time
if(idx === (mids.length - 1)) {
const onTransitionEnd = (e: TransitionEvent) => {
if(e.target !== contentWrapper) {
return;
}
animationPromise.resolve();
contentWrapper.removeEventListener('transitionend', onTransitionEnd);
};
contentWrapper.addEventListener('transitionend', onTransitionEnd);
}
//this.log('supa', bubble);
setBubbles.push(contentWrapper);
});
if(!mids.length) {
animationPromise.resolve();
}
(promise || Promise.resolve()).then(() => {
setTimeout(() => { // preload messages
this.loadMoreHistory(reverse, true);
}, 0);
return {lastMsDelay, animationPromise};
};
const topRes = animateAsLadder(topIds, offsetIndex);
const middleRes = animateAsLadder(middleIds);
const bottomRes = animateAsLadder(bottomIds, offsetIndex);
const promises = [topRes.animationPromise, middleRes.animationPromise, bottomRes.animationPromise];
const delays: number[] = [topRes.lastMsDelay, middleRes.lastMsDelay, bottomRes.lastMsDelay];
fastRaf(() => {
setBubbles.forEach(contentWrapper => {
contentWrapper.classList.remove('zoom-fade');
});
});
let promise: Promise<any>;
if(topIds.length || middleIds.length || bottomIds.length) {
promise = Promise.all(promises);
promise.then(() => {
fastRaf(() => {
setBubbles.forEach(contentWrapper => {
contentWrapper.style.transitionDelay = '';
});
});
// ! в хроме, каким-то образом из-за zoom-fade класса начинает прыгать скролл при подгрузке сообщений вверх,
// ! т.е. скролл не ставится, так же, как в сафари при translateZ на блок выше scrollable
if(!isSafari) {
this.needReflowScroll = true;
}
});
dispatchHeavyAnimationEvent(promise, Math.max(...delays) + 200); // * 200 - transition time
}
if(!isAdditionRender) {
this.messagesQueueOnRenderAdditional = undefined;
}
(promise || Promise.resolve()).then(() => {
setTimeout(() => { // preload messages
this.loadMoreHistory(reverse, true);
}, 0);
});
};
} else {
this.messagesQueueOnRenderAdditional = undefined;
@ -2938,7 +2937,7 @@ export default class ChatBubbles {
//ids = ids.slice(-removeCount);
//ids = ids.slice(removeCount * 2);
ids = ids.slice(safeCount);
this.scrolledAllDown = false;
this.scrollable.loadedAll.bottom = false;
//this.log('getHistory: slice bottom messages:', ids.length, loadCount);
//this.getHistoryBottomPromise = undefined; // !WARNING, это нужно для обратной загрузки истории, если запрос словил флуд
@ -2946,7 +2945,7 @@ export default class ChatBubbles {
//ids = ids.slice(0, removeCount);
//ids = ids.slice(0, ids.length - (removeCount * 2));
ids = ids.slice(0, ids.length - safeCount);
this.scrolledAll = false;
this.scrollable.loadedAll.top = false;
//this.log('getHistory: slice up messages:', ids.length, loadCount);
//this.getHistoryTopPromise = undefined; // !WARNING, это нужно для обратной загрузки истории, если запрос словил флуд
@ -2962,9 +2961,9 @@ export default class ChatBubbles {
// preload more
//if(!isFirstMessageRender) {
if(this.chat.type === 'chat'/* || this.chat.type === 'discussion' */) {
const storage = this.appMessagesManager.getHistoryStorage(peerId, this.chat.threadId);
/* const storage = this.appMessagesManager.getHistoryStorage(peerId, this.chat.threadId);
const isMaxIdInHistory = storage.history.indexOf(maxId) !== -1;
if(isMaxIdInHistory) { // * otherwise it is a search or jump
if(isMaxIdInHistory || true) { // * otherwise it is a search or jump */
setTimeout(() => {
if(reverse) {
this.loadMoreHistory(true, true);
@ -2972,7 +2971,7 @@ export default class ChatBubbles {
this.loadMoreHistory(false, true);
}
}, 0);
}
//}
}
//}
});

View File

@ -24,6 +24,9 @@ import ChatInput from "./input";
import ChatSelection from "./selection";
import ChatTopbar from "./topbar";
import { REPLIES_PEER_ID } from "../../lib/mtproto/mtproto_config";
import { renderImageFromUrl } from "../misc";
import SetTransition from "../singleTransition";
import { fastRaf } from "../../helpers/schedulers";
export type ChatType = 'chat' | 'pinned' | 'replies' | 'discussion' | 'scheduled';
@ -68,6 +71,36 @@ export default class Chat extends EventListenerBase<{
this.appImManager.chatsContainer.append(this.container);
}
public setBackground(url: string): Promise<void> {
const item = document.createElement('div');
item.classList.add('chat-background-item');
return new Promise<void>((resolve) => {
const cb = () => {
const prev = this.backgroundEl.children[this.backgroundEl.childElementCount - 1] as HTMLElement;
this.backgroundEl.append(item);
// * одного недостаточно, при обновлении страницы все равно фон появляется неплавно
// ! с requestAnimationFrame лучше, но все равно иногда моргает, так что использую два фаста.
fastRaf(() => {
fastRaf(() => {
SetTransition(item, 'is-visible', true, 200, prev ? () => {
prev.remove();
} : null);
});
});
resolve();
};
if(url) {
renderImageFromUrl(item, url, cb);
} else {
cb();
}
});
}
public setType(type: ChatType) {
this.type = type;

View File

@ -13,13 +13,12 @@ import apiManager from "../../lib/mtproto/mtprotoworker";
//import Recorder from '../opus-recorder/dist/recorder.min';
import opusDecodeController from "../../lib/opusDecodeController";
import RichTextProcessor from "../../lib/richtextprocessor";
import { attachClickEvent, blurActiveElement, cancelEvent, cancelSelection, findUpClassName, getRichValue, getSelectedNodes, isInputEmpty, markdownTags, MarkdownType, placeCaretAtEnd, isSendShortcutPressed, fixSafariStickyInput } from "../../helpers/dom";
import { attachClickEvent, blurActiveElement, cancelEvent, cancelSelection, findUpClassName, getRichValue, isInputEmpty, markdownTags, MarkdownType, placeCaretAtEnd, isSendShortcutPressed } from "../../helpers/dom";
import { ButtonMenuItemOptions } from '../buttonMenu';
import emoticonsDropdown from "../emoticonsDropdown";
import PopupCreatePoll from "../popups/createPoll";
import PopupForward from '../popups/forward';
import PopupNewMedia from '../popups/newMedia';
import Scrollable from "../scrollable";
import { toast } from "../toast";
import { wrapReply } from "../wrappers";
import InputField from '../inputField';
@ -36,7 +35,6 @@ import rootScope from '../../lib/rootScope';
import PopupPinMessage from '../popups/unpinMessage';
import { debounce } from '../../helpers/schedulers';
import { tsNow } from '../../helpers/date';
import { isSafari } from '../../helpers/userAgent';
const RECORD_MIN_TIME = 500;
const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this group.';
@ -756,7 +754,7 @@ export default class ChatInput {
if(this.chat.type === 'chat' || this.chat.type === 'discussion') {
this.listenerSetter.add(this.messageInput, 'focusin', () => {
if(this.chat.bubbles.scrolledAllDown) {
if(this.chat.bubbles.scrollable.loadedAll.bottom) {
this.appMessagesManager.readAllHistory(this.chat.peerId, this.chat.threadId);
}
});
@ -1448,7 +1446,7 @@ export default class ChatInput {
this.willSendWebPage = null;
}
this.replyToMsgId = this.chat.threadId;
this.replyToMsgId = undefined;
this.forwardingMids.length = 0;
this.forwardingFromPeerId = 0;
this.editMsgId = undefined;

View File

@ -1,6 +1,7 @@
import { getFullDate } from "../../helpers/date";
import { formatNumber } from "../../helpers/number";
import RichTextProcessor from "../../lib/richtextprocessor";
import { wrapReply } from "../wrappers";
import Chat from "./chat";
import RepliesElement from "./replies";
@ -34,7 +35,7 @@ export namespace MessageRender {
}
}
if(message.edit_date) {
if(message.edit_date && chat.type !== 'scheduled') {
bubble.classList.add('is-edited');
time = '<i class="edited">edited</i> ' + time;
}
@ -74,4 +75,54 @@ export namespace MessageRender {
bubbleContainer.prepend(repliesFooter);
return isFooter;
};
export const setReply = ({chat, bubble, bubbleContainer, message}: {
chat: Chat,
bubble: HTMLElement,
bubbleContainer?: HTMLElement,
message: any
}) => {
const isReplacing = !bubbleContainer;
if(isReplacing) {
bubbleContainer = bubble.querySelector('.bubble-content');
}
const currentReplyDiv = isReplacing ? bubbleContainer.querySelector('.reply') : null;
if(!message.reply_to_mid) {
if(currentReplyDiv) {
currentReplyDiv.remove();
}
bubble.classList.remove('is-reply');
return;
}
const replyToPeerId = message.reply_to.reply_to_peer_id ? chat.appPeersManager.getPeerId(message.reply_to.reply_to_peer_id) : chat.peerId;
let originalMessage = chat.appMessagesManager.getMessageByPeer(replyToPeerId, message.reply_to_mid);
let originalPeerTitle: string;
/////////this.log('message to render reply', originalMessage, originalPeerTitle, bubble, message);
// need to download separately
if(originalMessage._ === 'messageEmpty') {
//////////this.log('message to render reply empty, need download', message, message.reply_to_mid);
chat.appMessagesManager.wrapSingleMessage(replyToPeerId, message.reply_to_mid);
chat.bubbles.needUpdate.push({replyToPeerId, replyMid: message.reply_to_mid, mid: message.mid});
originalPeerTitle = 'Loading...';
} else {
originalPeerTitle = chat.appPeersManager.getPeerTitle(originalMessage.fromId || originalMessage.fwdFromId, true) || '';
}
const wrapped = wrapReply(originalPeerTitle, originalMessage.message || '', originalMessage);
if(currentReplyDiv) {
currentReplyDiv.replaceWith(wrapped);
} else {
bubbleContainer.append(wrapped);
}
//bubbleContainer.insertBefore(, nameContainer);
bubble.classList.add('is-reply');
};
}

View File

@ -62,7 +62,7 @@ export default class RepliesElement extends HTMLElement {
if(!avatarElem) {
avatarElem = new AvatarElement();
avatarElem.setAttribute('dialog', '0');
avatarElem.classList.add('avatar-32');
avatarElem.classList.add('avatar-30');
if(this.loadPromises) {
avatarElem.loadPromises = this.loadPromises;

View File

@ -17,7 +17,10 @@ const set = (elem: HTMLElement | HTMLImageElement | SVGImageElement | HTMLVideoE
// проблема функции в том, что она не подходит для ссылок, пригодна только для blob'ов, потому что обычным ссылкам нужен 'load' каждый раз.
export function renderImageFromUrl(elem: HTMLElement | HTMLImageElement | SVGImageElement | HTMLVideoElement, url: string, callback?: (err?: Event) => void, useCache = false): boolean {
if(((loadedURLs[url]/* && false */) && useCache) || elem instanceof HTMLVideoElement) {
set(elem, url);
if(elem) {
set(elem, url);
}
callback && callback();
return true;
} else {
@ -27,7 +30,7 @@ export function renderImageFromUrl(elem: HTMLElement | HTMLImageElement | SVGIma
loader.src = url;
//let perf = performance.now();
loader.addEventListener('load', () => {
if(!isImage) {
if(!isImage && elem) {
set(elem, url);
}

View File

@ -139,29 +139,42 @@ export default class ProgressivePreloader {
this.promise = promise;
const tempId = --this.tempId;
const startTime = Date.now();
const onEnd = (err: Error) => {
promise.notify = null;
if(tempId === this.tempId) {
if(!err && this.cancelable) {
this.setProgress(100);
if(tempId !== this.tempId) {
return;
}
const elapsedTime = Date.now() - startTime;
//console.log('[PP]: end', this.detached, performance.now());
if(!err && this.cancelable) {
this.setProgress(100);
const delay = TRANSITION_TIME * 0.75;
if(elapsedTime < delay) {
this.detach();
} else {
setTimeout(() => { // * wait for transition complete
if(tempId === this.tempId) {
this.detach();
}
}, TRANSITION_TIME * 0.75);
} else {
if(this.tryAgainOnFail) {
this.setManual();
} else {
this.detach();
}
}, delay);
}
} else {
if(this.tryAgainOnFail) {
this.setManual();
} else {
this.detach();
}
this.promise = promise = null;
}
this.promise = promise = null;
};
promise

View File

@ -446,7 +446,7 @@ export class SettingSection {
public title: HTMLElement;
public caption: HTMLElement;
constructor(name: string, caption?: string) {
constructor(name?: string, caption?: string) {
this.container = document.createElement('div');
this.container.classList.add('sidebar-left-section');
@ -473,7 +473,7 @@ export class SettingSection {
}
}
export const generateSection = (appendTo: Scrollable, name: string, caption?: string) => {
export const generateSection = (appendTo: Scrollable, name?: string, caption?: string) => {
const section = new SettingSection(name, caption);
appendTo.append(section.container);
return section.content;

View File

@ -0,0 +1,190 @@
import { generateSection } from "..";
import blur from "../../../helpers/blur";
import { deferredPromise } from "../../../helpers/cancellablePromise";
import { attachClickEvent, findUpClassName } from "../../../helpers/dom";
import { AccountWallPapers, WallPaper } from "../../../layer";
import appDocsManager, { MyDocument } from "../../../lib/appManagers/appDocsManager";
import appDownloadManager from "../../../lib/appManagers/appDownloadManager";
import appImManager from "../../../lib/appManagers/appImManager";
import appStateManager from "../../../lib/appManagers/appStateManager";
import apiManager from "../../../lib/mtproto/mtprotoworker";
import rootScope from "../../../lib/rootScope";
import Button from "../../button";
import CheckboxField from "../../checkbox";
import ProgressivePreloader from "../../preloader";
import SidebarSlider, { SliderSuperTab } from "../../slider";
import { wrapPhoto } from "../../wrappers";
export default class AppBackgroundTab extends SliderSuperTab {
constructor(slider: SidebarSlider) {
super(slider, true);
}
init() {
this.container.classList.add('background-container');
this.title.innerText = 'Chat Background';
{
const container = generateSection(this.scrollable);
const uploadButton = Button('btn-primary btn-transparent', {icon: 'cameraadd', text: 'Upload Wallpaper'});
const colorButton = Button('btn-primary btn-transparent', {icon: 'colorize', text: 'Set a Color'});
const blurCheckboxField = CheckboxField('Blur Wallpaper Image', 'blur', false, 'settings.background.blur');
blurCheckboxField.input.addEventListener('change', () => {
const active = grid.querySelector('.active') as HTMLElement;
if(!active) return;
// * wait for animation end
setTimeout(() => {
setBackgroundDocument(active.dataset.slug, appDocsManager.getDoc(active.dataset.docId));
}, 100);
});
container.append(uploadButton, colorButton, blurCheckboxField.label);
}
const grid = document.createElement('div');
grid.classList.add('grid');
const saveToCache = (url: string) => {
fetch(url).then(response => {
appDownloadManager.cacheStorage.save('background-image', response);
});
};
const setBackgroundDocument = (slug: string, doc: MyDocument) => {
rootScope.settings.background.slug = slug;
rootScope.settings.background.type = 'image';
appStateManager.pushToState('settings', rootScope.settings);
const download = appDocsManager.downloadDoc(doc, appImManager.chat.bubbles ? appImManager.chat.bubbles.lazyLoadQueue.queueId : 0);
const deferred = deferredPromise<void>();
deferred.addNotifyListener = download.addNotifyListener;
deferred.cancel = download.cancel;
download.then(() => {
if(rootScope.settings.background.slug !== slug || rootScope.settings.background.type !== 'image') {
return;
}
if(rootScope.settings.background.blur) {
setTimeout(() => {
blur(doc.url, 12, 4)
.then(url => {
if(rootScope.settings.background.slug !== slug || rootScope.settings.background.type !== 'image') {
return;
}
saveToCache(url);
return appImManager.setBackground(url);
})
.then(deferred.resolve);
}, 200);
} else {
saveToCache(doc.url);
appImManager.setBackground(doc.url).then(deferred.resolve);
}
});
return deferred;
};
const setActive = () => {
const active = grid.querySelector('.active');
const target = rootScope.settings.background.type === 'image' ? grid.querySelector(`.grid-item[data-slug="${rootScope.settings.background.slug}"]`) : null;
if(active === target) {
return;
}
if(active) {
active.classList.remove('active');
}
if(target) {
target.classList.add('active');
}
};
rootScope.on('background_change', setActive);
apiManager.invokeApiHashable('account.getWallPapers').then((accountWallpapers) => {
const wallpapers = (accountWallpapers as AccountWallPapers.accountWallPapers).wallpapers as WallPaper.wallPaper[];
wallpapers.forEach((wallpaper) => {
if(wallpaper.pFlags.pattern || (wallpaper.document as MyDocument).mime_type.indexOf('application/') === 0) {
return;
}
wallpaper.document = appDocsManager.saveDoc(wallpaper.document);
const container = document.createElement('div');
container.classList.add('grid-item');
const wrapped = wrapPhoto({
photo: wallpaper.document,
message: null,
container: container,
boxWidth: 0,
boxHeight: 0,
withoutPreloader: true
});
[wrapped.images.thumb, wrapped.images.full].filter(Boolean).forEach(image => {
image.classList.add('grid-item-media');
});
container.dataset.docId = wallpaper.document.id;
container.dataset.slug = wallpaper.slug;
if(rootScope.settings.background.type === 'image' && rootScope.settings.background.slug === wallpaper.slug) {
container.classList.add('active');
}
grid.append(container);
});
let clicked: Set<string> = new Set();
attachClickEvent(grid, (e) => {
const target = findUpClassName(e.target, 'grid-item') as HTMLElement;
if(!target) return;
const {docId, slug} = target.dataset;
if(clicked.has(docId)) return;
clicked.add(docId);
const preloader = new ProgressivePreloader({
cancelable: true,
tryAgainOnFail: false
});
const doc = appDocsManager.getDoc(docId);
const load = () => {
const promise = setBackgroundDocument(slug, doc);
if(!doc.url || rootScope.settings.background.blur) {
preloader.attach(target, true, promise);
}
};
preloader.construct();
attachClickEvent(target, (e) => {
if(preloader.preloader.parentElement) {
preloader.onClick(e);
} else {
load();
}
});
load();
console.log(doc);
});
console.log(accountWallpapers);
});
this.scrollable.append(grid);
}
}

View File

@ -9,6 +9,8 @@ import appStateManager from "../../../lib/appManagers/appStateManager";
import rootScope from "../../../lib/rootScope";
import { isApple } from "../../../helpers/userAgent";
import Row from "../../row";
import { attachClickEvent } from "../../../helpers/dom";
import AppBackgroundTab from "./background";
export class RangeSettingSelector {
public container: HTMLDivElement;
@ -72,6 +74,10 @@ export default class AppGeneralSettingsTab extends SliderSuperTab {
const chatBackgroundButton = Button('btn-primary btn-transparent', {icon: 'photo', text: 'Chat Background'});
attachClickEvent(chatBackgroundButton, () => {
new AppBackgroundTab(this.slider).open();
});
const animationsCheckboxField = CheckboxField('Enable Animations', 'animations', false, 'settings.animationsEnabled');
container.append(range.container, chatBackgroundButton, animationsCheckboxField.label);

View File

@ -69,12 +69,12 @@ export class SliderSuperTab implements SliderTab {
}
/* public onCloseAfterTimeout() {
public onCloseAfterTimeout() {
if(this.destroyable) { // ! WARNING, пока что это будет работать только с самой последней внутренней вкладкой !
delete this.slider.tabs[this.id];
this.container.remove();
}
} */
}
}
const TRANSITION_TIME = 250;

View File

@ -438,7 +438,7 @@ export function wrapDocument({message, withTime, fontWeight, voiceAsMusic, showS
const preloader = new ProgressivePreloader();
const load = () => {
const download = appDocsManager.saveDocFile(doc, appImManager.chat.bubbles.lazyLoadQueue.queueId);
const download = appDocsManager.saveDocFile(doc, appImManager.chat.bubbles ? appImManager.chat.bubbles.lazyLoadQueue.queueId : 0);
download.then(() => {
downloadDiv.classList.add('downloaded');

View File

@ -1,14 +1,18 @@
import { DEBUG } from '../lib/mtproto/mtproto_config';
import fastBlur from '../vendor/fastBlur';
import pushHeavyTask from './heavyQueue';
const RADIUS = 2;
const ITERATIONS = 2;
function processBlur(dataUri: string) {
function processBlur(dataUri: string, radius: number, iterations: number) {
return new Promise<string>((resolve) => {
const img = new Image();
console.log('[blur] start');
const perf = performance.now();
if(DEBUG) {
console.log('[blur] start');
}
img.onload = () => {
const canvas = document.createElement('canvas');
@ -18,12 +22,15 @@ function processBlur(dataUri: string) {
const ctx = canvas.getContext('2d')!;
ctx.drawImage(img, 0, 0);
fastBlur(ctx, 0, 0, canvas.width, canvas.height, RADIUS, ITERATIONS);
fastBlur(ctx, 0, 0, canvas.width, canvas.height, radius, iterations);
//resolve(canvas.toDataURL());
canvas.toBlob(blob => {
resolve(URL.createObjectURL(blob));
console.log('[blur] end');
if(DEBUG) {
console.log(`[blur] end, radius: ${radius}, iterations: ${iterations}, time: ${performance.now() - perf}`);
}
});
};
@ -31,11 +38,11 @@ function processBlur(dataUri: string) {
});
}
export default function blur(dataUri: string) {
export default function blur(dataUri: string, radius: number = RADIUS, iterations: number = ITERATIONS) {
return new Promise<string>((resolve) => {
//return resolve(dataUri);
pushHeavyTask({
items: [dataUri],
items: [[dataUri, radius, iterations]],
context: null,
process: processBlur
}).then(results => {

View File

@ -756,3 +756,9 @@ export function isSendShortcutPressed(e: KeyboardEvent) {
return false;
}
export function reflowScrollableElement(element: HTMLElement) {
element.style.display = 'none';
void element.offsetLeft; // reflow
element.style.display = '';
}

View File

@ -50,7 +50,7 @@ function timedChunk<T>(queue: HeavyQueue<T>) {
do {
await getHeavyAnimationPromise();
const possiblePromise = queue.process.call(queue.context, todo.shift());
const possiblePromise = queue.process.apply(queue.context, todo.shift());
let realResult: T;
if(possiblePromise instanceof Promise) {
try {

View File

@ -1,6 +1,6 @@
export function numberWithCommas(x: number) {
export function numberThousandSplitter(x: number, joiner = ',') {
const parts = x.toString().split(".");
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, joiner);
return parts.join(".");
}

View File

@ -1,4 +1,4 @@
import { numberWithCommas } from "../../helpers/number";
import { numberThousandSplitter } from "../../helpers/number";
import { isObject, safeReplaceObject, copy } from "../../helpers/object";
import { ChatAdminRights, ChatBannedRights, ChatFull, ChatParticipants, InputChannel, InputChatPhoto, InputFile, InputPeer, SendMessageAction, Updates } from "../../layer";
import apiManager from '../mtproto/mtprotoworker';
@ -398,7 +398,7 @@ export class AppChatsManager {
}
const isChannel = this.isBroadcast(id);
return numberWithCommas(count || 1) + ' ' + (isChannel ? 'followers' : 'members');
return numberThousandSplitter(count || 1, ' ') + ' ' + (isChannel ? 'subscribers' : 'members');
}
public wrapForFull(id: number, fullChat: any) {

View File

@ -23,7 +23,7 @@ export type Progress = {done: number, fileName: string, total: number, offset: n
export type ProgressCallback = (details: Progress) => void;
export class AppDownloadManager {
private cacheStorage = new CacheStorageController('cachedFiles');
public cacheStorage = new CacheStorageController('cachedFiles');
private downloads: {[fileName: string]: Download} = {};
private progress: {[fileName: string]: Progress} = {};
private progressCallbacks: {[fileName: string]: Array<ProgressCallback>} = {};
@ -51,14 +51,18 @@ export class AppDownloadManager {
const deferred = deferredPromise<Blob>();
deferred.cancel = () => {
const error = new Error('Download canceled');
error.name = 'AbortError';
apiManager.cancelDownload(fileName);
this.clearDownload(fileName);
//try {
const error = new Error('Download canceled');
error.name = 'AbortError';
apiManager.cancelDownload(fileName);
this.clearDownload(fileName);
deferred.reject(error);
deferred.cancel = () => {};
deferred.reject(error);
deferred.cancel = () => {};
/* } catch(err) {
} */
};
deferred.finally(() => {

View File

@ -21,7 +21,7 @@ import appStickersManager from './appStickersManager';
import appWebPagesManager from './appWebPagesManager';
import { cancelEvent, getFilesFromEvent, placeCaretAtEnd } from '../../helpers/dom';
import PopupNewMedia from '../../components/popups/newMedia';
import { numberWithCommas } from '../../helpers/number';
import { numberThousandSplitter } from '../../helpers/number';
import MarkupTooltip from '../../components/chat/markupTooltip';
import { isTouchSupported } from '../../helpers/touchSupport';
import appPollsManager from './appPollsManager';
@ -33,6 +33,9 @@ import useHeavyAnimationCheck, { dispatchHeavyAnimationEvent } from '../../hooks
import appDraftsManager from './appDraftsManager';
import serverTimeManager from '../mtproto/serverTimeManager';
import sessionStorage from '../sessionStorage';
import { renderImageFromUrl } from '../../components/misc';
import appDownloadManager from './appDownloadManager';
import appStateManager, { AppStateManager } from './appStateManager';
//console.log('appImManager included33!');
@ -154,6 +157,16 @@ export class AppImManager {
animationIntersector.checkAnimations(false);
});
const isDefaultBackground = rootScope.settings.background.blur === AppStateManager.STATE_INIT.settings.background.blur &&
rootScope.settings.background.slug === AppStateManager.STATE_INIT.settings.background.slug;
if(!isDefaultBackground) {
appDownloadManager.cacheStorage.getFile('background-image').then(blob => {
this.setBackground(URL.createObjectURL(blob), false);
});
} else {
this.setBackground('');
}
/* rootScope.on('peer_changing', (chat) => {
this.saveChatPosition(chat);
});
@ -163,6 +176,15 @@ export class AppImManager {
}); */
}
public setBackground(url: string, broadcastEvent = true): Promise<void> {
const promises = this.chats.map(chat => chat.setBackground(url));
return promises[promises.length - 1].then(() => {
if(broadcastEvent) {
rootScope.broadcast('background_change');
}
});
}
/* public saveChatPosition(chat: Chat) {
const bubble = chat.bubbles.getBubbleByPoint('top');
if(bubble) {
@ -503,6 +525,10 @@ export class AppImManager {
private createNewChat() {
const chat = new Chat(this, appChatsManager, appDocsManager, appInlineBotsManager, appMessagesManager, appPeersManager, appPhotosManager, appProfileManager, appStickersManager, appUsersManager, appWebPagesManager, appPollsManager, apiManager, appDraftsManager, serverTimeManager, sessionStorage);
if(this.chats.length) {
chat.backgroundEl.append(this.chat.backgroundEl.lastElementChild.cloneNode(true));
}
this.chats.push(chat);
}
@ -661,7 +687,7 @@ export class AppImManager {
if(participants_count < 2) return subtitle;
const onlines = await appChatsManager.getOnlines(chat.id);
if(onlines > 1) {
subtitle += ', ' + numberWithCommas(onlines) + ' online';
subtitle += ', ' + numberThousandSplitter(onlines, ' ') + ' online';
}
return subtitle;

View File

@ -264,7 +264,7 @@ export class AppMessagesManager {
const folder = this.dialogsStorage.getFolder(+folderId);
for(let dialog of folder) {
items.push(dialog);
items.push([dialog]);
}
}
@ -421,6 +421,10 @@ export class AppMessagesManager {
//this.checkSendOptions(options);
if(options.threadId && !options.replyToMsgId) {
options.replyToMsgId = options.threadId;
}
const MAX_LENGTH = 4096;
if(text.length > MAX_LENGTH) {
const splitted = splitStringByLength(text, MAX_LENGTH);
@ -603,6 +607,11 @@ export class AppMessagesManager {
waveform: Uint8Array
}> = {}) {
peerId = appPeersManager.getPeerMigratedTo(peerId) || peerId;
if(options.threadId && !options.replyToMsgId) {
options.replyToMsgId = options.threadId;
}
//this.checkSendOptions(options);
const messageId = this.generateTempMessageId(peerId);
const randomIdS = randomLong();
@ -999,6 +1008,10 @@ export class AppMessagesManager {
}> = {}) {
//this.checkSendOptions(options);
if(options.threadId && !options.replyToMsgId) {
options.replyToMsgId = options.threadId;
}
if(files.length === 1) {
return this.sendFile(peerId, files[0], {...options, ...options.sendFileDetails[0]});
}
@ -1152,6 +1165,10 @@ export class AppMessagesManager {
}> = {}) {
peerId = appPeersManager.getPeerMigratedTo(peerId) || peerId;
if(options.threadId && !options.replyToMsgId) {
options.replyToMsgId = options.threadId;
}
//this.checkSendOptions(options);
const messageId = this.generateTempMessageId(peerId);
const randomIdS = randomLong();
@ -1392,10 +1409,10 @@ export class AppMessagesManager {
private generateReplyHeader(replyToMsgId: number, replyToTopId?: number) {
const header = {
_: 'messageReplyHeader',
reply_to_msg_id: replyToMsgId,
reply_to_msg_id: replyToMsgId || replyToTopId,
} as MessageReplyHeader;
if(replyToTopId && replyToTopId !== replyToMsgId) {
if(replyToTopId && header.reply_to_msg_id !== replyToTopId) {
header.reply_to_top_id = replyToTopId;
}

View File

@ -1,5 +1,5 @@
import type { Dialog } from './appMessagesManager';
import { App, MOUNT_CLASS_TO, UserAuth } from '../mtproto/mtproto_config';
import { App, DEBUG, MOUNT_CLASS_TO, UserAuth } from '../mtproto/mtproto_config';
import EventListenerBase from '../../helpers/eventListenerBase';
import rootScope from '../rootScope';
import sessionStorage from '../sessionStorage';
@ -54,12 +54,18 @@ export type State = Partial<{
stickers: {
suggest: boolean,
loop: boolean
},
background: {
type: 'color' | 'image' | 'default',
blur: boolean,
color?: string,
slug?: string,
}
},
drafts: AppDraftsManager['drafts']
}>;
const STATE_INIT: State = {
export const STATE_INIT: State = {
dialogs: [],
allDialogsLoaded: {},
chats: {},
@ -95,6 +101,11 @@ const STATE_INIT: State = {
stickers: {
suggest: true,
loop: true
},
background: {
type: 'image',
blur: false,
slug: 'ByxGo2lrMFAIAAAAmkJxZabh8eM', // * new blurred camomile
}
},
drafts: {}
@ -108,6 +119,7 @@ const REFRESH_KEYS = ['dialogs', 'allDialogsLoaded', 'messages', 'contactsList',
export class AppStateManager extends EventListenerBase<{
save: (state: State) => Promise<void>
}> {
public static STATE_INIT = STATE_INIT;
public loaded: Promise<State>;
private log = logger('STATE'/* , LogLevels.error */);
@ -144,11 +156,25 @@ export class AppStateManager extends EventListenerBase<{
if(state.version !== STATE_VERSION) {
state = copy(STATE_INIT);
} else if((state.stateCreatedTime + REFRESH_EVERY) < time/* && false */) {
this.log('will refresh state', state.stateCreatedTime, time);
if(DEBUG) {
this.log('will refresh state', state.stateCreatedTime, time);
}
REFRESH_KEYS.forEach(key => {
// @ts-ignore
state[key] = copy(STATE_INIT[key]);
});
const users: typeof state['users'] = {}, chats: typeof state['chats'] = {};
if(state.recentSearch?.length) {
state.recentSearch.forEach(peerId => {
if(peerId < 0) chats[peerId] = state.chats[peerId];
else users[peerId] = state.users[peerId];
});
}
state.users = users;
state.chats = chats;
}
}
@ -160,7 +186,9 @@ export class AppStateManager extends EventListenerBase<{
// ! probably there is better place for it
rootScope.settings = this.state.settings;
this.log('state res', state);
if(DEBUG) {
this.log('state res', state);
}
//return resolve();

View File

@ -88,6 +88,8 @@ type BroadcastEvents = {
'im_tab_change': number,
'overlay_toggle': boolean,
'background_change': void,
};
class RootScope extends EventListenerBase<any> {

View File

@ -67,13 +67,21 @@ avatar-element {
}
img {
width: 100%;
height: 100%;
border-radius: inherit;
//width: 100% !important;
//height: 100% !important;
width: var(--size) !important;
height: var(--size) !important;
border-radius: inherit !important;
&.fade-in {
animation: fade-in-opacity .2s ease forwards;
}
&.emoji {
width: calc(1.125rem / var(--multiplier));
height: calc(1.125rem / var(--multiplier));
vertical-align: middle !important;
}
}
path {

View File

@ -248,7 +248,7 @@
}
}
// ! example: multiselect input, button in pinned messages chat
// ! example: multiselect input, button in pinned messages chat, settings, chat background tab
.btn-transparent {
color: #000;
background-color: transparent;
@ -256,7 +256,7 @@
align-items: center;
padding: 0 .875rem;
//width: auto;
text-transform: capitalize;
//text-transform: capitalize;
font-weight: normal;
html.no-touch &:hover {

View File

@ -116,7 +116,7 @@ $chat-helper-size: 39px;
background: none;
border: none;
width: 100%;
padding: 0 .5625rem;
padding: .5rem .5625rem;
/* height: 100%; */
margin-top: -1px;
max-height: calc(30rem - 2.5rem); // 2.5rem - input helper (reply)
@ -563,12 +563,13 @@ $chat-helper-size: 39px;
&-background {
overflow: hidden;
background-color: #e6ebee;
&.no-transition:before {
transition: none !important;
}
&, &:before {
&, &-item {
position: absolute !important;
top: 0;
left: 0;
@ -576,24 +577,32 @@ $chat-helper-size: 39px;
right: 0;
}
&:before {
content: "";
display: block;
&-item {
background-image: url('assets/img/bg.jpeg');
background-size: cover;
background-position: center center;
background-color: inherit;
body.animation-level-2 & {
transition: opacity var(--layer-transition);
opacity: 0;
&.is-visible:not(.backwards) {
opacity: 1;
}
}
@include respond-to(medium-screens) {
body.animation-level-2 & {
// !WARNING, МАГИЧЕСКОЕ ЧИСЛО
margin: -16rem -5rem -20rem 0;
margin: -16.5rem 0 -20rem 0;
transform: scale(1);
transform-origin: left center;
transition: transform var(--layer-transition);
transition: transform var(--layer-transition), opacity var(--layer-transition);
}
body.animation-level-2.is-right-column-shown & {
transform: scale(.67);
transform: scale(.666666667);
}
}
}
@ -1036,6 +1045,8 @@ $chat-helper-size: 39px;
cursor: pointer;
//--translateY: 0;
opacity: 1;
transition: opacity var(--layer-transition), visibility 0s 0s !important;
visibility: visible;
/* &.is-broadcast {
--translateY: 79px !important;
@ -1151,12 +1162,13 @@ $chat-helper-size: 39px;
bottom: calc(var(--chat-input-size) + var(--bottom) + 10px);
cursor: default;
opacity: 0;
visibility: hidden;
z-index: 2;
//transition: transform var(--layer-transition), opacity var(--layer-transition) !important;
overflow: visible;
//--translateY: calc(var(--chat-input-size) + 10px);
//--translateY: calc(100% + 10px);
transition: opacity var(--layer-transition) !important;
transition: opacity var(--layer-transition), visibility 0s .2s !important;
transform: none !important;
body.animation-level-0 & {

View File

@ -14,24 +14,6 @@ $bubble-margin: .25rem;
}
}
/*
* zoom-fade-opacity
*/
@keyframes zoom-opacity-fade-in {
0% {
//transform: scale(.8) translateZ(0);
transform: scale3d(.8, .8, 1);
//transform: scale(.8);
opacity: 0;
}
100% {
//transform: scale(1) translateZ(0);
transform: scale3d(1, 1, 1);
//transform: scale(1);
opacity: 1;
}
}
.bubbles-date-group {
position: relative;
@ -1522,11 +1504,9 @@ $bubble-margin: .25rem;
&.zoom-fade /* .bubble-content */ {
//transform: scale(.8) translateZ(0);
transform: scale3d(.8, .8, 1);
//transform: scale(.8);
transform: scale3d(.8, .8, 1) translateX(0);
//transform: scale(.8) translateX(0);
opacity: 0;
//animation: zoom-opacity-fade-in .2s ease-in-out forwards;
//animation-delay: 0s;
}
@include respond-to(not-handhelds) {

View File

@ -931,4 +931,40 @@
--thumb-size: 12px;
}
}
}
}
.background-container {
.grid {
padding: 0 .5rem;
&-item {
&:after {
content: " ";
display: block;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
border: 3px solid $color-blue;
opacity: 0;
transition: opacity .2s ease-in-out;
}
&.active {
&:after {
opacity: 1;
}
.grid-item-media {
transform: scale(.91);
}
}
&-media {
transition: transform .2s ease-in-out;
transform: scale(1);
}
}
}
}

View File

@ -843,21 +843,29 @@ img.emoji {
}
}
.grid-item {
height: 0;
padding-bottom: 100%;
//overflow: hidden;
position: relative;
cursor: pointer;
user-select: none;
&-media {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
object-fit: cover;
.grid {
width: 100%;
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-auto-rows: 1fr;
grid-gap: .25rem;
&-item {
height: 0;
padding-bottom: 100%;
//overflow: hidden;
position: relative;
cursor: pointer;
user-select: none;
&-media {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
}
}