Browse Source

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
master
Eduard Kuzmenko 3 years ago
parent
commit
43cfa9ee12
  1. 5
      src/components/appSearchSuper..ts
  2. 347
      src/components/chat/bubbles.ts
  3. 33
      src/components/chat/chat.ts
  4. 8
      src/components/chat/input.ts
  5. 53
      src/components/chat/messageRender.ts
  6. 2
      src/components/chat/replies.ts
  7. 7
      src/components/misc.ts
  8. 35
      src/components/preloader.ts
  9. 4
      src/components/sidebarLeft/index.ts
  10. 190
      src/components/sidebarLeft/tabs/background.ts
  11. 6
      src/components/sidebarLeft/tabs/generalSettings.ts
  12. 4
      src/components/slider.ts
  13. 2
      src/components/wrappers.ts
  14. 19
      src/helpers/blur.ts
  15. 6
      src/helpers/dom.ts
  16. 2
      src/helpers/heavyQueue.ts
  17. 4
      src/helpers/number.ts
  18. 4
      src/lib/appManagers/appChatsManager.ts
  19. 20
      src/lib/appManagers/appDownloadManager.ts
  20. 30
      src/lib/appManagers/appImManager.ts
  21. 23
      src/lib/appManagers/appMessagesManager.ts
  22. 36
      src/lib/appManagers/appStateManager.ts
  23. 2
      src/lib/rootScope.ts
  24. 14
      src/scss/partials/_avatar.scss
  25. 4
      src/scss/partials/_button.scss
  26. 30
      src/scss/partials/_chat.scss
  27. 24
      src/scss/partials/_chatBubble.scss
  28. 38
      src/scss/partials/_leftSidebar.scss
  29. 38
      src/scss/style.scss

5
src/components/appSearchSuper..ts

@ -386,8 +386,9 @@ export default class AppSearchSuper { @@ -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);

347
src/components/chat/bubbles.ts

@ -1,4 +1,4 @@ @@ -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"; @@ -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 @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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;
let targetMid: number;
if(backLimit) {
targetMid = maxId;
} else {
if(additionMsgId) {
targetMid = additionMsgId;
} else { // * if maxId === 0
targetMid = Math.max(...sortedMids);
}
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);
}
}
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 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);
const setBubbles: HTMLElement[] = [];
setBubbles.push(contentWrapper);
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;
}
fastRaf(() => {
contentWrapper.classList.remove('zoom-fade');
});
});
const contentWrapper = this.bubbles[mid].lastElementChild as HTMLElement;
if(!mids.length) {
animationPromise.resolve();
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';
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);
return {lastMsDelay, animationPromise};
};
setBubbles.push(contentWrapper);
});
if(!mids.length) {
animationPromise.resolve();
}
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];
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];
let promise: Promise<any>;
if(topIds.length || middleIds.length || bottomIds.length) {
promise = Promise.all(promises);
promise.then(() => {
fastRaf(() => {
setBubbles.forEach(contentWrapper => {
contentWrapper.style.transitionDelay = '';
});
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 = '';
});
});
dispatchHeavyAnimationEvent(promise, Math.max(...delays) + 200); // * 200 - transition time
}
(promise || Promise.resolve()).then(() => {
setTimeout(() => { // preload messages
this.loadMoreHistory(reverse, true);
}, 0);
// ! в хроме, каким-то образом из-за 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 { @@ -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 { @@ -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 { @@ -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 { @@ -2972,7 +2971,7 @@ export default class ChatBubbles {
this.loadMoreHistory(false, true);
}
}, 0);
}
//}
}
//}
});

33
src/components/chat/chat.ts

@ -24,6 +24,9 @@ import ChatInput from "./input"; @@ -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<{ @@ -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;

8
src/components/chat/input.ts

@ -13,13 +13,12 @@ import apiManager from "../../lib/mtproto/mtprotoworker"; @@ -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'; @@ -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 { @@ -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 { @@ -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;

53
src/components/chat/messageRender.ts

@ -1,6 +1,7 @@ @@ -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 { @@ -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 { @@ -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');
};
}

2
src/components/chat/replies.ts

@ -62,7 +62,7 @@ export default class RepliesElement extends HTMLElement { @@ -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;

7
src/components/misc.ts

@ -17,7 +17,10 @@ const set = (elem: HTMLElement | HTMLImageElement | SVGImageElement | HTMLVideoE @@ -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 @@ -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);
}

35
src/components/preloader.ts

@ -139,29 +139,42 @@ export default class ProgressivePreloader { @@ -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);
}, delay);
}
} else {
if(this.tryAgainOnFail) {
this.setManual();
} else {
if(this.tryAgainOnFail) {
this.setManual();
} else {
this.detach();
}
this.detach();
}
this.promise = promise = null;
}
this.promise = promise = null;
};
promise

4
src/components/sidebarLeft/index.ts

@ -446,7 +446,7 @@ export class SettingSection { @@ -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 { @@ -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;

190
src/components/sidebarLeft/tabs/background.ts

@ -0,0 +1,190 @@ @@ -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);
}
}

6
src/components/sidebarLeft/tabs/generalSettings.ts

@ -9,6 +9,8 @@ import appStateManager from "../../../lib/appManagers/appStateManager"; @@ -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 { @@ -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);

4
src/components/slider.ts

@ -69,12 +69,12 @@ export class SliderSuperTab implements SliderTab { @@ -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;

2
src/components/wrappers.ts

@ -438,7 +438,7 @@ export function wrapDocument({message, withTime, fontWeight, voiceAsMusic, showS @@ -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');

19
src/helpers/blur.ts

@ -1,14 +1,18 @@ @@ -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) { @@ -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) { @@ -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 => {

6
src/helpers/dom.ts

@ -756,3 +756,9 @@ export function isSendShortcutPressed(e: KeyboardEvent) { @@ -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 = '';
}

2
src/helpers/heavyQueue.ts

@ -50,7 +50,7 @@ function timedChunk<T>(queue: HeavyQueue<T>) { @@ -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 {

4
src/helpers/number.ts

@ -1,6 +1,6 @@ @@ -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(".");
}

4
src/lib/appManagers/appChatsManager.ts

@ -1,4 +1,4 @@ @@ -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 { @@ -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) {

20
src/lib/appManagers/appDownloadManager.ts

@ -23,7 +23,7 @@ export type Progress = {done: number, fileName: string, total: number, offset: n @@ -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 { @@ -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 = () => {};
/* } catch(err) {
deferred.reject(error);
deferred.cancel = () => {};
} */
};
deferred.finally(() => {

30
src/lib/appManagers/appImManager.ts

@ -21,7 +21,7 @@ import appStickersManager from './appStickersManager'; @@ -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 @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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;

23
src/lib/appManagers/appMessagesManager.ts

@ -264,7 +264,7 @@ export class AppMessagesManager { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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;
}

36
src/lib/appManagers/appStateManager.ts

@ -1,5 +1,5 @@ @@ -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<{ @@ -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 = { @@ -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', @@ -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<{ @@ -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<{ @@ -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();

2
src/lib/rootScope.ts

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

14
src/scss/partials/_avatar.scss

@ -67,13 +67,21 @@ avatar-element { @@ -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 {

4
src/scss/partials/_button.scss

@ -248,7 +248,7 @@ @@ -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 @@ @@ -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 {

30
src/scss/partials/_chat.scss

@ -116,7 +116,7 @@ $chat-helper-size: 39px; @@ -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; @@ -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; @@ -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; @@ -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; @@ -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 & {

24
src/scss/partials/_chatBubble.scss

@ -14,24 +14,6 @@ $bubble-margin: .25rem; @@ -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; @@ -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) {

38
src/scss/partials/_leftSidebar.scss

@ -931,4 +931,40 @@ @@ -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);
}
}
}
}

38
src/scss/style.scss

@ -843,21 +843,29 @@ img.emoji { @@ -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;
}
}
}

Loading…
Cancel
Save