tweb/src/components/chat/bubbles.ts

6647 lines
222 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import type {AppImManager, ChatSavedPosition, ChatSetPeerOptions} from '../../lib/appManagers/appImManager';
import type {HistoryResult, MyMessage} from '../../lib/appManagers/appMessagesManager';
import type {MyDocument} from '../../lib/appManagers/appDocsManager';
import type Chat from './chat';
import IS_TOUCH_SUPPORTED from '../../environment/touchSupport';
import {logger} from '../../lib/logger';
import rootScope from '../../lib/rootScope';
import BubbleGroups from './bubbleGroups';
import PopupDatePicker from '../popups/datePicker';
import PopupForward from '../popups/forward';
import PopupStickers from '../popups/stickers';
import ProgressivePreloader from '../preloader';
import Scrollable, {SliceSides} from '../scrollable';
import StickyIntersector from '../stickyIntersector';
import animationIntersector from '../animationIntersector';
import mediaSizes from '../../helpers/mediaSizes';
import {IS_ANDROID, IS_APPLE, IS_MOBILE, IS_SAFARI} from '../../environment/userAgent';
import I18n, {FormatterArguments, i18n, langPack, LangPackKey, UNSUPPORTED_LANG_PACK_KEY, _i18n} from '../../lib/langPack';
import AvatarElement from '../avatar';
import ripple from '../ripple';
import {MessageRender} from './messageRender';
import LazyLoadQueue from '../lazyLoadQueue';
import ListenerSetter from '../../helpers/listenerSetter';
import PollElement from '../poll';
import AudioElement from '../audio';
import {ChannelParticipant, Chat as MTChat, ChatInvite, ChatParticipant, Document, Message, MessageEntity, MessageMedia, MessageReplyHeader, Photo, PhotoSize, ReactionCount, ReplyMarkup, SponsoredMessage, Update, UrlAuthResult, User, WebPage} from '../../layer';
import {BOT_START_PARAM, NULL_PEER_ID, REPLIES_PEER_ID} from '../../lib/mtproto/mtproto_config';
import {FocusDirection, ScrollStartCallbackDimensions} from '../../helpers/fastSmoothScroll';
import useHeavyAnimationCheck, {getHeavyAnimationPromise, dispatchHeavyAnimationEvent, interruptHeavyAnimation} from '../../hooks/useHeavyAnimationCheck';
import {fastRaf, fastRafPromise} from '../../helpers/schedulers';
import deferredPromise from '../../helpers/cancellablePromise';
import RepliesElement from './replies';
import DEBUG from '../../config/debug';
import {SliceEnd} from '../../helpers/slicedArray';
import PeerTitle from '../peerTitle';
import findUpClassName from '../../helpers/dom/findUpClassName';
import findUpTag from '../../helpers/dom/findUpTag';
import {toast, toastNew} from '../toast';
import {getMiddleware, Middleware} from '../../helpers/middleware';
import cancelEvent from '../../helpers/dom/cancelEvent';
import {attachClickEvent, simulateClickEvent} from '../../helpers/dom/clickEvent';
import htmlToDocumentFragment from '../../helpers/dom/htmlToDocumentFragment';
import reflowScrollableElement from '../../helpers/dom/reflowScrollableElement';
import replaceContent from '../../helpers/dom/replaceContent';
import setInnerHTML from '../../helpers/dom/setInnerHTML';
import whichChild from '../../helpers/dom/whichChild';
import {cancelAnimationByKey} from '../../helpers/animation';
import assumeType from '../../helpers/assumeType';
import debounce, {DebounceReturnType} from '../../helpers/schedulers/debounce';
import {SEND_WHEN_ONLINE_TIMESTAMP} from '../../lib/mtproto/constants';
import windowSize from '../../helpers/windowSize';
import {formatPhoneNumber} from '../../helpers/formatPhoneNumber';
import AppMediaViewer from '../appMediaViewer';
import SetTransition from '../singleTransition';
import handleHorizontalSwipe from '../../helpers/dom/handleHorizontalSwipe';
import findUpAttribute from '../../helpers/dom/findUpAttribute';
import findUpAsChild from '../../helpers/dom/findUpAsChild';
import formatCallDuration from '../../helpers/formatCallDuration';
import IS_CALL_SUPPORTED from '../../environment/callSupport';
import Button from '../button';
import {CallType} from '../../lib/calls/types';
import getVisibleRect from '../../helpers/dom/getVisibleRect';
import PopupJoinChatInvite from '../popups/joinChatInvite';
import {InternalLink, INTERNAL_LINK_TYPE} from '../../lib/appManagers/internalLink';
import ReactionsElement, {REACTIONS_ELEMENTS} from './reactions';
import type ReactionElement from './reaction';
import RLottiePlayer from '../../lib/rlottie/rlottiePlayer';
import pause from '../../helpers/schedulers/pause';
import ScrollSaver from '../../helpers/scrollSaver';
import getObjectKeysAndSort from '../../helpers/object/getObjectKeysAndSort';
import forEachReverse from '../../helpers/array/forEachReverse';
import formatNumber from '../../helpers/number/formatNumber';
import getViewportSlice from '../../helpers/dom/getViewportSlice';
import SuperIntersectionObserver from '../../helpers/dom/superIntersectionObserver';
import generateFakeIcon from '../generateFakeIcon';
import copyFromElement from '../../helpers/dom/copyFromElement';
import PopupElement from '../popups';
import setAttachmentSize from '../../helpers/setAttachmentSize';
import wrapWebPageDescription from '../wrappers/webPageDescription';
import wrapWebPageTitle from '../wrappers/webPageTitle';
import wrapEmojiText from '../../lib/richTextProcessor/wrapEmojiText';
import wrapRichText from '../../lib/richTextProcessor/wrapRichText';
import wrapMessageActionTextNew from '../wrappers/messageActionTextNew';
import isMentionUnread from '../../lib/appManagers/utils/messages/isMentionUnread';
import getMediaFromMessage from '../../lib/appManagers/utils/messages/getMediaFromMessage';
import getPeerColorById from '../../lib/appManagers/utils/peers/getPeerColorById';
import getPeerId from '../../lib/appManagers/utils/peers/getPeerId';
import getServerMessageId from '../../lib/appManagers/utils/messageId/getServerMessageId';
import generateMessageId from '../../lib/appManagers/utils/messageId/generateMessageId';
import {AppManagers} from '../../lib/appManagers/managers';
import {Awaited, SendMessageEmojiInteractionData} from '../../types';
import idleController from '../../helpers/idleController';
import overlayCounter from '../../helpers/overlayCounter';
import {cancelContextMenuOpening} from '../../helpers/dom/attachContextMenuListener';
import contextMenuController from '../../helpers/contextMenuController';
import {AckedResult} from '../../lib/mtproto/superMessagePort';
import middlewarePromise from '../../helpers/middlewarePromise';
import {EmoticonsDropdown} from '../emoticonsDropdown';
import indexOfAndSplice from '../../helpers/array/indexOfAndSplice';
import noop from '../../helpers/noop';
import getAlbumText from '../../lib/appManagers/utils/messages/getAlbumText';
import paymentsWrapCurrencyAmount from '../../helpers/paymentsWrapCurrencyAmount';
import PopupPayment from '../popups/payment';
import isInDOM from '../../helpers/dom/isInDOM';
import getStickerEffectThumb from '../../lib/appManagers/utils/stickers/getStickerEffectThumb';
import attachStickerViewerListeners from '../stickerViewer';
import {makeMediaSize, MediaSize} from '../../helpers/mediaSize';
import wrapSticker, {onEmojiStickerClick} from '../wrappers/sticker';
import wrapAlbum from '../wrappers/album';
import wrapDocument from '../wrappers/document';
import wrapGroupedDocuments from '../wrappers/groupedDocuments';
import wrapPhoto from '../wrappers/photo';
import wrapPoll from '../wrappers/poll';
import wrapVideo from '../wrappers/video';
import isRTL from '../../helpers/string/isRTL';
import NBSP from '../../helpers/string/nbsp';
import DotRenderer from '../dotRenderer';
import toHHMMSS from '../../helpers/string/toHHMMSS';
import {BatchProcessor} from '../../helpers/sortedList';
import wrapUrl from '../../lib/richTextProcessor/wrapUrl';
import getMessageThreadId from '../../lib/appManagers/utils/messages/getMessageThreadId';
import wrapTopicNameButton from '../wrappers/topicNameButton';
import wrapMediaSpoiler, {onMediaSpoilerClick, toggleMediaSpoiler} from '../wrappers/mediaSpoiler';
import toggleDisability from '../../helpers/dom/toggleDisability';
import {copyTextToClipboard} from '../../helpers/clipboard';
import liteMode from '../../helpers/liteMode';
import getMediaDurationFromMessage from '../../lib/appManagers/utils/messages/getMediaDurationFromMessage';
import wrapLocalSticker from '../wrappers/localSticker';
import {LottieAssetName} from '../../lib/rlottie/lottieLoader';
import clamp from '../../helpers/number/clamp';
import getParticipantRank from '../../lib/appManagers/utils/chats/getParticipantRank';
import wrapParticipantRank from '../wrappers/participantRank';
export const USER_REACTIONS_INLINE = false;
const USE_MEDIA_TAILS = false;
type MESSAGE_ACTION_TYPE = Message.messageService['action']['_'];
type IGNORE_ACTION_KEY = MESSAGE_ACTION_TYPE;
type IGNORE_ACTION_VALUE = true | ((message: Message.messageService) => boolean);
const IGNORE_ACTIONS: Map<IGNORE_ACTION_KEY, IGNORE_ACTION_VALUE> = new Map([
['messageActionHistoryClear', true],
['messageActionChatCreate', (message) => message.pFlags.out]
// 'messageActionChannelMigrateFrom'
] as [IGNORE_ACTION_KEY, IGNORE_ACTION_VALUE][]);
export const SERVICE_AS_REGULAR: Set<MESSAGE_ACTION_TYPE> = new Set();
if(IS_CALL_SUPPORTED) {
SERVICE_AS_REGULAR.add('messageActionPhoneCall');
}
const TEST_SCROLL_TIMES: number = undefined;
let TEST_SCROLL = TEST_SCROLL_TIMES;
let queueId = 0;
type GenerateLocalMessageType<IsService> = IsService extends true ? Message.messageService : Message.message;
const SPONSORED_MESSAGE_ID_OFFSET = 1;
export const STICKY_OFFSET = 3;
const SCROLLED_DOWN_THRESHOLD = 300;
const PEER_CHANGED_ERROR = new Error('peer changed');
const DO_NOT_SLICE_VIEWPORT = false;
const DO_NOT_SLICE_VIEWPORT_ON_RENDER = false;
const DO_NOT_UPDATE_MESSAGE_VIEWS = false;
const DO_NOT_UPDATE_MESSAGE_REACTIONS = false;
const DO_NOT_UPDATE_MESSAGE_REPLY = false;
const BIG_EMOJI_SIZES: {[size: number]: number} = {
1: 96,
2: 90,
3: 84,
4: 72,
5: 60,
6: 48,
7: 36
};
const BIG_EMOJI_SIZES_LENGTH = Object.keys(BIG_EMOJI_SIZES).length;
type Bubble = {
bubble: HTMLElement,
mids: Set<number>,
groupedId?: string
};
type MyHistoryResult = HistoryResult | {history: number[]};
function getMainMidForGrouped(mids: number[]) {
return Math.max(...mids);
}
export default class ChatBubbles {
public container: HTMLDivElement;
public chatInner: HTMLDivElement;
public scrollable: Scrollable;
private getHistoryTopPromise: Promise<boolean>;
private getHistoryBottomPromise: Promise<boolean>;
// public messagesCount: number = -1;
private unreadOut = new Set<number>();
public needUpdate: {replyToPeerId: PeerId, replyMid: number, mid: number}[] = []; // if need wrapSingleMessage
public bubbles: {[mid: string]: HTMLElement} = {};
public skippedMids: Set<number> = new Set();
public bubblesNewByGroupedId: {[groupId: string]: Bubble} = {};
public bubblesNew: {[mid: string]: Bubble} = {};
private dateMessages: {[timestamp: number]: {
div: HTMLElement,
firstTimestamp: number,
container: HTMLElement,
timeout?: number
}} = {};
private scrolledDown = true;
private isScrollingTimeout = 0;
private stickyIntersector: StickyIntersector;
private unreaded: Map<HTMLElement, number> = new Map();
private unreadedSeen: Set<number> = new Set();
private readPromise: Promise<void>;
private bubbleGroups: BubbleGroups;
private preloader: ProgressivePreloader = null;
// private messagesQueueOnRender: () => void = null;
private messagesQueueOnRenderAdditional: () => void = null;
private firstUnreadBubble: HTMLElement = null;
private attachedUnreadBubble: boolean;
public lazyLoadQueue: LazyLoadQueue;
private middlewareHelper = getMiddleware();
private log: ReturnType<typeof logger>;
public listenerSetter: ListenerSetter;
private followStack: number[] = [];
private isHeavyAnimationInProgress = false;
private scrollingToBubble: HTMLElement;
private isFirstLoad = true;
private needReflowScroll: boolean;
private fetchNewPromise: Promise<void>;
private passEntities: Partial<{
[_ in MessageEntity['_']]: boolean
}> = {};
private onAnimateLadder: () => Promise<any> | void;
// private ladderDeferred: CancellablePromise<void>;
private resolveLadderAnimation: () => Promise<any>;
private emptyPlaceholderBubble: HTMLElement;
private viewsMids: Set<number> = new Set();
private sendViewCountersDebounced: () => Promise<void>;
private isTopPaddingSet = false;
private getSponsoredMessagePromise: Promise<void>;
private previousStickyDate: HTMLElement;
private sponsoredMessage: SponsoredMessage.sponsoredMessage;
private hoverBubble: HTMLElement;
private hoverReaction: HTMLElement;
private sliceViewportDebounced: DebounceReturnType<ChatBubbles['sliceViewport']>;
private resizeObserver: ResizeObserver;
private willScrollOnLoad: boolean;
public observer: SuperIntersectionObserver;
private renderingMessages: Set<number> = new Set();
private setPeerCached: boolean;
private attachPlaceholderOnRender: () => void;
private bubblesToEject: Set<HTMLElement> = new Set();
private bubblesToReplace: Map<HTMLElement, HTMLElement> = new Map(); // TO -> FROM
private updatePlaceholderPosition: () => void;
private setPeerOptions: {lastMsgId: number; topMessage: number; savedPosition: ChatSavedPosition};
private setPeerTempId: number = 0;
private renderNewPromises: Set<Promise<any>> = new Set();
private updateGradient: boolean;
private extendedMediaMessages: Set<number> = new Set();
private pollExtendedMediaMessagesPromise: Promise<void>;
private batchProcessor: BatchProcessor<Awaited<ReturnType<ChatBubbles['safeRenderMessage']>>>;
private ranks: Map<PeerId, ReturnType<typeof getParticipantRank>>;
private processRanks: Set<() => void>;
private canShowRanks: boolean;
// private reactions: Map<number, ReactionsElement>;
constructor(
private chat: Chat,
private managers: AppManagers
) {
this.log = this.chat.log;
// this.chat.log.error('Bubbles construction');
this.listenerSetter = new ListenerSetter();
this.constructBubbles();
// * constructor end
this.batchProcessor = new BatchProcessor({
log: this.log,
process: this.processBatch,
possibleError: PEER_CHANGED_ERROR
});
this.bubbleGroups = new BubbleGroups(this.chat);
this.preloader = new ProgressivePreloader({
cancelable: false
});
this.lazyLoadQueue = new LazyLoadQueue(undefined, true);
this.lazyLoadQueue.queueId = ++queueId;
// this.reactions = new Map();
// * events
// will call when sent for update pos
this.listenerSetter.add(rootScope)('history_update', async({storageKey, sequential, message}) => {
if(this.chat.messagesStorageKey !== storageKey || this.chat.type === 'scheduled') {
return;
}
const {mid} = message;
const log = false ? this.log.bindPrefix('history_update-' + mid) : undefined;
log && log('start');
const bubble = this.bubbles[mid];
if(!bubble) return;
if(this.renderNewPromises.size) {
log && log.error('will await new messages render');
await Promise.all(Array.from(this.renderNewPromises));
}
if(this.messagesQueuePromise) {
log && log.error('messages render in process');
await this.messagesQueuePromise;
}
if(this.bubbles[mid] !== bubble) return;
// await getHeavyAnimationPromise();
const item = this.bubbleGroups.getItemByBubble(bubble);
if(!item) { // probably a group item
log && log.error('no item by bubble', bubble);
return;
} else if(item.mid === mid) {
log && log.warn('wow what', item, mid);
return;
}
if(sequential) {
const group = item.group;
const newItem = this.bubbleGroups.createItem(bubble, message);
// newItem.mid = item.mid;
const _items = this.bubbleGroups.itemsArr.slice();
indexOfAndSplice(_items, item);
const foundItem = this.bubbleGroups.findGroupSiblingByItem(newItem, _items);
if(
group === foundItem?.group ||
(group === this.bubbleGroups.getLastGroup() && group.items.length === 1 && newItem.dateTimestamp === item.dateTimestamp) ||
(this.peerId === rootScope.myId && sequential && newItem.dateTimestamp === item.dateTimestamp)
) {
log && log('item has correct position', item);
this.bubbleGroups.changeBubbleMid(bubble, mid);
return;
}
}
// return;
// await fastRafPromise();
// if(this.bubbles[mid] !== bubble) return;
// const groupIndex = this.bubbleGroups.groups.indexOf(group);
this.bubbleGroups.removeAndUnmountBubble(bubble);
// if(!group.items.length) { // group has collapsed, next message can have higher mid so have to reposition them too
// log && log('group has collapsed', item);
// const siblingGroups = this.bubbleGroups.groups.slice(0, groupIndex + 1);
// for(let length = siblingGroups.length, i = length - 2; i >= 0; --i) {
// const siblingGroup = siblingGroups[i];
// const siblingItems = siblingGroup.items;
// const nextGroup = siblingGroups[i + 1];
// const nextItems = nextGroup.items;
// let _break = false, moved = false;
// for(let j = siblingItems.length - 1; j >= 0; --j) {
// const siblingItem = siblingItems[j];
// const foundItem = this.bubbleGroups.findGroupSiblingByItem(siblingItem, nextItems);
// if(!foundItem) {
// _break = true;
// break;
// }
// log('will move item', siblingItem, nextGroup);
// this.bubbleGroups.removeAndUnmountBubble(siblingItem.bubble);
// this.bubbleGroups.addItemToGroup(siblingItem, nextGroup);
// moved = true;
// }
// if(moved) {
// nextGroup.mount();
// }
// if(_break) {
// break;
// }
// }
// }
const {groups} = this.groupBubbles([{bubble, message}]);
this.bubbleGroups.mountUnmountGroups(groups);
if(this.scrollingToBubble) {
this.scrollToEnd();
}
log && log('end');
// this.bubbleGroups.findIncorrentPositions();
});
this.listenerSetter.add(rootScope)('dialog_flush', ({peerId}) => {
if(this.peerId === peerId) {
this.deleteMessagesByIds(Object.keys(this.bubbles).map((m) => +m));
}
});
// Calls when message successfully sent and we have an id
this.listenerSetter.add(rootScope)('message_sent', async(e) => {
const {storageKey, tempId, tempMessage, mid, message} = e;
// ! can't use peerId to validate here, because id can be the same in 'scheduled' and 'chat' types
if(this.chat.messagesStorageKey !== storageKey) {
return;
}
const bubbles = this.bubbles;
const _bubble = bubbles[tempId];
if(_bubble) {
const bubble = bubbles[tempId];
bubbles[mid] = bubble;
bubble.dataset.mid = '' + mid;
delete bubbles[tempId];
fastRaf(() => {
const mid = +bubble.dataset.mid;
if(bubbles[mid] === bubble && bubble.classList.contains('is-outgoing')) {
bubble.classList.remove('is-sending', 'is-outgoing');
bubble.classList.add((this.peerId === rootScope.myId && this.chat.type !== 'scheduled') || !this.unreadOut.has(mid) ? 'is-read' : 'is-sent');
}
});
}
if(this.unreadOut.has(tempId)) {
this.unreadOut.delete(tempId);
this.unreadOut.add(mid);
}
// * check timing of scheduled message
if(this.chat.type === 'scheduled') {
const timestamp = Date.now() / 1000 | 0;
const maxTimestamp = tempMessage.date - 10;
if(timestamp >= maxTimestamp) {
this.deleteMessagesByIds([mid]);
}
}
if(!_bubble) {
return;
}
let messages: (Message.message | Message.messageService)[], tempIds: number[];
const groupedId = (message as Message.message).grouped_id;
if(groupedId) {
messages = await this.managers.appMessagesManager.getMessagesByAlbum(groupedId);
const mids = messages.map(({mid}) => mid);
if(!mids.length || getMainMidForGrouped(mids) !== mid || bubbles[mid] !== _bubble) {
return;
}
if(bubbles[mid] !== _bubble) {
return;
}
tempIds = (Array.from(_bubble.querySelectorAll('.grouped-item')) as HTMLElement[]).map((el) => +el.dataset.mid);
} else {
messages = [message];
tempIds = [tempId];
}
const reactionsElements = Array.from(_bubble.querySelectorAll('reactions-element')) as ReactionsElement[];
if(reactionsElements.length) {
reactionsElements.forEach((reactionsElement) => {
reactionsElement.changeMessage(message as Message.message);
});
}
(messages as Message.message[]).forEach((message, idx) => {
if(!message) {
return;
}
const tempId = tempIds[idx];
const mid = message.mid;
const bubble: HTMLElement = _bubble.querySelector(`.document-container[data-mid="${mid}"]`) || _bubble;
if(message._ !== 'message') {
return;
}
if(message.replies) {
const repliesElement = _bubble.querySelector('replies-element') as RepliesElement;
if(repliesElement) {
repliesElement.message = message;
repliesElement.init();
}
}
const media = message.media ?? {} as MessageMedia.messageMediaEmpty;
const doc = (media as MessageMedia.messageMediaDocument).document as Document.document;
const poll = (media as MessageMedia.messageMediaPoll).poll;
const webPage = (media as MessageMedia.messageMediaWebPage).webpage as WebPage.webPage;
if(doc) {
const div = bubble.querySelector(`.document-container[data-mid="${tempId}"] .document`);
if(div) {
const container = findUpClassName(div, 'document-container');
if(!tempMessage.media?.document?.thumbs?.length && doc.thumbs?.length) {
getHeavyAnimationPromise().then(async() => {
const timeSpan = div.querySelector('.time');
const newDiv = await wrapDocument({message, fontSize: rootScope.settings.messagesTextSize});
div.replaceWith(newDiv);
if(timeSpan) {
(newDiv.querySelector('.document') || newDiv).append(timeSpan);
}
});
}
if(container) {
container.dataset.mid = '' + mid;
}
}
const element = bubble.querySelector(`audio-element[data-mid="${tempId}"], .document[data-doc-id="${tempId}"], .media-round[data-mid="${tempId}"]`) as HTMLElement;
if(element) {
if(element instanceof AudioElement || element.classList.contains('media-round')) {
element.dataset.mid = '' + message.mid;
delete element.dataset.isOutgoing;
(element as AudioElement).message = message;
(element as AudioElement).onLoad(true);
} else {
element.dataset.docId = '' + doc.id;
(element as any).doc = doc;
}
}
} else if(poll) {
const pollElement = bubble.querySelector('poll-element') as PollElement;
if(pollElement) {
pollElement.message = message;
pollElement.setAttribute('poll-id', '' + poll.id);
pollElement.setAttribute('message-id', '' + mid);
}
} else if(webPage && !bubble.querySelector('.web')) {
getHeavyAnimationPromise().then(() => {
this.safeRenderMessage(message, true, bubble);
this.scrollToBubbleIfLast(bubble);
});
}
// set new mids to album items for mediaViewer
if(groupedId) {
const item = (bubble.querySelector(`.grouped-item[data-mid="${tempId}"]`) as HTMLElement) || bubble; // * it can be .document-container
if(item) {
item.dataset.mid = '' + mid;
}
}
});
});
this.listenerSetter.add(rootScope)('message_edit', async({storageKey, message}) => {
if(storageKey !== this.chat.messagesStorageKey) return;
const bubble = this.bubbles[message.mid];
if(!bubble) return;
await getHeavyAnimationPromise();
if(this.bubbles[message.mid] !== bubble) return;
this.safeRenderMessage(message, true, bubble);
});
this.listenerSetter.add(rootScope)('message_error', async({storageKey, tempId}) => {
if(storageKey !== this.chat.messagesStorageKey) return;
const bubble = this.bubbles[tempId];
if(!bubble) return;
await getHeavyAnimationPromise();
if(this.bubbles[tempId] !== bubble) return;
bubble.classList.remove('is-outgoing');
bubble.classList.add('is-error');
});
this.listenerSetter.add(rootScope)('message_transcribed', ({peerId, mid, text, pending}) => {
if(peerId !== this.peerId) return;
const bubble = this.bubbles[mid];
if(!bubble) return;
// TODO: Move it to AudioElement method `finishVoiceTranscription`
const audioElement = bubble.querySelector('audio-element') as AudioElement;
if(!audioElement) {
return;
}
// const scrollSaver = this.createScrollSaver(false);
// scrollSaver.save();
const speechTextDiv = bubble.querySelector('.document-wrapper, .quote-text.has-document') as HTMLElement;
const speechRecognitionIcon = audioElement.querySelector('.audio-to-text-button span');
const speechRecognitionLoader = audioElement.querySelector('.loader');
if(speechTextDiv && speechRecognitionIcon) {
let transcribedText = speechTextDiv.querySelector('.audio-transcribed-text');
if(!transcribedText) {
transcribedText = document.createElement('div');
transcribedText.classList.add('audio-transcribed-text');
transcribedText.append(document.createTextNode(''));
if(speechTextDiv.classList.contains('document-wrapper')) {
audioElement.before(transcribedText);
} else {
speechTextDiv.append(transcribedText);
}
if(pending) {
const dots = document.createElement('span');
dots.classList.add('audio-transcribing-dots');
transcribedText.append(dots);
}
} else if(!pending) {
const dots = transcribedText.querySelector('.audio-transcribing-dots');
dots?.remove();
}
if(!text && !pending/* && !transcribedText.classList.contains('has-some-text') */) {
transcribedText.replaceChildren(i18n('Chat.Voice.Transribe.Error'));
transcribedText.classList.add('is-error');
} else if(text) {
// transcribedText.classList.add('has-some-text');
transcribedText.firstChild.textContent = text;
}
speechRecognitionIcon.classList.remove('tgico-transcribe');
speechRecognitionIcon.classList.add('tgico-up');
if(!pending && speechRecognitionLoader) {
speechRecognitionLoader.classList.remove('active');
setTimeout(() => {
speechRecognitionLoader.remove();
}, 300);
}
audioElement.transcriptionState = 2;
}
// scrollSaver.restore();
});
this.listenerSetter.add(rootScope)('album_edit', ({peerId, messages, deletedMids}) => {
if(peerId !== this.peerId) return;
const mids = messages.map(({mid}) => mid);
const oldMids = mids.concat(Array.from(deletedMids));
const wasMainMid = getMainMidForGrouped(oldMids);
const bubble = this.bubbles[wasMainMid];
if(!bubble) {
return;
}
const mainMid = getMainMidForGrouped(mids);
const message = messages.find((message) => message.mid === mainMid);
this.safeRenderMessage(message, true, bubble);
});
// this.listenerSetter.add(rootScope)('peer_title_edit', async(peerId) => {
// if(peerId.isUser()) {
// const middleware = this.getMiddleware();
// const user = await this.managers.appUsersManager.getUser(peerId.toUserId());
// if(!middleware()) return;
// const isPremium = user?.pFlags?.premium;
// const groups = this.bubbleGroups.groups.filter((group) => group.avatar?.peerId === peerId);
// groups.forEach((group) => {
// group.avatar.classList.toggle('is-premium', isPremium);
// group.avatar.classList.toggle('tgico-star', isPremium);
// });
// }
// });
if(!DO_NOT_UPDATE_MESSAGE_REACTIONS/* && false */) {
this.listenerSetter.add(rootScope)('messages_reactions', async(arr) => {
if(this.chat.type === 'scheduled') {
return;
}
let scrollSaver: ScrollSaver;
const a = arr.map(async({message, changedResults}) => {
if(this.peerId !== message.peerId) {
return;
}
const result = await this.getMountedBubble(message.mid, message);
if(!result) {
return;
}
// can be .document-container
return {bubble: findUpClassName(result.bubble, 'bubble'), message, changedResults};
});
let top: number;
(await Promise.all(a)).filter(Boolean).forEach(({bubble, message, changedResults}) => {
if(!scrollSaver) {
scrollSaver = this.createScrollSaver(false);
scrollSaver.save();
}
const key = message.peerId + '_' + message.mid;
const set = REACTIONS_ELEMENTS.get(key);
if(set) {
for(const element of set) {
element.update(message, changedResults);
}
} else if(!message.reactions || !message.reactions.results.length) {
return;
} else {
this.appendReactionsElementToBubble(bubble, message, message, changedResults);
}
});
if(scrollSaver) {
scrollSaver.restore();
}
});
}
!DO_NOT_UPDATE_MESSAGE_REPLY && this.listenerSetter.add(rootScope)('messages_downloaded', async({peerId, mids}) => {
const middleware = this.getMiddleware();
await getHeavyAnimationPromise();
if(!middleware()) return;
(mids as number[]).forEach((mid) => {
const needUpdate = this.needUpdate;
const filtered: typeof needUpdate[0][] = [];
forEachReverse(this.needUpdate, (obj, idx) => {
if(obj.replyMid === mid && obj.replyToPeerId === peerId) {
this.needUpdate.splice(idx, 1)[0];
filtered.push(obj);
}
});
filtered.forEach(async({mid, replyMid, replyToPeerId}) => {
const bubble = this.bubbles[mid];
if(!bubble) return;
const [message, originalMessage] = await Promise.all([
(await this.chat.getMessage(mid)) as Message.message,
(await this.managers.appMessagesManager.getMessageByPeer(replyToPeerId, replyMid)) as Message.message
]);
if(!middleware()) return;
MessageRender.setReply({
chat: this.chat,
bubble,
message
});
let maxMediaTimestamp: number;
const timestamps = bubble.querySelectorAll<HTMLAnchorElement>('.timestamp');
if(originalMessage && (maxMediaTimestamp = getMediaDurationFromMessage(originalMessage))) {
timestamps.forEach((timestamp) => {
const value = +timestamp.dataset.timestamp;
if(value < maxMediaTimestamp) {
timestamp.classList.remove('is-disabled');
} else {
timestamp.removeAttribute('href');
}
});
}
});
});
});
attachStickerViewerListeners({
listenTo: this.scrollable.container,
listenerSetter: this.listenerSetter,
findTarget: (e) => {
const target = e.target as HTMLElement;
const found = target.closest('.attachment.media-sticker-wrapper') || (findUpClassName(target, 'attachment') && target.closest('.custom-emoji'));
return found as HTMLElement;
}
});
attachClickEvent(this.scrollable.container, this.onBubblesClick, {listenerSetter: this.listenerSetter});
// this.listenerSetter.add(this.bubblesContainer)('click', this.onBubblesClick/* , {capture: true, passive: false} */);
this.listenerSetter.add(this.scrollable.container)('mousedown', (e) => {
if(e.button !== 0) return;
const code: HTMLElement = findUpTag(e.target, 'CODE');
if(code) {
cancelEvent(e);
copyFromElement(code);
const onClick = (e: MouseEvent) => {
cancelEvent(e);
toastNew({
langPackKey: 'TextCopied',
onClose: () => {
detach();
}
});
};
const detach = attachClickEvent(window, onClick, {listenerSetter: this.listenerSetter, once: true, capture: true});
return;
}
});
/* if(false) */this.stickyIntersector = new StickyIntersector(this.scrollable.container, (stuck, target) => {
for(const timestamp in this.dateMessages) {
const dateMessage = this.dateMessages[timestamp];
if(dateMessage.container === target) {
const dateBubble = dateMessage.div;
// dateMessage.container.classList.add('has-sticky-dates');
// SetTransition(dateBubble, 'kek', stuck, this.previousStickyDate ? 300 : 0);
// if(this.previousStickyDate) {
// dateBubble.classList.add('kek');
// }
dateBubble.classList.toggle('is-sticky', stuck);
if(stuck) {
this.previousStickyDate = dateBubble;
}
break;
}
}
if(this.previousStickyDate) {
// fastRaf(() => {
// this.bubblesContainer.classList.add('has-sticky-dates');
// });
}
});
if(!IS_SAFARI) {
this.sliceViewportDebounced = debounce(this.sliceViewport.bind(this), 3000, false, true);
}
let middleware: ReturnType<ChatBubbles['getMiddleware']>;
useHeavyAnimationCheck(() => {
this.isHeavyAnimationInProgress = true;
this.lazyLoadQueue.lock();
middleware = this.getMiddleware();
// if(this.sliceViewportDebounced) {
// this.sliceViewportDebounced.clearTimeout();
// }
}, () => {
this.isHeavyAnimationInProgress = false;
if(middleware?.()) {
this.lazyLoadQueue.unlockAndRefresh();
// if(this.sliceViewportDebounced) {
// this.sliceViewportDebounced();
// }
}
middleware = null;
}, this.listenerSetter);
}
private constructBubbles() {
const container = this.container = document.createElement('div');
container.classList.add('bubbles', 'scrolled-down');
const chatInner = this.chatInner = document.createElement('div');
chatInner.classList.add('bubbles-inner');
this.setScroll();
container.append(this.scrollable.container);
}
public attachContainerListeners() {
const container = this.container;
this.chat.contextMenu.attachTo(container);
this.chat.selection.attachListeners(container, new ListenerSetter());
if(DEBUG) {
this.listenerSetter.add(container)('dblclick', async(e) => {
const bubble = findUpClassName(e.target, 'grouped-item') || findUpClassName(e.target, 'bubble');
if(bubble) {
const mid = +bubble.dataset.mid
this.log('debug message:', await this.chat.getMessage(mid));
this.highlightBubble(bubble);
}
});
}
if(!IS_MOBILE) {
this.listenerSetter.add(container)('dblclick', async(e) => {
if(this.chat.type === 'pinned' ||
this.chat.selection.isSelecting ||
!(await this.chat.canSend())) {
return;
}
const target = e.target as HTMLElement;
const bubble = target.classList.contains('bubble') ?
target :
(target.classList.contains('document-selection') ? target.parentElement : null);
if(bubble && !bubble.classList.contains('bubble-first')) {
const mid = +bubble.dataset.mid;
const message = await this.chat.getMessage(mid);
if(message.pFlags.is_outgoing) {
return;
}
this.chat.input.initMessageReply(mid);
}
});
} else if(IS_TOUCH_SUPPORTED) {
const className = 'is-gesturing-reply';
const MAX = 64;
const replyAfter = MAX * .75;
let shouldReply = false;
let target: HTMLElement;
let icon: HTMLElement;
let swipeAvatar: HTMLElement;
handleHorizontalSwipe({
element: container,
verifyTouchTarget: async(e) => {
if(this.chat.type === 'pinned' || this.chat.selection.isSelecting || !(await this.chat.canSend())) {
return false;
}
// cancelEvent(e);
target = findUpClassName(e.target, 'bubble');
if(target) {
try {
const avatar = target.parentElement.querySelector('.bubbles-group-avatar') as HTMLElement
if(avatar) {
const visibleRect = getVisibleRect(avatar, target);
if(visibleRect) {
swipeAvatar = avatar;
}
}
} catch(err) {}
[target, swipeAvatar].filter(Boolean).forEach((element) => {
SetTransition({
element,
className,
forwards: true,
duration: 250
});
void element.offsetLeft; // reflow
});
if(!icon) {
icon = document.createElement('span');
icon.classList.add('tgico-reply_filled', 'bubble-gesture-reply-icon');
} else {
icon.classList.remove('is-visible');
icon.style.opacity = '';
}
target/* .querySelector('.bubble-content') */.append(icon);
}
return !!target;
},
onSwipe: (xDiff) => {
shouldReply = xDiff >= replyAfter;
if(shouldReply && !icon.classList.contains('is-visible')) {
icon.classList.add('is-visible');
}
icon.style.opacity = '' + Math.min(1, xDiff / replyAfter);
const x = -Math.max(0, Math.min(MAX, xDiff));
const transform = `translateX(${x}px)`;
target.style.transform = transform;
if(swipeAvatar) {
swipeAvatar.style.transform = transform;
}
cancelContextMenuOpening();
},
onReset: () => {
const _target = target;
const _swipeAvatar = swipeAvatar;
target = swipeAvatar = undefined;
const onTransitionEnd = () => {
if(icon.parentElement === _target) {
icon.classList.remove('is-visible');
icon.remove();
}
};
[_target, _swipeAvatar].filter(Boolean).forEach((element, idx) => {
SetTransition({
element,
className,
forwards: false,
duration: 250,
onTransitionEnd: idx === 0 ? onTransitionEnd : undefined
});
});
fastRaf(() => {
_target.style.transform = '';
if(_swipeAvatar) {
_swipeAvatar.style.transform = '';
}
if(shouldReply) {
const {mid} = _target.dataset;
this.chat.input.initMessageReply(+mid);
shouldReply = false;
}
});
},
listenerOptions: {capture: true}
});
}
}
public constructPeerHelpers() {
// will call when message is sent (only 1)
this.listenerSetter.add(rootScope)('history_append', async({storageKey, message}) => {
if(storageKey !== this.chat.messagesStorageKey || this.chat.type === 'scheduled') return;
if(liteMode.isAvailable('chat_background')) {
this.updateGradient = true;
}
if(!this.scrollable.loadedAll.bottom) {
this.chat.setMessageId();
} else {
this.renderNewMessage(message, true);
}
});
this.listenerSetter.add(rootScope)('history_multiappend', (message) => {
if(this.peerId !== message.peerId || this.chat.type === 'scheduled') return;
this.renderNewMessage(message);
});
this.listenerSetter.add(rootScope)('history_delete', ({peerId, msgs}) => {
if(peerId !== this.peerId || this.chat.type === 'scheduled') {
return;
}
this.deleteMessagesByIds(Array.from(msgs));
});
this.listenerSetter.add(rootScope)('dialog_unread', ({peerId}) => {
if(peerId === this.peerId) {
this.chat.input.setUnreadCount();
getHeavyAnimationPromise().then(() => {
this.updateUnreadByDialog();
});
}
});
this.listenerSetter.add(rootScope)('dialogs_multiupdate', (dialogs) => {
if(!dialogs.has(this.peerId) || this.chat.type === 'scheduled') {
return;
}
this.chat.input.setUnreadCount();
});
this.listenerSetter.add(rootScope)('dialog_notify_settings', (dialog) => {
if(this.peerId !== dialog.peerId || this.chat.type === 'scheduled') {
return;
}
this.chat.input.setUnreadCount();
});
this.listenerSetter.add(rootScope)('chat_update', async(chatId) => {
const {peerId} = this;
if(peerId !== chatId.toPeerId(true)) {
return;
}
const chat = await this.managers.appChatsManager.getChat(chatId);
const hadRights = this.chatInner.classList.contains('has-rights');
const hadPlainRights = this.chat.input.canSendPlain();
const [hasRights, hasPlainRights, canEmbedLinks] = await Promise.all([
this.chat.canSend('send_messages'),
this.chat.canSend('send_plain'),
this.chat.canSend('embed_links')
]);
if(hadRights !== hasRights || hadPlainRights !== hasPlainRights) {
const callbacks = await Promise.all([
this.finishPeerChange(),
this.chat.input.finishPeerChange({middleware: this.getMiddleware()})
]);
callbacks.forEach((callback) => callback());
}
// reset webpage
if((canEmbedLinks && !this.chat.input.willSendWebPage) || (!canEmbedLinks && this.chat.input.willSendWebPage)) {
this.chat.input.lastUrl = '';
this.chat.input.onMessageInput();
}
if(!!(chat as MTChat.channel).pFlags.forum !== this.chat.isForum && this.chat.type === 'chat') {
this.chat.peerId = 0;
this.chat.appImManager.setPeer({peerId});
}
});
this.listenerSetter.add(rootScope)('history_reload', (peerId) => {
if(peerId !== this.peerId) {
return;
}
const mids = getObjectKeysAndSort(this.bubbles, 'desc').filter((mid) => mid > 0);
const middleware = this.getMiddleware();
this.managers.appMessagesManager.reloadMessages(this.peerId, mids).then((messages) => {
if(!middleware()) return;
const toDelete: number[] = [];
messages.forEach((message, idx) => {
const mid = mids[idx];
if(message) {
const bubble = this.bubbles[message.mid];
if(!bubble) return;
this.safeRenderMessage(message, true, bubble);
} else {
toDelete.push(mid);
}
});
this.deleteMessagesByIds(toDelete);
this.setLoaded('top', false);
this.setLoaded('bottom', false);
this.scrollable.checkForTriggers();
});
});
this.listenerSetter.add(rootScope)('settings_updated', async({key}) => {
if(key === 'settings.emoji.big') {
const middleware = this.getMiddleware();
const mids = getObjectKeysAndSort(this.bubbles, 'desc');
const m = mids.map(async(mid) => {
const bubble = this.bubbles[mid];
if(bubble.classList.contains('can-have-big-emoji')) {
return {bubble, message: await this.chat.getMessage(mid)};
}
});
const awaited = await Promise.all(m);
if(!middleware()) {
return;
}
awaited.filter(Boolean).forEach(({bubble, message}) => {
if(this.bubbles[message.mid] !== bubble) {
return;
}
this.safeRenderMessage(message, true, bubble);
});
}
});
!DO_NOT_UPDATE_MESSAGE_VIEWS && this.listenerSetter.add(rootScope)('messages_views', (arr) => {
if(this.chat.type === 'scheduled') return;
fastRaf(() => {
let scrollSaver: ScrollSaver;
for(const {peerId, views, mid} of arr) {
if(this.peerId !== peerId) continue;
const bubble = this.bubbles[mid];
if(!bubble) continue;
const postViewsElements = Array.from(bubble.querySelectorAll('.post-views')) as HTMLElement[];
if(!postViewsElements.length) continue;
const str = formatNumber(views, 1);
let different = false;
postViewsElements.forEach((postViews) => {
if(different || postViews.textContent !== str) {
if(!scrollSaver) {
scrollSaver = this.createScrollSaver(true);
scrollSaver.save();
}
different = true;
postViews.textContent = str;
}
});
}
scrollSaver?.restore();
});
});
this.observer = new SuperIntersectionObserver({root: this.scrollable.container});
this.sendViewCountersDebounced = debounce(() => {
const mids = [...this.viewsMids];
this.viewsMids.clear();
this.managers.appMessagesManager.incrementMessageViews(this.peerId, mids);
}, 1000, false, true);
// * pinned part start
this.listenerSetter.add(rootScope)('peer_pinned_messages', ({peerId, mids, pinned}) => {
if(this.chat.type !== 'pinned' || peerId !== this.peerId) {
return;
}
if(mids) {
if(!pinned) {
this.deleteMessagesByIds(mids);
}
}
});
// * pinned part end
// * scheduled part start
const onUpdate = async() => {
this.chat.topbar.setTitle((await this.managers.appMessagesManager.getScheduledMessagesStorage(this.peerId)).size);
};
this.listenerSetter.add(rootScope)('scheduled_new', (message) => {
if(this.chat.type !== 'scheduled' || message.peerId !== this.peerId) return;
this.renderNewMessage(message);
onUpdate();
});
this.listenerSetter.add(rootScope)('scheduled_delete', ({peerId, mids}) => {
if(this.chat.type !== 'scheduled' || peerId !== this.peerId) return;
this.deleteMessagesByIds(mids);
onUpdate();
});
// * scheduled part end
}
private get peerId() {
return this.chat.peerId;
}
public get messagesQueuePromise() {
return this.batchProcessor.queuePromise;
}
private createScrollSaver(reverse = true) {
const scrollSaver = new ScrollSaver(this.scrollable, '.bubble:not(.is-date)', reverse);
return scrollSaver;
}
private unreadedObserverCallback = (entry: IntersectionObserverEntry) => {
if(entry.isIntersecting) {
const target = entry.target as HTMLElement;
const mid = this.unreaded.get(target as HTMLElement);
this.onUnreadedInViewport(target, mid);
}
};
private viewsObserverCallback = (entry: IntersectionObserverEntry) => {
if(entry.isIntersecting) {
const mid = +(entry.target as HTMLElement).dataset.mid;
this.observer.unobserve(entry.target, this.viewsObserverCallback);
if(mid) {
this.viewsMids.add(mid);
this.sendViewCountersDebounced();
} else {
const {sponsoredMessage} = this;
const randomId = sponsoredMessage?.random_id;
if(randomId) {
this.managers.appChatsManager.viewSponsoredMessage(this.peerId.toChatId(), randomId);
delete sponsoredMessage.random_id;
}
}
}
};
private stickerEffectObserverCallback = (entry: IntersectionObserverEntry) => {
if(entry.isIntersecting) {
this.observer.unobserve(entry.target, this.stickerEffectObserverCallback);
const attachmentDiv: HTMLElement = entry.target.querySelector('.attachment');
getHeavyAnimationPromise().then(() => {
if(isInDOM(attachmentDiv)) {
simulateClickEvent(attachmentDiv);
}
});
}
};
private createResizeObserver() {
if(!('ResizeObserver' in window) || this.resizeObserver) {
return;
}
const container = this.scrollable.container;
let wasHeight = 0/* container.offsetHeight */;
let resizing = false;
let skip = false;
let scrolled = 0;
let part = 0;
let rAF = 0;
// let skipNext = true;
const onResizeEnd = () => {
const height = container.offsetHeight;
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, part, this.scrollable.scrollTop, height, wasHeight, this.scrollable.isScrolledDown);
} */
if(part) {
this.scrollable.setScrollTopSilently(this.scrollable.scrollTop + Math.round(part));
}
wasHeight = height;
scrolled = 0;
rAF = 0;
part = 0;
resizing = false;
skip = false;
};
const setEndRAF = (single: boolean) => {
if(rAF) window.cancelAnimationFrame(rAF);
rAF = window.requestAnimationFrame(single ? onResizeEnd : () => {
rAF = window.requestAnimationFrame(onResizeEnd);
// this.log('resize after RAF', part);
});
};
const processEntries: ResizeObserverCallback = (entries) => {
/* if(skipNext) {
skipNext = false;
return;
} */
if(skip) {
setEndRAF(false);
return;
}
const entry = entries[0];
const height = entry.contentRect.height;/* Math.ceil(entry.contentRect.height); */
if(!wasHeight) {
wasHeight = height;
return;
}
const realDiff = wasHeight - height;
let diff = realDiff + part;
const _part = diff % 1;
diff -= _part;
if(!resizing) {
resizing = true;
/* if(DEBUG) {
this.log('resize start', realDiff, this.scrollable.scrollTop, this.scrollable.container.offsetHeight, this.scrollable.isScrolledDown);
} */
if(realDiff < 0 && this.scrollable.isScrolledDown) {
// if(isSafari) { // * fix opening keyboard while ESG is active
part = -realDiff;
// }
skip = true;
setEndRAF(false);
return;
}
}
scrolled += diff;
/* if(DEBUG) {
this.log('resize', wasHeight - height, diff, this.scrollable.container.offsetHeight, this.scrollable.isScrolledDown, height, wasHeight);
} */
if(diff) {
const needScrollTop = this.scrollable.scrollTop + diff;
this.scrollable.setScrollTopSilently(needScrollTop);
}
setEndRAF(false);
part = _part;
wasHeight = height;
};
const resizeObserver = this.resizeObserver = new ResizeObserver(processEntries);
resizeObserver.observe(container);
}
private destroyResizeObserver() {
const resizeObserver = this.resizeObserver;
if(!resizeObserver) {
return;
}
resizeObserver.disconnect();
this.resizeObserver = undefined;
}
private onBubblesMouseMove = async(e: MouseEvent) => {
const content = findUpClassName(e.target, 'bubble-content');
if(
this.chat.type !== 'scheduled' &&
content &&
!this.chat.selection.isSelecting &&
!findUpClassName(e.target, 'service') &&
!findUpClassName(e.target, 'bubble-beside-button')
) {
const bubble = findUpClassName(content, 'bubble');
if(!this.chat.selection.canSelectBubble(bubble)) {
this.unhoverPrevious();
return;
}
let {hoverBubble, hoverReaction} = this;
if(bubble === hoverBubble) {
return;
}
this.unhoverPrevious();
hoverBubble = this.hoverBubble = bubble;
hoverReaction = this.hoverReaction;
// hoverReaction = contentWrapper.querySelector('.bubble-hover-reaction');
if(!hoverReaction) {
hoverReaction = this.hoverReaction = document.createElement('div');
hoverReaction.classList.add('bubble-hover-reaction');
const stickerWrapper = document.createElement('div');
stickerWrapper.classList.add('bubble-hover-reaction-sticker');
hoverReaction.append(stickerWrapper);
content.append(hoverReaction);
let message = await this.chat.getMessage(+bubble.dataset.mid);
if(message?._ !== 'message') {
this.unhoverPrevious();
return;
}
message = await this.managers.appMessagesManager.getGroupsFirstMessage(message);
const middleware = this.getMiddleware(() => this.hoverReaction === hoverReaction);
Promise.all([
this.managers.appReactionsManager.getAvailableReactionsByMessage(message),
pause(400)
]).then(([availableReactions]) => {
const availableReaction = availableReactions[0];
if(!availableReaction) {
hoverReaction.remove();
return;
}
wrapSticker({
div: stickerWrapper,
doc: availableReaction.select_animation,
width: 18,
height: 18,
needUpscale: true,
middleware,
group: this.chat.animationGroup,
withThumb: false,
needFadeIn: false
}).then(({render}) => render).then((player) => {
assumeType<RLottiePlayer>(player);
player.addEventListener('firstFrame', () => {
if(!middleware()) {
// debugger;
return;
}
hoverReaction.dataset.loaded = '1';
this.setHoverVisible(hoverReaction, true);
}, {once: true});
attachClickEvent(hoverReaction, (e) => {
cancelEvent(e); // cancel triggering selection
this.managers.appReactionsManager.sendReaction(message as Message.message, availableReaction);
this.unhoverPrevious();
}, {listenerSetter: this.listenerSetter});
}, noop);
});
} else if(hoverReaction.dataset.loaded) {
this.setHoverVisible(hoverReaction, true);
}
} else {
this.unhoverPrevious();
}
};
public setReactionsHoverListeners() {
this.listenerSetter.add(contextMenuController)('toggle', this.unhoverPrevious);
this.listenerSetter.add(overlayCounter)('change', this.unhoverPrevious);
this.listenerSetter.add(this.chat.selection)('toggle', this.unhoverPrevious);
this.listenerSetter.add(this.container)('mousemove', this.onBubblesMouseMove);
}
private setHoverVisible(hoverReaction: HTMLElement, visible: boolean) {
if(hoverReaction.parentElement) {
hoverReaction.parentElement.classList.toggle('hover-reaction-visible', visible);
}
SetTransition({
element: hoverReaction,
className: 'is-visible',
forwards: visible,
duration: 200,
onTransitionEnd: visible ? undefined : () => {
hoverReaction.remove();
},
useRafs: 2
});
}
private unhoverPrevious = () => {
const {hoverBubble, hoverReaction} = this;
if(hoverBubble) {
this.setHoverVisible(hoverReaction, false);
this.hoverBubble = undefined;
this.hoverReaction = undefined;
}
};
public setStickyDateManually() {
return;
const timestamps = Object.keys(this.dateMessages).map((k) => +k).sort((a, b) => b - a);
let lastVisible: HTMLElement;
// if(this.chatInner.classList.contains('is-scrolling')) {
const {scrollTop} = this.scrollable.container;
const isOverflown = scrollTop > 0;
if(isOverflown) {
for(const timestamp of timestamps) {
const dateMessage = this.dateMessages[timestamp];
const visibleRect = getVisibleRect(dateMessage.container, this.scrollable.container);
if(visibleRect && visibleRect.overflow.top) {
lastVisible = dateMessage.div;
} else if(lastVisible) {
break;
}
}
}
// }
if(lastVisible === this.previousStickyDate) {
return;
}
if(lastVisible) {
const needReflow = /* !!this.chat.setPeerPromise || */!this.previousStickyDate;
if(needReflow) {
lastVisible.classList.add('no-transition');
}
lastVisible.classList.add('is-sticky');
if(needReflow) {
void lastVisible.offsetLeft; // reflow
lastVisible.classList.remove('no-transition');
}
}
if(this.previousStickyDate && this.previousStickyDate !== lastVisible) {
this.previousStickyDate.classList.remove('is-sticky');
}
this.previousStickyDate = lastVisible;
}
public getRenderedLength() {
return Object.keys(this.bubbles).length - this.skippedMids.size;
}
private onUnreadedInViewport(target: HTMLElement, mid: number) {
this.unreadedSeen.add(mid);
this.observer.unobserve(target, this.unreadedObserverCallback);
this.unreaded.delete(target);
this.readUnreaded();
}
private readUnreaded() {
if(this.readPromise) return;
const middleware = this.getMiddleware();
this.readPromise = idleController.getFocusPromise().then(async() => {
if(!middleware()) return;
let maxId = Math.max(...Array.from(this.unreadedSeen));
// ? if message with maxId is not rendered ?
if(this.scrollable.loadedAll.bottom) {
const bubblesMaxId = Math.max(...Object.keys(this.bubbles).map((i) => +i));
if(maxId >= bubblesMaxId) {
maxId = Math.max((await this.chat.getHistoryMaxId()) || 0, maxId);
}
}
this.unreaded.forEach((mid, target) => {
if(mid <= maxId) {
this.onUnreadedInViewport(target, mid);
}
});
const readContents: number[] = [];
for(const mid of this.unreadedSeen) {
const message: MyMessage = await this.chat.getMessage(mid);
if(isMentionUnread(message)) {
readContents.push(mid);
}
}
this.managers.appMessagesManager.readMessages(this.peerId, readContents);
this.unreadedSeen.clear();
if(DEBUG) {
this.log('will readHistory by maxId:', maxId);
}
// return;
return this.managers.appMessagesManager.readHistory(this.peerId, maxId, this.chat.threadId).catch((err: any) => {
this.log.error('readHistory err:', err);
this.managers.appMessagesManager.readHistory(this.peerId, maxId, this.chat.threadId);
}).finally(() => {
if(!middleware()) return;
this.readPromise = undefined;
if(this.unreadedSeen.size) {
this.readUnreaded();
}
});
});
}
public onBubblesClick = async(e: Event) => {
let target = e.target as HTMLElement;
let bubble: HTMLElement = null;
try {
bubble = findUpClassName(target, 'bubble');
} catch(err) {}
if(!bubble && !this.chat.selection.isSelecting) {
const avatar = findUpClassName(target, 'user-avatar');
if(!avatar) {
return;
}
const peerId = avatar.dataset.peerId.toPeerId();
if(peerId !== NULL_PEER_ID) {
this.chat.appImManager.setInnerPeer({peerId});
} else {
toast(I18n.format('HidAccount', true));
}
return;
}
if(bubble.classList.contains('is-date') && findUpClassName(target, 'bubble-content')) {
if(bubble.classList.contains('is-fake')) {
bubble = bubble.previousElementSibling as HTMLElement;
}
if(bubble.classList.contains('is-sticky') && !this.chatInner.classList.contains('is-scrolling')) {
return;
}
for(const timestamp in this.dateMessages) {
const d = this.dateMessages[timestamp];
if(d.div === bubble) {
PopupElement.createPopup(PopupDatePicker, new Date(+timestamp), this.onDatePick).show();
break;
}
}
return;
}
if(!IS_TOUCH_SUPPORTED && findUpClassName(target, 'time')) {
this.chat.selection.toggleByElement(bubble);
return;
}
// ! Trusted - due to audio autoclick
if(this.chat.selection.isSelecting && e.isTrusted) {
if(bubble.classList.contains('service') && bubble.dataset.mid === undefined) {
return;
}
cancelEvent(e);
// console.log('bubble click', e);
if(IS_TOUCH_SUPPORTED && this.chat.selection.selectedText) {
this.chat.selection.selectedText = undefined;
return;
}
// this.chatSelection.toggleByBubble(bubble);
this.chat.selection.toggleByElement(findUpClassName(target, 'grouped-item') || bubble);
return;
}
const mediaSpoiler: HTMLElement = findUpClassName(target, 'media-spoiler-container');
if(mediaSpoiler) {
onMediaSpoilerClick({
event: e,
mediaSpoiler
});
return;
}
const contactDiv: HTMLElement = findUpClassName(target, 'contact');
if(contactDiv) {
const peerId = contactDiv.dataset.peerId.toPeerId();
if(peerId) {
this.chat.appImManager.setInnerPeer({
peerId
});
} else {
const phone = contactDiv.querySelector<HTMLElement>('.contact-number');
copyTextToClipboard(phone.innerText.replace(/\s/g, ''));
toastNew({langPackKey: 'PhoneCopied'});
cancelEvent(e);
}
return;
}
const callDiv: HTMLElement = findUpClassName(target, 'bubble-call');
if(callDiv) {
this.chat.appImManager.callUser(this.peerId.toUserId(), callDiv.dataset.type as any);
return;
}
const buyButton: HTMLElement = findUpClassName(target, 'is-buy');
if(buyButton) {
cancelEvent(e);
const message = await this.chat.getMessage(+bubble.dataset.mid);
if(!message) {
return;
}
PopupElement.createPopup(
PopupPayment,
message as Message.message,
await this.managers.appPaymentsManager.getInputInvoiceByPeerId(message.peerId, message.mid)
);
return;
}
const reactionElement = findUpTag(target, 'REACTION-ELEMENT') as ReactionElement;
if(reactionElement) {
cancelEvent(e);
if(reactionElement.classList.contains('is-inactive')) {
return;
}
const reactionsElement = reactionElement.parentElement as ReactionsElement;
const reactionCount = reactionsElement.getReactionCount(reactionElement);
const message = reactionsElement.getMessage();
this.managers.appReactionsManager.sendReaction(message, reactionCount.reaction);
return;
}
const stickerEmojiEl = findUpAttribute(target, 'data-sticker-emoji');
if(stickerEmojiEl && stickerEmojiEl.parentElement.querySelectorAll('[data-sticker-emoji]').length === 1 && bubble.classList.contains('emoji-big')) {
onEmojiStickerClick({
event: e,
container: stickerEmojiEl,
managers: this.managers,
middleware: this.getMiddleware(),
peerId: this.peerId
});
return;
}
const commentsDiv: HTMLElement = findUpClassName(target, 'replies');
if(commentsDiv) {
const bubbleMid = +bubble.dataset.mid;
if(this.peerId === REPLIES_PEER_ID) {
const message = await this.chat.getMessage(bubbleMid) as Message.message;
const peerId = getPeerId(message.reply_to.reply_to_peer_id);
const threadId = message.reply_to.reply_to_top_id;
const lastMsgId = message.fwd_from.saved_from_msg_id;
this.chat.appImManager.openThread({
peerId,
lastMsgId,
threadId
});
} else {
const message1 = await this.chat.getMessage(bubbleMid);
const message = await this.managers.appMessagesManager.getMessageWithReplies(message1 as Message.message);
const replies = message.replies;
if(replies) {
this.managers.appMessagesManager.getDiscussionMessage(this.peerId, message.mid).then((message) => {
this.chat.appImManager.setInnerPeer({
peerId: replies.channel_id.toPeerId(true),
type: 'discussion',
threadId: (message as MyMessage).mid
});
});
}
}
return;
}
const via = findUpClassName(target, 'is-via');
if(via) {
const el = via.querySelector('.peer-title') as HTMLElement;
if(target === el || findUpAsChild(target, el)) {
const message = el.innerText + ' ';
this.managers.appDraftsManager.setDraft(this.peerId, this.chat.threadId, message);
cancelEvent(e);
return;
}
}
const nameDiv = findUpClassName(target, 'peer-title') || findUpTag(target, 'AVATAR-ELEMENT') || findUpAttribute(target, 'data-saved-from');
if(nameDiv && nameDiv !== bubble) {
target = nameDiv || target;
const peerIdStr = target.dataset.peerId || target.getAttribute('peer') || (target as AvatarElement).peerId;
const savedFrom = target.dataset.savedFrom;
if(typeof(peerIdStr) === 'string' || savedFrom) {
if(savedFrom) {
const [peerId, mid] = savedFrom.split('_');
if(target.classList.contains('is-receipt-link')) {
const message = await this.managers.appMessagesManager.getMessageByPeer(peerId.toPeerId(), +mid);
if(message) {
const inputInvoice = await this.managers.appPaymentsManager.getInputInvoiceByPeerId(this.peerId, +bubble.dataset.mid);
PopupElement.createPopup(PopupPayment, message as Message.message, inputInvoice, undefined, true);
}
} else {
this.chat.appImManager.setInnerPeer({
peerId: peerId.toPeerId(),
lastMsgId: +mid
});
}
} else {
const peerId = peerIdStr.toPeerId();
if(peerId !== NULL_PEER_ID) {
this.chat.appImManager.setInnerPeer({peerId});
} else {
toast(I18n.format('HidAccount', true));
}
}
}
return;
}
// this.log('chatInner click:', target);
// const isVideoComponentElement = target.tagName === 'SPAN' && findUpClassName(target, 'media-container');
/* if(isVideoComponentElement) {
const video = target.parentElement.querySelector('video') as HTMLElement;
if(video) {
video.click(); // hot-fix for time and play button
return;
}
} */
if(bubble.classList.contains('sticker') && target.parentElement.classList.contains('attachment')) {
const messageId = +bubble.dataset.mid;
const message = await this.chat.getMessage(messageId);
const doc = ((message as Message.message).media as MessageMedia.messageMediaDocument)?.document as Document.document;
if(doc?.stickerSetInput) {
PopupElement.createPopup(PopupStickers, doc.stickerSetInput).show();
}
return;
}
if(await this.checkTargetForMediaViewer(target, e)) {
return;
}
if(['IMG', 'DIV', 'SPAN'/* , 'A' */].indexOf(target.tagName) === -1) target = findUpTag(target, 'DIV');
if(['DIV', 'SPAN'].indexOf(target.tagName) !== -1/* || target.tagName === 'A' */) {
if(target.classList.contains('goto-original')) {
const savedFrom = bubble.dataset.savedFrom;
const [peerId, mid] = savedFrom.split('_');
// //this.log('savedFrom', peerId, msgID);
this.chat.appImManager.setInnerPeer({
peerId: peerId.toPeerId(),
lastMsgId: +mid
});
return;
} else if(target.classList.contains('forward')) {
const mid = +bubble.dataset.mid;
const message = await this.managers.appMessagesManager.getMessageByPeer(this.peerId, mid);
PopupElement.createPopup(PopupForward, {
[this.peerId]: await this.managers.appMessagesManager.getMidsByMessage(message)
});
// appSidebarRight.forwardTab.open([mid]);
return;
}
let isReplyClick = false;
try {
isReplyClick = !!findUpClassName(e.target, 'reply');
} catch(err) {}
if(isReplyClick && bubble.classList.contains('is-reply')/* || bubble.classList.contains('forwarded') */) {
const bubbleMid = +bubble.dataset.mid;
this.followStack.push(bubbleMid);
const message = (await this.chat.getMessage(bubbleMid)) as Message.message;
const replyToPeerId = message.reply_to.reply_to_peer_id ? getPeerId(message.reply_to.reply_to_peer_id) : this.peerId;
const replyToMid = message.reply_to.reply_to_msg_id;
this.chat.appImManager.setInnerPeer({
peerId: replyToPeerId,
lastMsgId: replyToMid,
type: this.chat.type,
threadId: this.chat.threadId
});
/* if(this.chat.type === 'discussion') {
this.chat.appImManager.setMessageId(, originalMessageId);
} else {
this.chat.appImManager.setInnerPeer(this.peerId, originalMessageId);
} */
// this.chat.setMessageId(, originalMessageId);
}
}
// console.log('chatInner click', e);
};
public async checkTargetForMediaViewer(target: HTMLElement, e?: Event, mediaTimestamp?: number) {
const bubble = findUpClassName(target, 'bubble');
const documentDiv = findUpClassName(target, 'document-with-thumb');
if((target.tagName === 'IMG' && !target.classList.contains('emoji') && !target.classList.contains('document-thumb')) ||
target.classList.contains('album-item') ||
// || isVideoComponentElement
(target.tagName === 'VIDEO' && !bubble.classList.contains('round')) ||
(documentDiv && !documentDiv.querySelector('.preloader-container')) ||
target.classList.contains('canvas-thumbnail')) {
const groupedItem = findUpClassName(target, 'album-item') || findUpClassName(target, 'document-container');
const preloader = (groupedItem || bubble).querySelector<HTMLElement>('.preloader-container');
if(preloader && e) {
simulateClickEvent(preloader);
cancelEvent(e);
return;
}
cancelEvent(e);
const messageId = +(groupedItem || bubble).dataset.mid;
const message = await this.chat.getMessage(messageId);
if(!message) {
this.log.warn('no message by messageId:', messageId);
return;
}
const SINGLE_MEDIA_CLASSNAME = 'webpage';
const isSingleMedia = bubble.classList.contains(SINGLE_MEDIA_CLASSNAME);
const f = documentDiv ? (media: any) => {
return AppMediaViewer.isMediaCompatibleForDocumentViewer(media);
} : (media: any) => {
return media._ === 'photo' || ['video', 'gif'].includes(media.type);
};
const targets: {element: HTMLElement, mid: number, peerId: PeerId}[] = [];
const ids = isSingleMedia ? [messageId] : (await Promise.all(Object.keys(this.bubbles).map((k) => +k).map(async(mid) => {
/* if(isSingleMedia && !this.bubbles[id].classList.contains(SINGLE_MEDIA_CLASSNAME)) {
return false;
} */
// if(!this.scrollable.visibleElements.find((e) => e.element === this.bubbles[id])) return false;
const message = await this.chat.getMessage(mid);
const media = getMediaFromMessage(message);
return media && f(media) && mid;
}))).filter(Boolean).sort((a, b) => a - b);
ids.forEach((id) => {
let selector: string;
if(documentDiv) {
selector = '.document-container';
} else {
const withTail = this.bubbles[id].classList.contains('with-media-tail');
selector = '.album-item video, .album-item img, .preview video, .preview img, ';
if(withTail) {
selector += '.bubble__media-container';
} else {
selector += '.attachment video, .attachment img';
}
}
const elements = Array.from(this.bubbles[id].querySelectorAll(selector)) as HTMLElement[];
const parents: Set<HTMLElement> = new Set();
if(documentDiv) {
elements.forEach((element) => {
targets.push({
element: element.querySelector('.document-ico'),
mid: +element.dataset.mid,
peerId: this.peerId
});
});
} else {
const hasAspecter = !!this.bubbles[id].querySelector('.media-container-aspecter');
elements.forEach((element) => {
if(hasAspecter && !findUpClassName(element, 'media-container-aspecter')) return;
const albumItem = findUpClassName(element, 'album-item');
const parent = albumItem || element.parentElement;
if(parents.has(parent)) return;
parents.add(parent);
targets.push({
element,
mid: albumItem ? +albumItem.dataset.mid : id,
peerId: this.peerId
});
});
}
});
targets.sort((a, b) => a.mid - b.mid);
const idx = targets.findIndex((t) => t.mid === messageId);
if(DEBUG) {
this.log('open mediaViewer single with ids:', ids, idx, targets);
}
if(!targets[idx]) {
this.log('no target for media viewer!', target);
return;
}
new AppMediaViewer()
.setSearchContext({
threadId: this.chat.threadId,
peerId: this.peerId,
inputFilter: {_: documentDiv ? 'inputMessagesFilterDocument' : 'inputMessagesFilterPhotoVideo'},
useSearch: this.chat.type !== 'scheduled' && !isSingleMedia,
isScheduled: this.chat.type === 'scheduled'
})
.openMedia({
message: message,
target: targets[idx].element,
fromRight: 0,
reverse: true,
prevTargets: targets.slice(0, idx),
nextTargets: targets.slice(idx + 1),
mediaTimestamp
});
return true;
}
}
public async onGoDownClick() {
if(!this.followStack.length) {
// this.onScroll(true, undefined, true);
this.chat.setMessageId(/* , dialog.top_message */);
// const dialog = this.appMessagesManager.getDialogByPeerId(this.peerId)[0];
// if(dialog) {
// this.chat.setPeer(this.peerId/* , dialog.top_message */);
// } else {
// this.log('will scroll down 3');
// this.scroll.scrollTop = this.scroll.scrollHeight;
// }
return;
}
const middleware = this.getMiddleware();
const slice = this.followStack.slice();
const messages = await Promise.all(slice.map((mid) => this.chat.getMessage(mid)));
if(!middleware()) return;
slice.forEach((mid, idx) => {
const message = messages[idx];
const bubble = this.bubbles[mid];
let bad = true;
if(bubble) {
const rect = bubble.getBoundingClientRect();
bad = (windowSize.height / 2) > rect.top;
} else if(message) {
bad = false;
}
if(bad) {
this.followStack.splice(this.followStack.indexOf(mid), 1);
}
});
this.followStack.sort((a, b) => b - a);
const mid = this.followStack.pop();
this.chat.setMessageId(mid);
}
public getBubbleByPoint(verticalSide: 'top' | 'bottom') {
const slice = this.getViewportSlice();
const item = slice.visible[verticalSide === 'top' ? 0 : slice.visible.length - 1];
return item?.element;
}
public async getGroupedBubble(groupId: string) {
const mids = await this.managers.appMessagesManager.getMidsByAlbum(groupId);
for(const mid of mids) {
if(this.bubbles[mid] && !this.skippedMids.has(mid)) {
// const maxId = Math.max(...mids); // * because in scheduled album can be rendered by lowest mid during sending
return {
bubble: this.bubbles[mid],
mid: mid
// message: await this.chat.getMessage(maxId) as Message.message
};
}
}
}
public getBubbleGroupedItems(bubble: HTMLElement) {
return Array.from(bubble.querySelectorAll('.grouped-item')) as HTMLElement[];
}
public async getMountedBubble(mid: number, message?: Message.message | Message.messageService) {
if(message === undefined) {
message = await this.chat.getMessage(mid);
}
if(!message) {
return;
}
const groupedId = (message as Message.message).grouped_id;
if(groupedId) {
const a = await this.getGroupedBubble(groupedId);
if(a) {
a.bubble = a.bubble.querySelector(`.document-container[data-mid="${mid}"]`) || a.bubble;
return a;
}
}
const bubble = this.bubbles[mid];
if(!bubble || this.skippedMids.has(mid)) return;
return {bubble, mid};
}
private findNextMountedBubbleByMsgId(mid: number, prev?: boolean) {
const mids = getObjectKeysAndSort(this.bubbles, prev ? 'desc' : 'asc');
let filterCallback: (_mid: number) => boolean;
if(prev) filterCallback = (_mid) => _mid < mid;
else filterCallback = (_mid) => mid < _mid;
const foundMid = mids.find((_mid) => {
if(!filterCallback(_mid)) return false;
return !!this.bubbles[_mid]?.parentElement;
});
return this.bubbles[foundMid];
}
public loadMoreHistory(top: boolean, justLoad = false) {
// this.log('loadMoreHistory', top);
if(
!this.peerId ||
/* TEST_SCROLL || */
this.chat.setPeerPromise ||
this.isHeavyAnimationInProgress ||
(top && (this.getHistoryTopPromise || this.scrollable.loadedAll.top)) ||
(!top && (this.getHistoryBottomPromise || this.scrollable.loadedAll.bottom))
) {
return;
}
// warning, если иды только отрицательные то вниз не попадёт (хотя мб и так не попадёт)
// some messages can have negative id (such as sponsored message)
const history = Object.keys(this.bubbles)
.map((id) => +id)
.filter((id) => id > 0 && !this.skippedMids.has(id))
.sort((a, b) => a - b);
if(!history.length) {
history.push(0);
}
if(top) {
if(DEBUG) {
this.log('Will load more (up) history by id:', history[0], 'maxId:', history[history.length - 1], justLoad/* , history */);
}
this.getHistory1(history[0], true, undefined, undefined, justLoad);
} else {
// let dialog = this.appMessagesManager.getDialogByPeerId(this.peerId)[0];
// const historyMaxId = await this.chat.getHistoryMaxId();
// // if scroll down after search
// if(history.indexOf(historyMaxId) !== -1) {
// this.setLoaded('bottom', true);
// return;
// }
if(DEBUG) {
this.log('Will load more (down) history by id:', history[history.length - 1], justLoad/* , history */);
}
this.getHistory1(history[history.length - 1], false, true, undefined, justLoad);
}
}
public onScroll = (ignoreHeavyAnimation?: boolean, scrollDimensions?: ScrollStartCallbackDimensions, forceDown?: boolean) => {
// return;
if(this.isHeavyAnimationInProgress) {
this.sliceViewportDebounced?.clearTimeout();
// * В таком случае, кнопка не будет моргать если чат в самом низу, и правильно отработает случай написания нового сообщения и проскролла вниз
if(this.scrolledDown && !ignoreHeavyAnimation) {
return;
}
} else {
this.chat.topbar.pinnedMessage?.setCorrectIndexThrottled(this.scrollable.lastScrollDirection);
this.sliceViewportDebounced?.();
this.setStickyDateManually();
}
// lottieLoader.checkAnimations(false, 'chat');
if(scrollDimensions && scrollDimensions.distanceToEnd < SCROLLED_DOWN_THRESHOLD && this.scrolledDown) {
return;
}
const distanceToEnd = forceDown ? 0 : scrollDimensions?.distanceToEnd ?? this.scrollable.getDistanceToEnd();
if(/* !IS_TOUCH_SUPPORTED && */(this.scrollable.lastScrollDirection !== 0 && distanceToEnd > 0) || scrollDimensions || forceDown) {
// if(/* !IS_TOUCH_SUPPORTED && */(this.scrollable.lastScrollDirection !== 0 || scrollDimensions) && distanceToEnd > 0) {
if(this.isScrollingTimeout) {
clearTimeout(this.isScrollingTimeout);
} else if(!this.chatInner.classList.contains('is-scrolling')) {
this.chatInner.classList.add('is-scrolling');
}
this.isScrollingTimeout = window.setTimeout(() => {
this.chatInner.classList.remove('is-scrolling');
this.isScrollingTimeout = 0;
}, 1350 + (scrollDimensions?.duration ?? 0));
}
if(distanceToEnd < SCROLLED_DOWN_THRESHOLD && (forceDown || this.scrollable.loadedAll.bottom || this.chat.setPeerPromise || !this.peerId)) {
this.container.classList.add('scrolled-down');
this.scrolledDown = true;
} else if(this.container.classList.contains('scrolled-down')) {
this.container.classList.remove('scrolled-down');
this.scrolledDown = false;
}
};
public setScroll() {
if(this.scrollable) {
this.destroyScrollable();
}
this.scrollable = new Scrollable(null, 'IM', /* 10300 */300);
this.setLoaded('top', false, false);
this.setLoaded('bottom', false, false);
this.scrollable.container.append(this.chatInner);
/* const getScrollOffset = () => {
//return Math.round(Math.max(300, appPhotosManager.windowH / 1.5));
return 300;
};
window.addEventListener('resize', () => {
this.scrollable.onScrollOffset = getScrollOffset();
});
this.scrollable = new Scrollable(this.bubblesContainer, 'y', 'IM', this.chatInner, getScrollOffset()); */
this.scrollable.onAdditionalScroll = this.onScroll;
this.scrollable.onScrolledTop = () => this.loadMoreHistory(true);
this.scrollable.onScrolledBottom = () => this.loadMoreHistory(false);
// this.scrollable.attachSentinels(undefined, 300);
if(IS_TOUCH_SUPPORTED && false) {
this.scrollable.container.addEventListener('touchmove', () => {
if(this.isScrollingTimeout) {
clearTimeout(this.isScrollingTimeout);
} else if(!this.chatInner.classList.contains('is-scrolling')) {
this.chatInner.classList.add('is-scrolling');
}
}, {passive: true});
this.scrollable.container.addEventListener('touchend', () => {
if(!this.chatInner.classList.contains('is-scrolling')) {
return;
}
if(this.isScrollingTimeout) {
clearTimeout(this.isScrollingTimeout);
}
this.isScrollingTimeout = window.setTimeout(() => {
this.chatInner.classList.remove('is-scrolling');
this.isScrollingTimeout = 0;
}, 1350);
}, {passive: true});
}
}
public async updateUnreadByDialog() {
const historyStorage = await this.chat.getHistoryStorage();
const maxId = this.peerId === rootScope.myId ? historyStorage.readMaxId : historyStorage.readOutboxMaxId;
// /////this.log('updateUnreadByDialog', maxId, dialog, this.unreadOut);
for(const msgId of this.unreadOut) {
if(msgId > 0 && msgId <= maxId) {
const bubble = this.bubbles[msgId];
if(bubble) {
this.unreadOut.delete(msgId);
if(bubble.classList.contains('is-outgoing') || bubble.classList.contains('is-error')) {
continue;
}
bubble.classList.remove('is-sent', 'is-sending', 'is-outgoing'); // is-sending can be when there are bulk of updates (e.g. sending command to Stickers bot)
bubble.classList.add('is-read');
}
}
}
}
public destroyBubble(bubble: HTMLElement, mid = +bubble.dataset.mid) {
// this.log.warn('destroy bubble', bubble, mid);
bubble.middlewareHelper.destroy();
/* const mounted = this.getMountedBubble(mid);
if(!mounted) return; */
if(this.bubbles[mid] === bubble) { // have to check because can clear bubble with same id later
delete this.bubbles[mid];
}
this.skippedMids.delete(mid);
if(this.firstUnreadBubble === bubble) {
this.firstUnreadBubble = null;
}
this.bubbleGroups.removeAndUnmountBubble(bubble);
if(this.observer) {
this.observer.unobserve(bubble, this.unreadedObserverCallback);
this.unreaded.delete(bubble);
this.observer.unobserve(bubble, this.viewsObserverCallback);
this.viewsMids.delete(mid);
this.observer.unobserve(bubble, this.stickerEffectObserverCallback);
}
// this.reactions.delete(mid);
}
public deleteMessagesByIds(mids: number[], permanent = true, ignoreOnScroll?: boolean) {
let deleted = false;
mids.forEach((mid) => {
const bubble = this.bubbles[mid];
if(!bubble) return;
this.destroyBubble(bubble, mid);
deleted = true;
});
if(!deleted) {
return;
}
this.scrollable.ignoreNextScrollEvent();
if(permanent && this.chat.selection.isSelecting) {
this.chat.selection.deleteSelectedMids(this.peerId, mids);
}
animationIntersector.checkAnimations(false, this.chat.animationGroup);
this.deleteEmptyDateGroups();
if(!ignoreOnScroll) {
this.scrollable.onScroll();
// this.onScroll();
}
}
private pollExtendedMediaMessages() {
const mids = Array.from(this.extendedMediaMessages);
return this.managers.appMessagesManager.getExtendedMedia(this.peerId, mids);
}
private setExtendedMediaMessagesPollInterval() {
if(this.pollExtendedMediaMessagesPromise || !this.extendedMediaMessages.size) {
return;
}
this.pollExtendedMediaMessagesPromise = pause(30000)
.then(() => this.pollExtendedMediaMessages())
.then(() => this.setExtendedMediaMessagesPollInterval());
}
private setTopPadding(middleware = this.getMiddleware()) {
let isPaddingNeeded = false;
let setPaddingTo: HTMLElement;
if(!this.isTopPaddingSet && this.chat.type !== 'scheduled') {
const {clientHeight, scrollHeight} = this.scrollable.container;
isPaddingNeeded = clientHeight === scrollHeight;
/* const firstEl = this.chatInner.firstElementChild as HTMLElement;
if(this.chatInner.firstElementChild) {
const visibleRect = getVisibleRect(firstEl, this.scrollable.container);
isPaddingNeeded = !visibleRect.overflow.top && (visibleRect.rect.top - firstEl.offsetTop) !== this.scrollable.container.getBoundingClientRect().top;
} else {
isPaddingNeeded = true;
} */
if(isPaddingNeeded) {
/* const add = clientHeight - scrollHeight;
this.chatInner.style.paddingTop = add + 'px';
this.scrollable.scrollTop += add; */
setPaddingTo = this.chatInner;
setPaddingTo.style.paddingTop = clientHeight + 'px';
this.scrollable.setScrollTopSilently(scrollHeight);
this.isTopPaddingSet = true;
}
}
return {
isPaddingNeeded,
unsetPadding: isPaddingNeeded ? () => {
if(middleware() && isPaddingNeeded) {
setPaddingTo.style.paddingTop = '';
this.isTopPaddingSet = false;
}
} : undefined
};
}
private renderNewMessage(message: MyMessage, scrolledDown?: boolean) {
const promise = this._renderNewMessage(message, scrolledDown);
this.renderNewPromises.add(promise);
promise.catch(noop).finally(() => {
this.renderNewPromises.delete(promise);
});
return promise;
}
private async _renderNewMessage(message: MyMessage, scrolledDown?: boolean) {
if(!this.scrollable.loadedAll.bottom) { // seems search active or sliced
// this.log('renderNewMessagesByIds: seems search is active, skipping render:', mids);
const setPeerPromise = this.chat.setPeerPromise;
if(setPeerPromise) {
const middleware = this.getMiddleware();
setPeerPromise.then(async() => {
if(!middleware()) return;
const newMessage = await this.chat.getMessage(message.mid);
if(!middleware()) return;
this.renderNewMessage(newMessage);
});
}
return;
}
if(this.chat.threadId && getMessageThreadId(message, this.chat.isForum) !== this.chat.threadId) {
return;
}
if(this.bubbles[message.mid]) {
return;
}
// ! should scroll even without new messages
/* if(!mids.length) {
return;
} */
if(!scrolledDown) {
scrolledDown = this.scrolledDown && (
!this.scrollingToBubble ||
this.scrollingToBubble === this.getLastBubble() ||
this.scrollingToBubble === this.chatInner
);
}
const middleware = this.getMiddleware();
const {isPaddingNeeded, unsetPadding} = this.setTopPadding(middleware);
const promise = this.performHistoryResult({history: [message]}, false);
if(scrolledDown) {
promise.then(() => {
if(!middleware()) return;
// this.log('renderNewMessagesByIDs: messagesQueuePromise after', this.scrollable.isScrolledDown);
// this.scrollable.scrollTo(this.scrollable.scrollHeight, 'top', true, true, 5000);
// const bubble = this.bubbles[Math.max(...mids)];
let bubble: HTMLElement;
if(this.chat.type === 'scheduled') {
bubble = this.bubbles[message.mid];
}
const promise = bubble ? this.scrollToBubbleEnd(bubble) : this.scrollToEnd();
if(isPaddingNeeded) {
// it will be called only once even if was set multiple times (that won't happen)
promise.then(unsetPadding);
}
// this.scrollable.scrollIntoViewNew(this.chatInner, 'end');
/* setTimeout(() => {
this.log('messagesQueuePromise afterafter:', this.chatInner.childElementCount, this.scrollable.scrollHeight);
}, 10); */
});
}
return promise;
}
public getLastBubble() {
const group = this.bubbleGroups.getLastGroup();
return group?.lastItem?.bubble;
}
public scrollToBubble(
element: HTMLElement,
position: ScrollLogicalPosition,
forceDirection?: FocusDirection,
forceDuration?: number
) {
const bubble = findUpClassName(element, 'bubble');
if(!element.parentElement) {
this.log.error('element is not connected', bubble);
}
let fallbackToElementStartWhenCentering: HTMLElement;
// * if it's a start, then scroll to start of the group
if(bubble && position !== 'end') {
const item = this.bubbleGroups.getItemByBubble(bubble);
if(item && item.group.firstItem === item && whichChild(item.group.container) === (this.stickyIntersector ? STICKY_OFFSET : 1)) {
const dateGroup = item.group.container.parentElement;
// if(whichChild(dateGroup) === 0) {
fallbackToElementStartWhenCentering = dateGroup;
// position = 'start';
// element = dateGroup;
// }
}
}
// const isLastBubble = this.getLastBubble() === bubble;
/* if(isLastBubble) {
element = this.getLastDateGroup();
} */
const margin = 4; // * 4 = .25rem
/* if(isLastBubble && this.chat.type === 'chat' && this.bubblesContainer.classList.contains('is-chat-input-hidden')) {
margin = 20;
} */
const isChangingHeight = (this.chat.input.messageInput && this.chat.input.messageInput.classList.contains('is-changing-height')) || this.chat.container.classList.contains('is-toggling-helper');
const promise = this.scrollable.scrollIntoViewNew({
element,
position,
margin,
forceDirection,
forceDuration,
axis: 'y',
getNormalSize: isChangingHeight ? ({rect}) => {
// return rect.height;
let height = windowSize.height;
// height -= this.chat.topbar.container.getBoundingClientRect().height;
height -= this.container.offsetTop;
height -= mediaSizes.isMobile || windowSize.height < 570 ? 58 : 78;
return height;
/* const rowsWrapperHeight = this.chat.input.rowsWrapper.getBoundingClientRect().height;
const diff = rowsWrapperHeight - 54;
return rect.height + diff; */
} : undefined,
fallbackToElementStartWhenCentering,
startCallback: (dimensions) => {
// this.onScroll(true, this.scrolledDown && dimensions.distanceToEnd <= SCROLLED_DOWN_THRESHOLD ? undefined : dimensions);
this.onScroll(true, dimensions);
if(this.updateGradient) {
const {gradientRenderer} = this.chat;
gradientRenderer?.toNextPosition(dimensions.getProgress);
this.updateGradient = undefined;
}
}
});
// fix flickering date when opening unread chat and focusing message
if(forceDirection === FocusDirection.Static) {
this.scrollable.lastScrollPosition = this.scrollable.scrollTop;
}
return promise;
}
public scrollToEnd() {
return this.scrollToBubbleEnd(this.chatInner);
}
public async scrollToBubbleEnd(bubble: HTMLElement) {
/* if(DEBUG) {
this.log('scrollToNewLastBubble: will scroll into view:', bubble);
} */
if(bubble) {
this.scrollingToBubble = bubble;
const middleware = this.getMiddleware();
await this.scrollToBubble(bubble, 'end', undefined, undefined);
if(!middleware()) return;
this.scrollingToBubble = undefined;
}
}
// ! can't get it by chatInner.lastElementChild because placeholder can be the last...
// private getLastDateGroup() {
// let lastTime = 0, lastElem: HTMLElement;
// for(const i in this.dateMessages) {
// const dateMessage = this.dateMessages[i];
// if(dateMessage.firstTimestamp > lastTime) {
// lastElem = dateMessage.container;
// lastTime = dateMessage.firstTimestamp;
// }
// }
// return lastElem;
// }
public async scrollToBubbleIfLast(bubble: HTMLElement) {
if(this.getLastBubble() === bubble) {
// return this.scrollToBubbleEnd(bubble);
return this.scrollToEnd();
}
}
public highlightBubble(element: HTMLElement) {
const datasetKey = 'highlightTimeout';
if(element.dataset[datasetKey]) {
clearTimeout(+element.dataset[datasetKey]);
element.classList.remove('is-highlighted');
void element.offsetWidth; // reflow
}
element.classList.add('is-highlighted');
element.dataset[datasetKey] = '' + setTimeout(() => {
element.classList.remove('is-highlighted');
delete element.dataset[datasetKey];
}, 2000);
}
private createDateBubble(timestamp: number, date: Date = new Date(timestamp * 1000)) {
let dateElement: HTMLElement;
const today = new Date();
today.setHours(0, 0, 0, 0);
const isScheduled = this.chat.type === 'scheduled';
if(today.getTime() === date.getTime()) {
dateElement = i18n(isScheduled ? 'Chat.Date.ScheduledForToday' : 'Date.Today');
} else if(isScheduled && timestamp === SEND_WHEN_ONLINE_TIMESTAMP) {
dateElement = i18n('MessageScheduledUntilOnline');
} else {
const options: Intl.DateTimeFormatOptions = {
day: 'numeric',
month: 'long'
};
if(date.getFullYear() !== today.getFullYear()) {
options.year = 'numeric';
}
dateElement = new I18n.IntlDateElement({
date,
options
}).element;
if(isScheduled) {
dateElement = i18n('Chat.Date.ScheduledFor', [dateElement]);
}
}
const bubble = document.createElement('div');
bubble.className = 'bubble service is-date';
const bubbleContent = document.createElement('div');
bubbleContent.classList.add('bubble-content');
const serviceMsg = document.createElement('div');
serviceMsg.classList.add('service-msg');
serviceMsg.append(dateElement);
bubbleContent.append(serviceMsg);
bubble.append(bubbleContent);
return bubble;
}
public getDateForDateContainer(timestamp: number) {
const date = new Date(timestamp * 1000);
date.setHours(0, 0, 0);
return {date, dateTimestamp: date.getTime()};
}
public getDateContainerByTimestamp(timestamp: number) {
const {date, dateTimestamp} = this.getDateForDateContainer(timestamp);
let ret = this.dateMessages[dateTimestamp];
if(ret) {
return ret;
}
const bubble = this.createDateBubble(timestamp, date);
// bubble.classList.add('is-sticky');
const fakeBubble = this.createDateBubble(timestamp, date);
fakeBubble.classList.add('is-fake');
const container = document.createElement('section');
container.className = 'bubbles-date-group';
container.append(bubble, fakeBubble);
ret = this.dateMessages[dateTimestamp] = {
div: bubble,
container,
firstTimestamp: date.getTime()
};
const haveTimestamps = getObjectKeysAndSort(this.dateMessages, 'asc');
const length = haveTimestamps.length;
let i = 0, insertBefore: HTMLElement; // there can be 'first bubble' (e.g. bot description) so can't insert by index
for(; i < haveTimestamps.length; ++i) {
const t = haveTimestamps[i];
insertBefore = this.dateMessages[t].container;
if(dateTimestamp < t) {
break;
}
}
if(i === length && insertBefore) {
insertBefore = insertBefore.nextElementSibling as HTMLElement;
}
if(!insertBefore) {
this.chatInner.append(container);
} else {
this.chatInner.insertBefore(container, insertBefore);
}
this.stickyIntersector?.observeStickyHeaderChanges(container);
if(this.chatInner.parentElement) {
this.container.classList.add('has-groups');
}
return ret;
}
private destroyScrollable() {
this.scrollable.destroy();
}
public destroy() {
// this.chat.log.error('Bubbles destroying');
this.destroyScrollable();
this.listenerSetter.removeAll();
this.lazyLoadQueue.clear();
this.observer && this.observer.disconnect();
this.stickyIntersector && this.stickyIntersector.disconnect();
delete this.lazyLoadQueue;
this.observer && delete this.observer;
this.stickyIntersector && delete this.stickyIntersector;
}
public cleanup(bubblesToo = false) {
this.log('cleanup');
this.bubbles = {}; // clean it before so sponsored message won't be deleted faster on peer changing
// //console.time('appImManager cleanup');
this.setLoaded('top', false, false);
this.setLoaded('bottom', false, false);
// cancel scroll
cancelAnimationByKey(this.scrollable.container);
// do not wait ending of previous scale animation
interruptHeavyAnimation();
if(TEST_SCROLL !== undefined) {
TEST_SCROLL = TEST_SCROLL_TIMES;
}
this.skippedMids.clear();
this.dateMessages = {};
this.bubbleGroups.cleanup();
this.unreadOut.clear();
this.needUpdate.length = 0;
this.lazyLoadQueue.clear();
this.renderNewPromises.clear();
// clear messages
if(bubblesToo) {
this.scrollable.container.textContent = '';
this.chatInner.textContent = '';
this.cleanupPlaceholders();
}
this.firstUnreadBubble = null;
this.attachedUnreadBubble = false;
this.batchProcessor.clear();
this.getHistoryTopPromise = this.getHistoryBottomPromise = undefined;
this.fetchNewPromise = undefined;
this.getSponsoredMessagePromise = undefined;
this.updateGradient = undefined;
if(this.stickyIntersector) {
this.stickyIntersector.disconnect();
}
if(this.observer) {
this.observer.disconnect();
this.unreaded.clear();
this.unreadedSeen.clear();
this.readPromise = undefined;
this.viewsMids.clear();
}
this.middlewareHelper.clean();
this.onAnimateLadder = undefined;
this.resolveLadderAnimation = undefined;
this.attachPlaceholderOnRender = undefined;
this.emptyPlaceholderBubble = undefined;
this.sponsoredMessage = undefined;
this.previousStickyDate = undefined;
this.scrollingToBubble = undefined;
// //console.timeEnd('appImManager cleanup');
this.isTopPaddingSet = false;
this.renderingMessages.clear();
this.bubblesToEject.clear();
this.bubblesToReplace.clear();
// this.reactions.clear();
if(this.isScrollingTimeout) {
clearTimeout(this.isScrollingTimeout);
this.isScrollingTimeout = 0;
}
this.container.classList.remove('has-sticky-dates');
this.scrollable.cancelMeasure();
}
private cleanupPlaceholders(bubble = this.emptyPlaceholderBubble) {
if(bubble) {
this.destroyBubble(bubble);
}
}
public async setPeer(options: ChatSetPeerOptions & {samePeer: boolean}): Promise<{cached?: boolean, promise: Chat['setPeerPromise']}> {
const {samePeer, peerId, stack} = options;
let {lastMsgId, startParam} = options;
const tempId = ++this.setPeerTempId;
if(!peerId) {
this.cleanup(true);
this.preloader.detach();
return null;
}
const perf = performance.now();
const log = this.log.bindPrefix('setPeer');
log.warn('start');
const middleware = () => {
return this.setPeerTempId === tempId;
};
const m = middlewarePromise(middleware, PEER_CHANGED_ERROR);
if(!samePeer) {
await m(this.chat.onChangePeer(options, m));
}
/* if(samePeer && this.chat.setPeerPromise) {
return {cached: true, promise: this.chat.setPeerPromise};
} */
const chatType = this.chat.type;
if(chatType === 'scheduled' || this.chat.isRestricted) {
lastMsgId = 0;
}
const historyStorage = await m(this.chat.getHistoryStorage());
const topMessage = chatType === 'pinned' ? await m(this.managers.appMessagesManager.getPinnedMessagesMaxId(peerId, this.chat.threadId)) : historyStorage.maxId ?? 0;
const isTarget = lastMsgId !== undefined;
// * this one will fix topMessage for null message in history (e.g. channel comments with only 1 comment and it is a topMessage)
/* if(chatType !== 'pinned' && topMessage && !historyStorage.history.slice.includes(topMessage)) {
topMessage = 0;
} */
let followingUnread: boolean;
let readMaxId = 0, savedPosition: ReturnType<AppImManager['getChatSavedPosition']>, overrideAdditionMsgId: number;
if(!isTarget) {
if(!samePeer) {
savedPosition = this.chat.appImManager.getChatSavedPosition(this.chat);
}
if(savedPosition) {
} else if(topMessage) {
readMaxId = await m(this.managers.appMessagesManager.getReadMaxIdIfUnread(peerId, this.chat.threadId));
const dialog = await m(this.chat.getDialogOrTopic());
if(/* dialog.unread_count */readMaxId && !samePeer && (!dialog || dialog.unread_count !== 1)) {
const foundSlice = historyStorage.history.findSliceOffset(readMaxId);
if(foundSlice && foundSlice.slice.isEnd(SliceEnd.Bottom)) {
overrideAdditionMsgId = foundSlice.slice[foundSlice.offset - 25] || foundSlice.slice[0] || readMaxId;
}
followingUnread = !isTarget;
lastMsgId = readMaxId;
} else {
lastMsgId = topMessage;
// lastMsgID = topMessage;
}
}
}
const isJump = lastMsgId !== topMessage/* && overrideAdditionMsgId === undefined */;
if(startParam === undefined && await m(this.chat.isStartButtonNeeded())) {
startParam = BOT_START_PARAM;
}
if(samePeer) {
if(stack && lastMsgId) {
this.followStack.push(stack);
}
const mounted = await m(this.getMountedBubble(lastMsgId));
if(mounted) {
if(isTarget) {
this.scrollToBubble(mounted.bubble, 'center');
this.highlightBubble(mounted.bubble);
this.chat.dispatchEvent('setPeer', lastMsgId, false);
} else if(topMessage && !isJump) {
// log('will scroll down', this.scroll.scrollTop, this.scroll.scrollHeight);
// scrollable.setScrollTopSilently(scrollable.scrollHeight);
this.scrollToEnd();
this.chat.dispatchEvent('setPeer', lastMsgId, true);
}
if(startParam !== undefined) {
this.chat.input.setStartParam(startParam);
}
if(options.mediaTimestamp) {
getHeavyAnimationPromise().then(() => {
this.playMediaWithTimestampAndMid({
lastMsgId,
middleware,
mediaTimestamp: options.mediaTimestamp
});
});
}
return null;
}
} else {
if(this.peerId) { // * set new queue id if new peer (setting not from 0)
this.lazyLoadQueue.queueId = ++queueId;
this.managers.apiFileManager.setQueueId(this.chat.bubbles.lazyLoadQueue.queueId);
}
this.followStack.length = 0;
this.passEntities = {
messageEntityBotCommand: await m(this.managers.appPeersManager.isAnyGroup(peerId)) || this.chat.isBot
};
}
if(DEBUG) {
log('setPeer peerId:', peerId, historyStorage, lastMsgId, topMessage);
}
// add last message, bc in getHistory will load < max_id
const additionMsgId = overrideAdditionMsgId ?? (isJump || chatType === 'scheduled' || this.chat.isRestricted ? 0 : topMessage);
let maxBubbleId = 0;
if(samePeer) {
const el = this.getBubbleByPoint('bottom'); // ! this may not work if being called when chat is hidden
// this.chat.log('[PM]: setCorrectIndex: get last element perf:', performance.now() - perf, el);
if(el) {
maxBubbleId = +el.dataset.mid;
}
if(maxBubbleId <= 0) {
maxBubbleId = Math.max(...Object.keys(this.bubbles).map((mid) => +mid));
}
} else {
this.isFirstLoad = true;
this.destroyResizeObserver();
}
const oldChatInner = this.chatInner;
const oldPlaceholderBubble = this.emptyPlaceholderBubble;
this.cleanup();
const chatInner = this.chatInner = document.createElement('div');
if(samePeer) {
chatInner.className = oldChatInner.className;
chatInner.classList.remove('disable-hover', 'is-scrolling');
} else {
chatInner.classList.add('bubbles-inner');
}
this.lazyLoadQueue.lock();
// const haveToScrollToBubble = (topMessage && (isJump || samePeer)) || isTarget;
const haveToScrollToBubble = samePeer || (topMessage && isJump) || isTarget;
const fromUp = maxBubbleId > 0 && (!lastMsgId || maxBubbleId < lastMsgId || lastMsgId < 0);
const scrollFromDown = !fromUp && samePeer;
const scrollFromUp = !scrollFromDown && fromUp/* && (samePeer || forwardingUnread) */;
this.willScrollOnLoad = scrollFromDown || scrollFromUp;
this.setPeerOptions = {
lastMsgId,
topMessage,
savedPosition
};
if(!samePeer) {
this.ranks = undefined;
this.processRanks = undefined;
this.canShowRanks = false;
if(this.chat.isChannel) {
this.canShowRanks = true;
const processRanks = this.processRanks = new Set();
const promise = this.managers.acknowledged.appProfileManager.getParticipants(this.peerId.toChatId(), {_: 'channelParticipantsAdmins'}, 100);
const ackedResult = await m(promise);
const setRanksPromise = ackedResult.result.then((channelParticipants) => {
if(this.processRanks !== processRanks) {
return;
}
const participants = channelParticipants.participants as (ChatParticipant.chatParticipantAdmin | ChannelParticipant.channelParticipantAdmin)[];
this.ranks = new Map();
participants.forEach((participant) => {
const rank = getParticipantRank(participant);
this.ranks.set(participant.user_id.toPeerId(), rank);
});
getHeavyAnimationPromise().then(() => {
if(this.processRanks !== processRanks) {
return;
}
processRanks.forEach((callback) => callback());
this.processRanks = undefined;
});
}, (err) => {
if((err as ApiError).type !== 'CHAT_ADMIN_REQUIRED') {
this.log.error('ranks error', err);
}
this.ranks = new Map();
});
if(ackedResult.cached) {
await m(setRanksPromise);
}
}
}
let result: Awaited<ReturnType<ChatBubbles['getHistory']>>;
if(!savedPosition) {
result = await m(this.getHistory1(lastMsgId, true, isJump, additionMsgId));
} else {
result = {
promise: getHeavyAnimationPromise().then(() => {
return this.performHistoryResult({history: savedPosition.mids}, true);
}) as any,
cached: true,
waitPromise: Promise.resolve()
};
}
this.setPeerCached = result.cached;
log.warn('got history');// warning
const {promise, cached} = result;
const finishPeerChangeOptions: Parameters<Chat['finishPeerChange']>[0] = {
isTarget,
isJump,
lastMsgId,
startParam,
middleware
};
if(!cached && !samePeer) {
await m(this.chat.finishPeerChange(finishPeerChangeOptions));
this.scrollable.container.replaceChildren();
// oldContainer.textContent = '';
// oldChatInner.remove();
this.preloader.attach(this.container);
}
/* this.ladderDeferred && this.ladderDeferred.resolve();
this.ladderDeferred = deferredPromise<void>(); */
animationIntersector.lockGroup(this.chat.animationGroup);
const setPeerPromise = m(promise).then(async() => {
log.warn('promise fulfilled');
const mountedByLastMsgId = haveToScrollToBubble ? await m(lastMsgId ? this.getMountedBubble(lastMsgId) : {bubble: this.getLastBubble()}) : undefined;
if(cached && !samePeer) {
log.warn('finishing peer change');
await m(this.chat.finishPeerChange(finishPeerChangeOptions)); // * костыль
log.warn('finished peer change');
}
this.preloader.detach();
if(this.resolveLadderAnimation) {
this.resolveLadderAnimation();
this.resolveLadderAnimation = undefined;
}
this.setPeerCached = undefined;
// this.ladderDeferred.resolve();
const scrollable = this.scrollable;
scrollable.lastScrollDirection = 0;
scrollable.lastScrollPosition = 0;
replaceContent(scrollable.container, chatInner);
// this.chat.topbar.container.nextElementSibling.replaceWith(container);
if(oldPlaceholderBubble) {
this.cleanupPlaceholders(oldPlaceholderBubble);
}
if(this.attachPlaceholderOnRender) {
this.attachPlaceholderOnRender();
}
if(!isTarget && this.chat.isPinnedMessagesNeeded()) {
this.chat.topbar.pinnedMessage?.setCorrectIndex(0);
}
this.container.classList.toggle('has-groups', !!Object.keys(this.dateMessages).length);
log.warn('mounted chat', this.chatInner === chatInner, this.chatInner.parentElement, performance.now() - perf);
animationIntersector.unlockGroup(this.chat.animationGroup);
animationIntersector.checkAnimations(false, this.chat.animationGroup/* , true */);
// fastRaf(() => {
this.lazyLoadQueue.unlock();
// });
// if(dialog && lastMsgID && lastMsgID !== topMessage && (this.bubbles[lastMsgID] || this.firstUnreadBubble)) {
if(savedPosition) {
scrollable.setScrollTopSilently(savedPosition.top);
/* const mountedByLastMsgId = this.getMountedBubble(lastMsgId);
let bubble: HTMLElement = mountedByLastMsgId?.bubble;
if(!bubble?.parentElement) {
bubble = this.findNextMountedBubbleByMsgId(lastMsgId);
}
if(bubble) {
const top = bubble.getBoundingClientRect().top;
const distance = savedPosition.top - top;
scrollable.scrollTop += distance;
} */
} else if(haveToScrollToBubble) {
let unsetPadding: () => void;
if(scrollFromDown) {
scrollable.setScrollTopSilently(99999);
} else if(scrollFromUp) {
const set = this.setTopPadding();
if(set.isPaddingNeeded) {
unsetPadding = set.unsetPadding;
}
scrollable.setScrollTopSilently(0);
}
// const mountedByLastMsgId = lastMsgId ? this.getMountedBubble(lastMsgId) : {bubble: this.getLastBubble()};
let bubble: HTMLElement = (followingUnread && this.firstUnreadBubble) || mountedByLastMsgId?.bubble;
if(!bubble?.parentElement) {
bubble = this.findNextMountedBubbleByMsgId(lastMsgId, false) || this.findNextMountedBubbleByMsgId(lastMsgId, true);
}
let promise: Promise<void>;
// ! sometimes there can be no bubble
if(bubble) {
const lastBubble = this.getLastBubble();
const position: ScrollLogicalPosition = followingUnread ? 'start' : (!isJump && !isTarget && lastBubble === bubble ? 'end' : 'center');
if(position === 'end' && lastBubble === bubble && samePeer) {
promise = this.scrollToEnd();
} else {
promise = this.scrollToBubble(bubble, position, !samePeer ? FocusDirection.Static : undefined);
}
if(!followingUnread && isTarget) {
this.highlightBubble(bubble);
}
}
if(unsetPadding) {
(promise || Promise.resolve()).then(() => {
unsetPadding();
});
}
} else {
scrollable.setScrollTopSilently(99999);
}
// if(!cached) {
this.onRenderScrollSet();
// }
this.onScroll();
const afterSetPromise = Promise.all([
setPeerPromise,
getHeavyAnimationPromise()
]);
afterSetPromise.then(() => { // check whether list isn't full
if(!middleware()) {
return;
}
scrollable.checkForTriggers();
if(options.mediaTimestamp !== undefined) {
// ! :(
const p = cached && !samePeer && liteMode.isAvailable('animations') && this.chat.appImManager.chats.length > 1 ?
pause(400) :
Promise.resolve();
p.then(() => {
return this.playMediaWithTimestampAndMid({
lastMsgId,
middleware,
mediaTimestamp: options.mediaTimestamp
});
});
}
// if(cached) {
// this.onRenderScrollSet();
// }
});
this.chat.dispatchEvent('setPeer', lastMsgId, !isJump);
Promise.all([
this.setFetchReactionsInterval(afterSetPromise),
this.setFetchHistoryInterval({
afterSetPromise,
lastMsgId,
samePeer,
savedPosition,
topMessage
})
]).then(() => {
log('scrolledAllDown:', scrollable.loadedAll.bottom);
// if(!this.unreaded.length && dialog) { // lol
if(scrollable.loadedAll.bottom && topMessage && !this.unreaded.size) { // lol
this.onScrolledAllDown();
}
});
if(chatType === 'chat' && !this.chat.isForumTopic) {
const dialog = await m(this.managers.appMessagesManager.getDialogOnly(peerId));
if(dialog?.pFlags.unread_mark) {
this.managers.appMessagesManager.markDialogUnread(peerId, true);
}
}
// this.chatInner.classList.remove('disable-hover', 'is-scrolling'); // warning, performance!
}).catch((err) => {
log.error('setPeer promise error:', err);
if(!middleware()) {
this.preloader.detach();
}
throw err;
});
return {cached, promise: setPeerPromise};
}
public playMediaWithTimestampAndMid({
middleware,
lastMsgId,
mediaTimestamp
}: {
middleware: () => boolean,
lastMsgId: number,
mediaTimestamp: number
}) {
this.getMountedBubble(lastMsgId).then((mounted) => {
if(!middleware() || !mounted) {
return;
}
this.playMediaWithTimestamp(mounted.bubble, mediaTimestamp);
});
}
public playMediaWithTimestamp(element: HTMLElement, timestamp: number) {
const bubble = findUpClassName(element, 'bubble');
const groupedItem = findUpClassName(element, 'grouped-item');
const albumItemMid = groupedItem ? +groupedItem.dataset.mid : +bubble.dataset.textMid;
let attachment = bubble.querySelector<HTMLElement>('.attachment');
if(attachment) {
if(albumItemMid) {
attachment = attachment.querySelector(`[data-mid="${albumItemMid}"]`);
}
const media = attachment.querySelector<HTMLElement>('img, video, canvas');
this.checkTargetForMediaViewer(media, undefined, timestamp);
return;
}
const audio = (groupedItem || bubble).querySelector<AudioElement>('.audio');
if(audio) {
audio.playWithTimestamp(timestamp);
return;
}
const replyToPeerId = bubble.dataset.replyToPeerId.toPeerId();
const replyToMid = +bubble.dataset.replyToMid;
if(replyToPeerId && replyToMid) {
if(replyToPeerId === this.peerId) {
this.chat.setMessageId(replyToMid, timestamp);
} else {
this.chat.appImManager.setInnerPeer({
peerId: replyToPeerId,
mediaTimestamp: timestamp
});
}
}
}
private async setFetchReactionsInterval(afterSetPromise: Promise<any>) {
const middleware = this.getMiddleware();
const needReactionsInterval = this.chat.isChannel;
if(needReactionsInterval) {
const fetchReactions = async() => {
if(!middleware()) return;
const mids: number[] = [];
for(const mid in this.bubbles) {
let message = await this.chat.getMessage(+mid);
if(message?._ !== 'message') {
continue;
}
message = await this.managers.appMessagesManager.getGroupsFirstMessage(message);
mids.push(message.mid);
}
const promise = mids.length ? this.managers.appReactionsManager.getMessagesReactions(this.peerId, mids) : Promise.resolve();
promise.then(() => {
setTimeout(fetchReactions, 10e3);
});
};
Promise.all([afterSetPromise, getHeavyAnimationPromise(), pause(500)]).then(() => {
fetchReactions();
});
}
}
private async setFetchHistoryInterval({
lastMsgId,
topMessage,
afterSetPromise,
savedPosition,
samePeer
}: {
lastMsgId: number,
topMessage: number,
afterSetPromise: Promise<any>,
savedPosition: ChatSavedPosition,
samePeer: boolean
}) {
const peerId = this.peerId;
if(peerId.isUser()) {
return;
}
const middleware = this.getMiddleware();
const needFetchInterval = await this.managers.appMessagesManager.isFetchIntervalNeeded(peerId);
const needFetchNew = savedPosition || needFetchInterval;
if(!needFetchNew) {
return;
}
await afterSetPromise;
if(!middleware()) {
return;
}
const chatId = peerId.toChatId();
middleware.onClean(() => {
this.managers.apiUpdatesManager.unsubscribeFromChannelUpdates(chatId);
});
this.managers.apiUpdatesManager.subscribeToChannelUpdates(chatId);
return;
this.setLoaded('bottom', false);
this.scrollable.checkForTriggers();
if(!needFetchInterval) {
return;
}
const f = () => {
this.fetchNewPromise = new Promise<void>(async(resolve) => {
if(!middleware() || !(await this.managers.appMessagesManager.isFetchIntervalNeeded(peerId))) {
resolve();
return;
}
this.managers.appMessagesManager.getNewHistory(peerId, this.chat.threadId).then((result) => {
if(!middleware() || !result) {
resolve();
return;
}
const {isBottomEnd} = result;
if(this.scrollable.loadedAll.bottom && this.scrollable.loadedAll.bottom !== isBottomEnd) {
this.setLoaded('bottom', isBottomEnd);
this.onScroll();
}
setTimeout(f, 30e3);
resolve();
});
}).finally(() => {
this.fetchNewPromise = undefined;
});
};
if(samePeer) {
setTimeout(f, 30e3);
} else {
f();
}
}
public async onScrolledAllDown() {
if(this.chat.type === 'chat' || this.chat.type === 'discussion') {
const historyMaxId = await this.chat.getHistoryMaxId();
this.managers.appMessagesManager.readHistory(this.peerId, historyMaxId, this.chat.threadId, true);
}
}
public async finishPeerChange() {
const [isChannel, canWrite, isAnyGroup] = await Promise.all([
this.chat.isChannel,
this.chat.canSend(),
this.chat.isAnyGroup
]);
return () => {
this.chatInner.classList.toggle('has-rights', canWrite);
this.container.classList.toggle('is-chat-input-hidden', !canWrite);
this.chatInner.classList.toggle('is-chat', isAnyGroup);
this.chatInner.classList.toggle('is-channel', isChannel);
this.createResizeObserver();
};
}
private processBatch = async(...args: Parameters<ChatBubbles['batchProcessor']['process']>) => {
let [loadQueue, m, log] = args;
const filterQueue = (queue: typeof loadQueue) => {
return queue.filter((details) => {
// message can be deleted during rendering
return details && this.bubbles[details.bubble.dataset.mid] === details.bubble;
});
};
loadQueue = filterQueue(loadQueue);
log('messages rendered');
const reverse = loadQueue[0]?.reverse;
const {groups, avatarPromises} = this.groupBubbles(loadQueue.filter((details) => details.updatePosition));
// if(groups.length > 2 && loadQueue.length === 1) {
// debugger;
// }
const promises = loadQueue.reduce((acc, details) => {
const perf = performance.now();
const promises = details.promises.slice();
const timePromises = promises.map(async(promise) => (await promise, performance.now() - perf));
Promise.all(timePromises).then((times) => {
log.groupCollapsed('media message time', performance.now() - perf, details, times);
times.forEach((time, idx) => {
log('media message time', time, idx, promises[idx]);
});
log.groupEnd();
});
// if(details.updatePosition) {
// if(res) {
// groups.add(res.group);
// if(details.needAvatar) {
// details.promises.push(res.group.createAvatar(details.message));
// }
// }
// }
acc.push(...details.promises);
return acc;
}, [] as Promise<any>[]);
promises.push(...avatarPromises);
// promises.push(pause(200));
// * это нужно для того, чтобы если захочет подгрузить reply или какое-либо сообщение, то скролл не прервался
// * если добавить этот промис - в таком случае нужно сделать, чтобы скроллило к последнему сообщению после рендера
// promises.push(getHeavyAnimationPromise());
log('media promises to call', promises, loadQueue, this.isHeavyAnimationInProgress);
await m(Promise.all([...promises, this.setUnreadDelimiter()]).catch(noop)); // не нашёл места лучше
await m(fastRafPromise()); // have to be the last
log('media promises end');
loadQueue = filterQueue(loadQueue);
const {restoreScroll, scrollSaver} = this.prepareToSaveScroll(reverse);
// if(this.messagesQueueOnRender) {
// this.messagesQueueOnRender();
// }
this.messagesQueueOnRenderAdditional?.();
this.ejectBubbles();
for(const [bubble, oldBubble] of this.bubblesToReplace) {
if(scrollSaver) {
scrollSaver.replaceSaved(oldBubble, bubble);
}
if(!loadQueue.find((details) => details.bubble === bubble)) {
continue;
}
const item = this.bubbleGroups.getItemByBubble(bubble);
if(!item) {
this.log.error('NO ITEM BY BUBBLE', bubble);
} else {
item.mounted = false;
if(!groups.includes(item.group)) {
groups.push(item.group);
}
}
this.bubblesToReplace.delete(bubble);
}
if(this.chat.selection.isSelecting) {
loadQueue.forEach(({bubble}) => {
this.chat.selection.toggleElementCheckbox(bubble, true);
});
}
loadQueue.forEach(({message, bubble, updatePosition}) => {
if(message.pFlags.local && updatePosition) {
this.chatInner[(message as Message.message).pFlags.sponsored ? 'append' : 'prepend'](bubble);
return;
}
});
this.bubbleGroups.mountUnmountGroups(groups);
// this.bubbleGroups.findIncorrentPositions();
this.updatePlaceholderPosition?.();
restoreScroll?.();
m(pause(!this.chat.setPeerPromise ? 0 : 1000))
.then(() => m(getHeavyAnimationPromise()))
.then(() => {
this.lazyLoadQueue.setAllSeen();
}).catch(noop);
// this.setStickyDateManually();
};
public renderMessagesQueue(options: ReturnType<ChatBubbles['safeRenderMessage']>) {
return this.batchProcessor.addToQueue(options);
}
private ejectBubbles() {
for(const bubble of this.bubblesToEject) {
bubble.remove();
// this.bubbleGroups.removeAndUnmountBubble(bubble);
}
this.bubblesToEject.clear();
}
public groupBubbles(items: Array<{
// Awaited<ReturnType<ChatBubbles['safeRenderMessage']>> &
bubble: HTMLElement,
message: Message.message | Message.messageService
}/* & {
unmountIfFound?: boolean
} */>) {
let modifiedGroups: typeof groups;
if(this.chat.type === 'scheduled') {
modifiedGroups = new Set();
items.forEach(({bubble, message}) => {
const item = this.bubbleGroups.getItemByBubble(bubble);
const group = item?.group;
if(group && item.message.date !== message.date) {
this.bubbleGroups.removeItem(item);
modifiedGroups.add(group);
}
});
}
items.forEach(({bubble, message}) => {
this.bubbleGroups.prepareForGrouping(bubble, message);
});
const groups = this.bubbleGroups.groupUngrouped();
const avatarPromises = Array.from(groups).map((group) => {
if(group.avatar) return;
const firstItem = group.firstItem;
if(firstItem && this.chat.isAvatarNeeded(firstItem.message)) {
return group.createAvatar(firstItem.message);
}
}).filter(Boolean);
if(modifiedGroups) {
for(const group of modifiedGroups) {
groups.add(group);
}
}
return {
groups: [...groups],
avatarPromises
};
}
public getMiddleware(additionalCallback?: () => boolean) {
return this.middlewareHelper.get(additionalCallback);
}
private async wrapMediaSpoiler({
media,
promise,
middleware,
attachmentDiv
}: {
media: Photo.photo | MyDocument,
promise: Promise<any>,
middleware: Middleware,
attachmentDiv: HTMLElement
}) {
await promise;
if(!middleware()) {
return;
}
const {width, height} = attachmentDiv.style;
const container = await wrapMediaSpoiler({
media,
width: parseInt(width),
height: parseInt(height),
middleware,
animationGroup: this.chat.animationGroup
});
if(!middleware()) {
return;
}
attachmentDiv.append(container);
}
private async safeRenderMessage(
message: Message.message | Message.messageService,
reverse?: boolean,
bubble?: HTMLElement,
updatePosition = true,
processResult?: (result: ReturnType<ChatBubbles['renderMessage']>, bubble: HTMLElement) => typeof result
) {
if(!message || this.renderingMessages.has(message.mid) || (this.bubbles[message.mid] && !bubble)) {
return;
}
const middlewareHelper = this.getMiddleware().create();
const middleware = middlewareHelper.get();
let result: Awaited<ReturnType<ChatBubbles['renderMessage']>> & {updatePosition: typeof updatePosition};
try {
this.renderingMessages.add(message.mid);
// const groupedId = (message as Message.message).grouped_id;
const newBubble = document.createElement('div');
newBubble.middlewareHelper = middlewareHelper;
newBubble.dataset.mid = '' + message.mid;
newBubble.dataset.peerId = '' + message.peerId;
newBubble.dataset.timestamp = '' + message.date;
// const bubbleNew: Bubble = this.bubblesNew[message.mid] ??= {
// bubble: newBubble,
// mids: new Set(),
// groupedId
// };
// bubbleNew.mids.add(message.mid);
if(bubble) {
bubble.middlewareHelper.destroy();
this.skippedMids.delete(message.mid);
this.bubblesToEject.add(bubble);
this.bubblesToReplace.delete(bubble);
this.bubblesToReplace.set(newBubble, bubble);
this.bubbleGroups.changeBubbleByBubble(bubble, newBubble);
}
bubble = this.bubbles[message.mid] = newBubble;
let originalPromise = this.renderMessage(message, reverse, bubble, middleware);
if(processResult) {
originalPromise = processResult(originalPromise, bubble);
}
const promise = originalPromise.then((r) => ((r && middleware() ? {...r, updatePosition} : undefined) as typeof result));
this.renderMessagesQueue(promise.catch(() => undefined));
result = await promise;
if(!middleware()) {
return;
}
if(!result) {
this.skippedMids.add(+message.mid);
}
} catch(err) {
this.log.error('renderMessage error:', err);
}
if(!middleware()) {
return;
}
this.renderingMessages.delete(message.mid);
return result;
}
// reverse means top
private async renderMessage(
message: Message.message | Message.messageService,
reverse = false,
bubble: HTMLElement,
middleware: Middleware
) {
// if(DEBUG) {
// this.log('message to render:', message);
// }
// if(!bubble && this.bubbles[message.mid]) {
// return;
// }
// await pause(1000);
const isMessage = message._ === 'message';
const groupedId = isMessage && message.grouped_id;
let albumMids: number[], reactionsMessage: Message.message;
const albumMessages = groupedId ? await this.managers.appMessagesManager.getMessagesByAlbum(groupedId) : undefined;
const albumMustBeRenderedFull = this.chat.type !== 'pinned';
if(groupedId && albumMustBeRenderedFull) { // will render only last album's message
albumMids = albumMessages.map((message) => message.mid);
const mainMid = getMainMidForGrouped(albumMids);
if(message.mid !== mainMid) {
return;
}
}
if(isMessage) {
reactionsMessage = groupedId ? albumMessages[0] : message;
}
// * can't use 'message.pFlags.out' here because this check will be used to define side of message (left-right)
const our = this.chat.isOurMessage(message);
const messageDiv = document.createElement('div');
messageDiv.classList.add('message', 'spoilers-container');
const contentWrapper = document.createElement('div');
contentWrapper.classList.add('bubble-content-wrapper');
const bubbleContainer = document.createElement('div');
bubbleContainer.classList.add('bubble-content');
bubble.classList.add('bubble');
contentWrapper.append(bubbleContainer);
bubble.append(contentWrapper);
let isInUnread = !our &&
!message.pFlags.out &&
(
message.pFlags.unread ||
isMentionUnread(message)
);
if(!isInUnread && this.chat.peerId.isAnyChat()) {
const readMaxId = await this.managers.appMessagesManager.getReadMaxIdIfUnread(this.chat.peerId, this.chat.threadId);
if(readMaxId !== undefined && readMaxId < message.mid) {
isInUnread = true;
}
}
if(isInUnread && this.observer) {
// this.log('not our message', message, message.pFlags.unread);
this.observer.observe(bubble, this.unreadedObserverCallback);
this.unreaded.set(bubble, message.mid);
}
const loadPromises: Promise<any>[] = [];
const ret = {
bubble,
promises: loadPromises,
message,
reverse
};
const wrapOptions: WrapSomethingOptions = {
lazyLoadQueue: this.lazyLoadQueue,
middleware,
customEmojiSize: this.chat.appImManager.customEmojiSize,
animationGroup: this.chat.animationGroup
};
if(message._ === 'messageService' && (!message.action || !SERVICE_AS_REGULAR.has(message.action._))) {
const action = message.action;
if(action) {
const _ = action._;
const ignoreAction = IGNORE_ACTIONS.get(_);
if(ignoreAction && (ignoreAction === true || ignoreAction(message))) {
return;
}
if(langPack.hasOwnProperty(_) && !langPack[_]) {
return;
}
}
bubble.className = 'bubble service';
bubbleContainer.replaceChildren();
const s = document.createElement('div');
s.classList.add('service-msg');
if(action) {
let promise: Promise<any>;
if(action._ === 'messageActionChannelMigrateFrom') {
const peerTitle = new PeerTitle();
promise = peerTitle.update({peerId: action.chat_id.toPeerId(true), wrapOptions});
s.append(i18n('ChatMigration.From', [peerTitle.element]));
} else if(action._ === 'messageActionChatMigrateTo') {
const peerTitle = new PeerTitle();
promise = peerTitle.update({peerId: action.channel_id.toPeerId(true), wrapOptions});
s.append(i18n('ChatMigration.To', [peerTitle.element]));
} else {
promise = wrapMessageActionTextNew({
message,
...wrapOptions
}).then((el) => s.append(el));
}
if(action._ === 'messageActionGiftPremium') {
const content = bubbleContainer.cloneNode(false) as HTMLElement;
content.classList.add('bubble-premium-gift-container');
content.style.height = '12.875rem';
content.style.width = '12.5rem';
const service = s.cloneNode(false) as HTMLElement;
service.classList.add('bubble-premium-gift-wrapper');
const size = 160;
const months = action.months;
const durationAssetMap: {[key: number]: LottieAssetName} = {
3: 'Gift3',
6: 'Gift6',
12: 'Gift12'
};
const assetName = durationAssetMap[clamp(months, 3, 12)];
const promise = wrapLocalSticker({
width: size,
height: size,
assetName,
middleware,
loop: false,
autoplay: liteMode.isAvailable('stickers_chat')
}).then(({container, promise}) => {
container.classList.add('bubble-premium-gift-sticker');
container.style.position = 'relative';
container.style.width = container.style.height = size + 'px';
service.prepend(container);
return promise;
});
const isYears = months >= 12 && !(months % 12);
const duration = i18n(isYears ? 'Years' : 'Months', [isYears ? months / 12 : months]);
const title = i18n('ActionGiftPremiumTitle');
const subtitle = i18n('ActionGiftPremiumSubtitle', [duration]);
title.classList.add('text-bold');
service.append(title, subtitle);
loadPromises.push(promise);
content.append(service);
bubbleContainer.after(content);
}
loadPromises.push(promise);
}
bubbleContainer.append(s);
if(message.pFlags.is_single) { // * Ignore 'Discussion started'
bubble.classList.add('is-group-last');
}
return ret;
}
let messageMedia: MessageMedia = isMessage && message.media;
let needToSetHTML = true;
let messageMessage: string, totalEntities: MessageEntity[], albumTextMessage: Message.message;
if(isMessage) {
if(groupedId && albumMustBeRenderedFull) {
const t = albumTextMessage = getAlbumText(albumMessages);
messageMessage = t?.message || '';
// totalEntities = t.entities;
totalEntities = t?.totalEntities || [];
} else {
messageMessage = message.message;
// totalEntities = message.entities;
totalEntities = message.totalEntities;
}
const document = (messageMedia as MessageMedia.messageMediaDocument)?.document as MyDocument;
if(document) {
if(document?.type === 'sticker') {
messageMessage = totalEntities = undefined;
} else if(!['video', 'gif'].includes(document.type)) {
needToSetHTML = false;
}
}
} else {
if(message.action._ === 'messageActionPhoneCall') {
messageMedia = {
_: 'messageMediaCall',
action: message.action
};
}
}
let bigEmojis = 0, customEmojiSize: MediaSize;
if(totalEntities && !messageMedia) {
const emojiEntities = totalEntities.filter((e) => e._ === 'messageEntityEmoji'/* || e._ === 'messageEntityCustomEmoji' */);
const strLength = messageMessage.replace(/\s/g, '').length;
const emojiStrLength = emojiEntities.reduce((acc, curr) => acc + curr.length, 0);
if(emojiStrLength === strLength /* && emojiEntities.length <= 3 *//* && totalEntities.length === emojiEntities.length */) {
bigEmojis = Math.min(BIG_EMOJI_SIZES_LENGTH, emojiEntities.length);
customEmojiSize = mediaSizes.active.customEmoji;
const size = BIG_EMOJI_SIZES[bigEmojis];
if(size) {
customEmojiSize = makeMediaSize(size, size);
bubble.style.setProperty('--emoji-size', size + 'px');
}
}
}
customEmojiSize ??= this.chat.appImManager.customEmojiSize;
let maxMediaTimestamp = getMediaDurationFromMessage(albumTextMessage || message as Message.message);
if(albumTextMessage && needToSetHTML) {
bubble.dataset.textMid = '' + albumTextMessage.mid;
}
if(message.reply_to) {
const replyToPeerId = message.reply_to.reply_to_peer_id ? getPeerId(message.reply_to.reply_to_peer_id) : this.peerId;
bubble.dataset.replyToPeerId = '' + replyToPeerId;
bubble.dataset.replyToMid = '' + message.reply_to_mid;
if(maxMediaTimestamp === undefined) {
const originalMessage = await rootScope.managers.appMessagesManager.getMessageByPeer(replyToPeerId, message.reply_to_mid);
if(originalMessage) {
maxMediaTimestamp = getMediaDurationFromMessage(originalMessage as Message.message);
} else {
// this.managers.appMessagesManager.fetchMessageReplyTo(message);
// this.needUpdate.push({replyToPeerId, replyMid: message.reply_to_mid, mid: message.mid});
maxMediaTimestamp = Infinity;
}
}
}
const richTextOptions: Parameters<typeof wrapRichText>[1] = {
entities: totalEntities,
passEntities: this.passEntities,
loadPromises,
lazyLoadQueue: this.lazyLoadQueue,
customEmojiSize,
middleware,
animationGroup: this.chat.animationGroup,
maxMediaTimestamp
};
const richText = messageMessage ? wrapRichText(messageMessage, richTextOptions) : undefined;
let canHaveTail = true;
let isStandaloneMedia = false;
let attachmentDiv: HTMLElement;
if(bigEmojis) {
if(rootScope.settings.emoji.big) {
const sticker = bigEmojis === 1 &&
!totalEntities.find((entity) => entity._ === 'messageEntityCustomEmoji') &&
await this.managers.appStickersManager.getAnimatedEmojiSticker(messageMessage);
if(bigEmojis === 1 && !messageMedia && sticker) {
messageMedia = {
_: 'messageMediaDocument',
document: sticker,
pFlags: {}
};
} else {
attachmentDiv = document.createElement('div');
attachmentDiv.classList.add('attachment', 'spoilers-container');
setInnerHTML(attachmentDiv, richText);
bubbleContainer.append(attachmentDiv);
}
bubble.classList.add('is-message-empty', 'emoji-big');
isStandaloneMedia = true;
canHaveTail = false;
needToSetHTML = false;
}
bubble.classList.add('can-have-big-emoji');
}
if(needToSetHTML) {
setInnerHTML(messageDiv, richText);
}
const haveRTLChar = isRTL(messageMessage, true);
const timeSpan = MessageRender.setTime({
chatType: this.chat.type,
message,
reactionsMessage
});
messageDiv.append(timeSpan);
bubbleContainer.prepend(messageDiv);
// bubble.prepend(timeSpan, messageDiv); // that's bad
if(haveRTLChar) {
timeSpan.classList.add('is-block');
}
let topicNameButtonContainer: HTMLElement;
if(isMessage && this.chat.isAllMessagesForum) {
const result = await wrapTopicNameButton({
peerId: this.peerId,
threadId: getMessageThreadId(message, this.chat.isForum),
lastMsgId: message.mid,
wrapOptions: {
middleware
},
withIcons: true
});
const {element} = result;
// if(isStandaloneMedia) {
// element.classList.add('floating-part');
// }
topicNameButtonContainer = document.createElement('div');
topicNameButtonContainer.classList.add(/* 'name', */'topic-name-button-container');
topicNameButtonContainer.append(element);
}
if(isMessage && message.views) {
bubble.classList.add('channel-post');
if(!message.fwd_from?.saved_from_msg_id && this.chat.type !== 'pinned') {
const forward = document.createElement('div');
forward.classList.add('bubble-beside-button', 'forward', 'tgico-forward_filled');
bubbleContainer.prepend(forward);
bubble.classList.add('with-beside-button');
}
if(!message.pFlags.is_outgoing && this.observer) {
this.observer.observe(bubble, this.viewsObserverCallback);
}
}
const replyMarkup = isMessage && message.reply_markup;
let replyMarkupRows = replyMarkup?._ === 'replyInlineMarkup' && replyMarkup.rows;
if(replyMarkupRows) {
replyMarkupRows = replyMarkupRows.filter((row) => row.buttons.length);
}
if(replyMarkupRows) {
const containerDiv = document.createElement('div');
containerDiv.classList.add('reply-markup');
replyMarkupRows.forEach((row) => {
const buttons = row.buttons;
const rowDiv = document.createElement('div');
rowDiv.classList.add('reply-markup-row');
buttons.forEach((button) => {
let text: DocumentFragment | HTMLElement | string = wrapRichText(button.text, {noLinks: true, noLinebreaks: true});
let buttonEl: HTMLButtonElement | HTMLAnchorElement;
switch(button._) {
case 'keyboardButtonUrl': {
const r = wrapRichText(' ', {
entities: [{
_: 'messageEntityTextUrl',
length: 1,
offset: 0,
url: button.url
}]
});
buttonEl = htmlToDocumentFragment(r).firstElementChild as HTMLAnchorElement;
buttonEl.classList.add('is-link');
break;
}
case 'keyboardButtonSwitchInline': {
buttonEl = document.createElement('button');
buttonEl.classList.add('is-switch-inline');
attachClickEvent(buttonEl, (e) => {
cancelEvent(e);
const botId = message.viaBotId || message.fromId;
let promise: Promise<PeerId>;
if(button.pFlags.same_peer) promise = Promise.resolve(this.peerId);
else promise = this.managers.appInlineBotsManager.checkSwitchReturn(botId).then((peerId) => {
if(peerId) {
return peerId;
}
return new Promise<PeerId>((resolve, reject) => {
const popup = PopupElement.createPopup(PopupForward, undefined, (peerId) => {
resolve(peerId);
});
popup.addEventListener('close', () => {
reject();
});
});
});
promise.then((peerId) => {
const threadId = this.peerId === peerId ? this.chat.threadId : undefined;
this.chat.appImManager.setInnerPeer({peerId});
this.managers.appInlineBotsManager.switchInlineQuery(peerId, threadId, botId, button.query);
});
});
break;
}
case 'keyboardButtonBuy': {
const mediaInvoice = messageMedia._ === 'messageMediaInvoice' ? messageMedia : undefined;
if(mediaInvoice?.extended_media) {
break;
}
buttonEl = document.createElement('button');
buttonEl.classList.add('is-buy');
if(mediaInvoice?.receipt_msg_id) {
text = i18n('Message.ReplyActionButtonShowReceipt');
}
break;
}
case 'keyboardButtonUrlAuth': {
buttonEl = document.createElement('button');
buttonEl.classList.add('is-url-auth');
const {peerId} = this;
const {mid} = message;
const {url, button_id} = button;
attachClickEvent(buttonEl, () => {
const toggle = toggleDisability([buttonEl], true);
this.chat.appImManager.handleUrlAuth({
peerId,
mid,
url,
buttonId: button_id
}).then(() => {
toggle();
});
});
break;
}
default: {
buttonEl = document.createElement('button');
break;
}
}
if(!buttonEl) {
return;
}
buttonEl.classList.add('reply-markup-button', 'rp', 'tgico');
const t = document.createElement('span');
t.classList.add('reply-markup-button-text');
if(typeof(text) === 'string') {
t.insertAdjacentHTML('beforeend', text);
} else {
t.append(text);
}
ripple(buttonEl);
buttonEl.append(t);
rowDiv.append(buttonEl);
});
if(!rowDiv.childElementCount) {
return;
}
containerDiv.append(rowDiv);
});
const haveButtons = !!containerDiv.childElementCount;
haveButtons && attachClickEvent(containerDiv, (e) => {
let target = e.target as HTMLElement;
if(!target.classList.contains('reply-markup-button')) target = findUpClassName(target, 'reply-markup-button');
if(
!target ||
target.classList.contains('is-link') ||
target.classList.contains('is-switch-inline') ||
target.classList.contains('is-buy') ||
target.classList.contains('is-url-auth')
) return;
cancelEvent(e);
const column = whichChild(target);
const row = replyMarkupRows[whichChild(target.parentElement)];
if(!row.buttons || !row.buttons[column]) {
this.log.warn('no such button', row, column, message);
return;
}
const button = row.buttons[column];
this.managers.appInlineBotsManager.callbackButtonClick(this.peerId, message.mid, button).then((callbackAnswer) => {
if(typeof callbackAnswer.message === 'string' && callbackAnswer.message.length) {
toast(wrapRichText(callbackAnswer.message, {noLinks: true, noLinebreaks: true}));
}
// console.log('callbackButtonClick callbackAnswer:', callbackAnswer);
});
});
if(haveButtons) {
// canHaveTail = false;
bubble.classList.add('with-reply-markup');
contentWrapper.append(containerDiv);
}
}
const isOutgoing = message.pFlags.is_outgoing/* && this.peerId !== rootScope.myId */;
if(our) {
if(message.pFlags.unread || isOutgoing) this.unreadOut.add(message.mid);
let status = '';
if(message.error) status = 'is-error';
else if(isOutgoing) status = 'is-sending';
else status = message.pFlags.unread || (message as Message.message).pFlags.is_scheduled ? 'is-sent' : 'is-read';
bubble.classList.add(status);
}
if(isOutgoing && !message.error) {
bubble.classList.add('is-outgoing');
}
const messageWithReplies = isMessage && await this.managers.appMessagesManager.getMessageWithCommentReplies(message);
const withReplies = !!messageWithReplies && message.mid > 0;
if(withReplies) {
bubble.classList.add('with-replies');
}
const fwdFrom = isMessage && message.fwd_from;
const fwdFromId = isMessage && message.fwdFromId;
const isOut = this.chat.isOutMessage(message);
let nameContainer: HTMLElement = bubbleContainer;
const canHideNameIfMedia = !message.viaBotId && (message.fromId === rootScope.myId || !message.pFlags.out);
const isMessageEmpty = !messageMessage/* && (!topicNameButtonContainer || isStandaloneMedia) */;
let viewButton: HTMLAnchorElement;
// media
if(messageMedia/* && messageMedia._ === 'messageMediaPhoto' */) {
attachmentDiv = document.createElement('div');
attachmentDiv.classList.add('attachment');
if(isMessageEmpty) {
bubble.classList.add('is-message-empty');
}
let processingWebPage = false;
/* if(isMessage) */switch(messageMedia._) {
case 'messageMediaPhoto': {
const photo = messageMedia.photo;
// //////this.log('messageMediaPhoto', photo);
if(isMessageEmpty) {
canHaveTail = false;
}
if(canHideNameIfMedia) {
bubble.classList.add('hide-name');
}
bubble.classList.add('photo');
if(albumMustBeRenderedFull && groupedId && albumMids.length !== 1) {
bubble.classList.add('is-album', 'is-grouped');
wrapAlbum({
messages: albumMessages,
attachmentDiv,
middleware: this.getMiddleware(),
isOut: our,
lazyLoadQueue: this.lazyLoadQueue,
chat: this.chat,
loadPromises,
autoDownload: this.chat.autoDownload
});
break;
}
const withTail = !IS_ANDROID && canHaveTail && !withReplies && USE_MEDIA_TAILS;
if(withTail) bubble.classList.add('with-media-tail');
const p = wrapPhoto({
photo: photo as Photo.photo,
message,
container: attachmentDiv,
withTail,
isOut,
lazyLoadQueue: this.lazyLoadQueue,
middleware: this.getMiddleware(),
loadPromises,
autoDownloadSize: this.chat.autoDownload.photo
});
if(messageMedia.pFlags.spoiler) {
loadPromises.push(this.wrapMediaSpoiler({
media: photo as Photo.photo,
promise: p,
middleware,
attachmentDiv
}));
}
break;
}
case 'messageMediaWebPage': {
processingWebPage = true;
const webPage: WebPage = messageMedia.webpage;
// //////this.log('messageMediaWebPage', webpage);
if(webPage._ !== 'webPage') {
break;
}
const wrapped = wrapUrl(webPage.url);
if(wrapped?.onclick === 'im') {
const map: {[type: string]: LangPackKey} = {
telegram_channel: 'Chat.Message.ViewChannel',
telegram_megagroup: 'OpenGroup',
telegram_bot: 'Chat.Message.ViewBot'
};
const langPackKey = map[webPage.type] || 'OpenMessage';
viewButton = this.makeViewButton({text: langPackKey, asLink: true});
viewButton.href = wrapped.url;
viewButton.setAttribute('onclick', `${wrapped.onclick}(this)`);
}
bubble.classList.add('webpage');
const box = document.createElement('div');
box.classList.add('web');
const quote = document.createElement('div');
quote.classList.add('quote');
let previewResizer: HTMLDivElement, preview: HTMLDivElement;
const photo: Photo.photo = webPage.photo as any;
if(photo || webPage.document) {
previewResizer = document.createElement('div');
previewResizer.classList.add('preview-resizer');
preview = document.createElement('div');
preview.classList.add('preview');
previewResizer.append(preview);
}
const quoteTextDiv = document.createElement('div');
quoteTextDiv.classList.add('quote-text');
const doc = webPage.document as MyDocument;
if(doc) {
if(doc.type === 'gif' || doc.type === 'video' || doc.type === 'round') {
// if(doc.size <= 20e6) {
const mediaSize = doc.type === 'round' ? mediaSizes.active.round : mediaSizes.active.webpage;
if(doc.type === 'round') {
bubble.classList.add('round');
preview.classList.add('is-round');
} else {
bubble.classList.add('video');
}
wrapVideo({
doc,
container: preview,
message: message as Message.message,
boxWidth: mediaSize.width,
boxHeight: mediaSize.height,
lazyLoadQueue: this.lazyLoadQueue,
middleware: this.getMiddleware(),
isOut,
group: this.chat.animationGroup,
loadPromises,
autoDownload: this.chat.autoDownload,
noInfo: message.mid < 0
});
// }
} else {
const docDiv = await wrapDocument({
message: message as Message.message,
autoDownloadSize: this.chat.autoDownload.file,
lazyLoadQueue: this.lazyLoadQueue,
loadPromises,
sizeType: 'documentName',
searchContext: {
useSearch: false,
peerId: this.peerId,
inputFilter: {
_: 'inputMessagesFilterEmpty'
}
},
fontSize: rootScope.settings.messagesTextSize,
canTranscribeVoice: true
});
preview.append(docDiv);
preview.classList.add('preview-with-document');
quoteTextDiv.classList.add('has-document');
// messageDiv.classList.add((webpage.type || 'document') + '-message');
// doc = null;
}
}
if(previewResizer) {
quoteTextDiv.append(previewResizer);
}
let t: HTMLElement;
if(webPage.site_name) {
const html = wrapRichText(webPage.url);
const a: HTMLAnchorElement = htmlToDocumentFragment(html).firstElementChild as any;
a.classList.add('webpage-name');
const strong = document.createElement('strong');
setInnerHTML(strong, wrapEmojiText(webPage.site_name));
a.textContent = '';
a.append(strong);
quoteTextDiv.append(a);
t = a;
}
const title = wrapWebPageTitle(webPage);
if(title.textContent) {
const titleDiv = document.createElement('div');
titleDiv.classList.add('title');
const strong = document.createElement('strong');
setInnerHTML(strong, title);
titleDiv.append(strong);
quoteTextDiv.append(titleDiv);
t = titleDiv;
}
const description = wrapWebPageDescription(webPage);
if(description.textContent) {
const textDiv = document.createElement('div');
textDiv.classList.add('text');
setInnerHTML(textDiv, description);
quoteTextDiv.append(textDiv);
t = textDiv;
}
/* if(t) {
t.append(timeSpan);
} else {
box.classList.add('no-text');
} */
quote.append(quoteTextDiv);
if(photo && !doc) {
bubble.classList.add('photo');
const size: PhotoSize.photoSize = photo.sizes[photo.sizes.length - 1] as any;
let isSquare = false;
if(size.w === size.h && t) {
bubble.classList.add('is-square-photo');
isSquare = true;
setAttachmentSize(photo, preview, 48, 48, false);
/* if(t) {
t.append(timeSpan);
} */
} else if(size.h > size.w) {
bubble.classList.add('is-vertical-photo');
}
wrapPhoto({
photo,
message,
container: preview,
boxWidth: isSquare ? 0 : mediaSizes.active.webpage.width,
boxHeight: isSquare ? 0 : mediaSizes.active.webpage.height,
isOut,
lazyLoadQueue: this.lazyLoadQueue,
middleware: this.getMiddleware(),
loadPromises,
withoutPreloader: isSquare,
autoDownloadSize: this.chat.autoDownload.photo
});
}
box.append(quote);
// bubble.prepend(box);
// if(timeSpan.parentElement === messageDiv) {
messageDiv.insertBefore(box, timeSpan);
// } else {
// messageDiv.append(box);
// }
// this.log('night running', bubble.scrollHeight);
break;
}
case 'messageMediaDocument': {
const doc = messageMedia.document as MyDocument;
// this.log('messageMediaDocument', doc, bubble);
if(doc.sticker/* && doc.size <= 1e6 */) {
bubble.classList.add('sticker');
canHaveTail = false;
isStandaloneMedia = true;
if(doc.animated) {
bubble.classList.add('sticker-animated');
}
const sizes = mediaSizes.active;
const isEmoji = bubble.classList.contains('emoji-big');
const boxSize = isEmoji ? sizes.emojiSticker : (doc.animated ? sizes.animatedSticker : sizes.staticSticker);
setAttachmentSize(doc, attachmentDiv, boxSize.width, boxSize.height);
// let preloader = new ProgressivePreloader(attachmentDiv, false);
bubbleContainer.style.minWidth = attachmentDiv.style.width;
bubbleContainer.style.minHeight = attachmentDiv.style.height;
// appPhotosManager.setAttachmentSize(doc, bubble);
wrapSticker({
doc,
div: attachmentDiv,
middleware,
lazyLoadQueue: this.lazyLoadQueue,
group: this.chat.animationGroup,
// play: !!message.pending || !multipleRender,
play: true,
liteModeKey: 'stickers_chat',
loop: true,
emoji: isEmoji ? messageMessage : undefined,
withThumb: true,
loadPromises,
isOut,
noPremium: messageMedia?.pFlags?.nopremium
});
if((getStickerEffectThumb(doc) || isEmoji) && (isInUnread || isOutgoing)/* || true */) {
this.observer.observe(bubble, this.stickerEffectObserverCallback);
}
} else if(doc.type === 'video' || doc.type === 'gif' || doc.type === 'round'/* && doc.size <= 20e6 */) {
// this.log('never get free 2', doc);
const isRound = doc.type === 'round';
if(isRound) {
isStandaloneMedia = true;
}
if(isRound || isMessageEmpty) {
canHaveTail = false;
}
if(canHideNameIfMedia) {
bubble.classList.add('hide-name');
}
bubble.classList.add(isRound ? 'round' : 'video');
if(albumMustBeRenderedFull && groupedId && albumMids.length !== 1) {
bubble.classList.add('is-album', 'is-grouped');
wrapAlbum({
messages: albumMessages,
attachmentDiv,
middleware,
isOut: our,
lazyLoadQueue: this.lazyLoadQueue,
chat: this.chat,
loadPromises,
autoDownload: this.chat.autoDownload
});
} else {
const withTail = !IS_ANDROID && !IS_APPLE && !isRound && canHaveTail && !withReplies && USE_MEDIA_TAILS;
if(withTail) bubble.classList.add('with-media-tail');
const p = wrapVideo({
doc,
container: attachmentDiv,
message: message as Message.message,
boxWidth: mediaSizes.active.regular.width,
boxHeight: mediaSizes.active.regular.height,
withTail,
isOut,
lazyLoadQueue: this.lazyLoadQueue,
middleware,
group: this.chat.animationGroup,
loadPromises,
autoDownload: this.chat.autoDownload,
searchContext: isRound ? {
peerId: this.peerId,
inputFilter: {_: 'inputMessagesFilterRoundVoice'},
threadId: this.chat.threadId,
useSearch: !(message as Message.message).pFlags.is_scheduled,
isScheduled: (message as Message.message).pFlags.is_scheduled
} : undefined,
noInfo: message.mid < 0,
noAutoplayAttribute: !!messageMedia.pFlags.spoiler
});
if(messageMedia.pFlags.spoiler) {
loadPromises.push(this.wrapMediaSpoiler({
media: doc,
promise: p,
middleware,
attachmentDiv
}));
}
}
} else {
const newNameContainer = await wrapGroupedDocuments({
albumMustBeRenderedFull,
message,
bubble,
messageDiv,
chat: this.chat,
loadPromises,
autoDownloadSize: this.chat.autoDownload.file,
lazyLoadQueue: this.lazyLoadQueue,
searchContext: doc.type === 'voice' || doc.type === 'audio' ? {
peerId: this.peerId,
inputFilter: {_: doc.type === 'voice' ? 'inputMessagesFilterRoundVoice' : 'inputMessagesFilterMusic'},
threadId: this.chat.threadId,
useSearch: !(message as Message.message).pFlags.is_scheduled,
isScheduled: (message as Message.message).pFlags.is_scheduled
} : undefined,
sizeType: 'documentName',
fontSize: rootScope.settings.messagesTextSize,
richTextFragment: typeof(richText) === 'string' ? undefined : richText,
richTextOptions,
canTranscribeVoice: true
});
if(newNameContainer) {
nameContainer = newNameContainer;
}
const lastContainer = messageDiv.lastElementChild.querySelector('.document-message, .document, .audio');
// lastContainer && lastContainer.append(timeSpan.cloneNode(true));
lastContainer && lastContainer.append(timeSpan);
bubble.classList.remove('is-message-empty');
const addClassName = (!(['photo', 'pdf'] as MyDocument['type'][]).includes(doc.type) ? doc.type || 'document' : 'document') + '-message';
messageDiv.classList.add(addClassName);
if(doc.type === 'audio' || doc.type === 'voice') {
bubble.classList.add('min-content');
}
processingWebPage = true;
}
break;
}
case 'messageMediaCall': {
const action = messageMedia.action;
const div = document.createElement('div');
div.classList.add('bubble-call', action.pFlags.video ? 'tgico-videocamera' : 'tgico-phone');
const type: CallType = action.pFlags.video ? 'video' : 'voice';
div.dataset.type = type;
const title = document.createElement('div');
title.classList.add('bubble-call-title');
_i18n(title, isOut ?
(action.pFlags.video ? 'CallMessageVideoOutgoing' : 'CallMessageOutgoing') :
(action.pFlags.video ? 'CallMessageVideoIncoming' : 'CallMessageIncoming'));
const subtitle = document.createElement('div');
subtitle.classList.add('bubble-call-subtitle');
if(action.duration !== undefined) {
subtitle.append(formatCallDuration(action.duration));
} else {
let langPackKey: LangPackKey;
switch(action.reason._) {
case 'phoneCallDiscardReasonBusy':
langPackKey = 'Call.StatusBusy';
break;
case 'phoneCallDiscardReasonMissed':
langPackKey = 'Chat.Service.Call.Missed';
break;
// case 'phoneCallDiscardReasonHangup':
default:
langPackKey = 'Chat.Service.Call.Cancelled';
break;
}
subtitle.classList.add('is-reason');
_i18n(subtitle, langPackKey);
}
subtitle.classList.add('tgico', 'arrow-' + (action.duration !== undefined ? 'green' : 'red'));
div.append(title, subtitle);
processingWebPage = true;
bubble.classList.remove('is-message-empty');
messageDiv.classList.add('call-message');
messageDiv.append(div);
break;
}
case 'messageMediaContact': {
// this.log('wrapping contact', message);
const contact = messageMedia;
const contactDiv = document.createElement('div');
contactDiv.classList.add('contact');
contactDiv.dataset.peerId = '' + contact.user_id;
processingWebPage = true;
const contactDetails = document.createElement('div');
contactDetails.className = 'contact-details';
const contactNameDiv = document.createElement('div');
contactNameDiv.className = 'contact-name';
const fullName = [
contact.first_name,
contact.last_name
].filter(Boolean).join(' ');
contactNameDiv.append(
fullName.trim() ? wrapEmojiText(fullName) : i18n('AttachContact')
);
const contactNumberDiv = document.createElement('div');
contactNumberDiv.className = 'contact-number';
contactNumberDiv.textContent = contact.phone_number ? '+' + formatPhoneNumber(contact.phone_number).formatted : 'Unknown phone number';
contactDiv.append(contactDetails);
contactDetails.append(contactNameDiv, contactNumberDiv);
const avatarElem = new AvatarElement();
avatarElem.updateWithOptions({
lazyLoadQueue: this.lazyLoadQueue,
peerId: contact.user_id.toPeerId(),
peerTitle: contact.user_id ? undefined : (fullName.trim() ? fullName : I18n.format('AttachContact', true)[0])
});
avatarElem.classList.add('contact-avatar', 'avatar-54');
contactDiv.prepend(avatarElem);
bubble.classList.remove('is-message-empty');
messageDiv.classList.add('contact-message');
messageDiv.append(contactDiv);
break;
}
case 'messageMediaPoll': {
bubble.classList.remove('is-message-empty');
const pollElement = wrapPoll(message);
messageDiv.prepend(pollElement);
messageDiv.classList.add('poll-message');
break;
}
case 'messageMediaInvoice': {
const isTest = messageMedia.pFlags.test;
const extendedMedia = messageMedia.extended_media;
const isAlreadyPaid = extendedMedia?._ === 'messageExtendedMedia';
const isNotPaid = extendedMedia?._ === 'messageExtendedMediaPreview';
let innerMedia = isAlreadyPaid ?
(extendedMedia.media as MessageMedia.messageMediaPhoto).photo as Photo.photo ||
(extendedMedia.media as MessageMedia.messageMediaDocument).document as Document.document :
messageMedia.photo;
const wrappedPrice = paymentsWrapCurrencyAmount(messageMedia.total_amount, messageMedia.currency);
let priceEl: HTMLElement;
if(!extendedMedia) {
priceEl = document.createElement(innerMedia ? 'span' : 'div');
const f = document.createDocumentFragment();
const l = i18n(messageMedia.receipt_msg_id ? 'PaymentReceipt' : (isTest ? 'PaymentTestInvoice' : 'PaymentInvoice'));
l.classList.add('text-uppercase');
const joiner = ' ' + NBSP;
const p = document.createElement('span');
p.classList.add('text-bold');
p.textContent = wrappedPrice + joiner;
f.append(p, l);
if(isTest && messageMedia.receipt_msg_id) {
const a = document.createElement('span');
a.classList.add('text-uppercase', 'pre-wrap');
a.append(joiner + '(Test)');
f.append(a);
}
setInnerHTML(priceEl, f);
} else if(isNotPaid) {
priceEl = document.createElement('span');
priceEl.classList.add('extended-media-buy', 'tgico-premium_lock');
attachmentDiv.classList.add('is-buy');
_i18n(priceEl, 'Checkout.PayPrice', [wrappedPrice]);
if(extendedMedia.video_duration !== undefined) {
const videoTime = document.createElement('span');
videoTime.classList.add('video-time');
videoTime.textContent = toHHMMSS(extendedMedia.video_duration, false);
attachmentDiv.append(videoTime);
}
}
if(isNotPaid) {
(extendedMedia.thumb as PhotoSize.photoStrippedSize).w = extendedMedia.w;
(extendedMedia.thumb as PhotoSize.photoStrippedSize).h = extendedMedia.h;
innerMedia = {
_: 'photo',
access_hash: '',
pFlags: {},
date: 0,
dc_id: 0,
file_reference: [],
id: 0,
sizes: [extendedMedia.thumb]
};
}
if(innerMedia) {
const mediaSize = extendedMedia ? mediaSizes.active.extendedInvoice : mediaSizes.active.invoice;
if(innerMedia._ === 'document') {
wrapVideo({
doc: innerMedia,
container: attachmentDiv,
withTail: false,
isOut,
lazyLoadQueue: this.lazyLoadQueue,
middleware,
loadPromises,
boxWidth: mediaSize.width,
boxHeight: mediaSize.height,
group: this.chat.animationGroup,
message: message as Message.message
});
bubble.classList.add('video');
} else {
wrapPhoto({
photo: innerMedia,
container: attachmentDiv,
withTail: false,
isOut,
lazyLoadQueue: this.lazyLoadQueue,
middleware,
loadPromises,
boxWidth: mediaSize.width,
boxHeight: mediaSize.height,
message: isAlreadyPaid ? message : undefined
});
bubble.classList.add('photo');
}
if(priceEl) {
if(!extendedMedia) {
priceEl.classList.add('video-time');
}
attachmentDiv.append(priceEl);
}
} else {
attachmentDiv = undefined;
}
if(isNotPaid) {
const {mid} = message;
this.extendedMediaMessages.add(mid);
middleware.onClean(() => {
this.extendedMediaMessages.delete(mid);
});
this.setExtendedMediaMessagesPollInterval();
const {width, height} = attachmentDiv.style;
const dotRenderer = DotRenderer.create({
width: parseInt(width),
height: parseInt(height),
middleware,
animationGroup: this.chat.animationGroup
});
attachmentDiv.append(dotRenderer.canvas);
}
let titleDiv: HTMLElement;
if(!extendedMedia) {
titleDiv = document.createElement('div');
titleDiv.classList.add('bubble-primary-color');
setInnerHTML(titleDiv, wrapEmojiText(messageMedia.title));
}
const richText = isAlreadyPaid ? undefined : wrapEmojiText(messageMedia.description);
messageDiv.prepend(...[titleDiv, !innerMedia && priceEl, richText].filter(Boolean));
if(!richText) canHaveTail = false;
else bubble.classList.remove('is-message-empty');
bubble.classList.add('is-invoice');
break;
}
default:
attachmentDiv = undefined;
bubble.classList.remove('is-message-empty');
messageDiv.append(i18n(UNSUPPORTED_LANG_PACK_KEY), timeSpan);
this.log.warn('unrecognized media type:', messageMedia._, message);
break;
}
if(!processingWebPage && attachmentDiv) {
bubbleContainer.append(attachmentDiv);
}
/* if(bubble.classList.contains('is-message-empty') && (bubble.classList.contains('photo') || bubble.classList.contains('video'))) {
bubble.classList.add('no-tail');
if(!bubble.classList.contains('with-media-tail')) {
bubble.classList.add('use-border-radius');
}
} */
}
if(isStandaloneMedia) {
bubble.classList.add('just-media');
}
if(viewButton) {
timeSpan.before(viewButton);
// messageDiv.append(viewButton);
}
let savedFrom = '';
// const needName = ((peerId.isAnyChat() && (peerId !== message.fromId || our)) && message.fromId !== rootScope.myId) || message.viaBotId;
const needName = (message.fromId !== rootScope.myId && this.chat.isAnyGroup) || message.viaBotId || (message as Message.message).pFlags.sponsored;
if(needName || fwdFrom || message.reply_to_mid || topicNameButtonContainer) { // chat
let title: HTMLElement | DocumentFragment;
let titleVia: typeof title;
const isForwardFromChannel = message.from_id?._ === 'peerChannel' && message.fromId === fwdFromId;
const mustHaveName = !!(message.viaBotId/* || topicNameButtonContainer */);
const isHidden = fwdFrom && !fwdFrom.from_id;
if(message.viaBotId) {
titleVia = document.createElement('span');
titleVia.innerText = '@' + (await this.managers.appPeersManager.getPeerUsername(message.viaBotId));
titleVia.classList.add('peer-title');
}
if(mustHaveName) {
bubble.classList.add('must-have-name');
}
const isForward = fwdFromId || fwdFrom;
if(isHidden) {
title = document.createElement('span');
setInnerHTML(title, wrapEmojiText(fwdFrom.from_name));
title.classList.add('peer-title');
bubble.classList.add('hidden-profile');
} else {
title = new PeerTitle({
peerId: fwdFromId || message.fromId,
withPremiumIcon: !isForward,
wrapOptions
}).element;
}
let replyContainer: HTMLElement;
if(
isMessage &&
message.reply_to_mid &&
message.reply_to_mid !== this.chat.threadId &&
(!this.chat.isAllMessagesForum || message.reply_to.reply_to_top_id)
) {
replyContainer = await MessageRender.setReply({
chat: this.chat,
bubble,
bubbleContainer,
message
});
}
// this.log(title);
let nameDiv: HTMLElement;
if(isForward) {
if(this.peerId !== rootScope.myId && !isForwardFromChannel) {
bubble.classList.add('forwarded');
}
if(message.savedFrom) {
savedFrom = message.savedFrom;
title.dataset.savedFrom = savedFrom;
}
nameDiv = document.createElement('div');
title.dataset.peerId = '' + fwdFromId;
if((this.peerId === rootScope.myId || this.peerId === REPLIES_PEER_ID || isForwardFromChannel) && !isStandaloneMedia) {
nameDiv.style.color = getPeerColorById(fwdFromId, false);
nameDiv.classList.add('colored-name');
nameDiv.append(title);
} else {
/* const fromTitle = message.fromId === this.myID || appPeersManager.isBroadcast(fwdFromId || message.fromId) ? '' : `<div class="name" data-peer-id="${message.fromId}" style="color: ${appPeersManager.getPeerColorByID(message.fromId, false)};">${appPeersManager.getPeerTitle(message.fromId)}</div>`;
nameDiv.innerHTML = fromTitle + 'Forwarded from ' + title; */
const args: FormatterArguments = [title];
if(isStandaloneMedia) {
const br = document.createElement('br');
br.classList.add('hide-ol');
args.unshift(br);
}
nameDiv.append(i18n('ForwardedFrom', [args]));
}
} else if(!message.viaBotId) {
if(!isStandaloneMedia && needName) {
nameDiv = document.createElement('div');
nameDiv.append(title);
const peer = await this.managers.appPeersManager.getPeer(message.fromId);
const pFlags = (peer as User.user)?.pFlags;
if(pFlags && (pFlags.scam || pFlags.fake)) {
nameDiv.append(generateFakeIcon(pFlags.scam));
}
if(!our) {
nameDiv.style.color = getPeerColorById(message.fromId, false);
nameDiv.classList.add('colored-name');
}
nameDiv.dataset.peerId = '' + message.fromId;
} else /* if(!message.reply_to_mid) */ {
bubble.classList.add('hide-name');
}
}
if(message.viaBotId) {
if(!nameDiv) {
nameDiv = document.createElement('div');
} else {
nameDiv.append(' ');
}
const span = document.createElement('span');
span.append(i18n('ViaBot'), ' ', titleVia);
span.classList.add('is-via');
nameDiv.append(span);
}
if(topicNameButtonContainer) {
if(isStandaloneMedia) {
topicNameButtonContainer.classList.add('floating-part');
} else {
if(!nameDiv) {
nameDiv = document.createElement('div');
}
nameDiv.append(topicNameButtonContainer);
bubble.classList.remove('hide-name');
}
}
if(nameDiv) {
nameDiv.classList.add('name');
if(isStandaloneMedia) {
nameContainer.append(nameContainer = document.createElement('div'));
nameContainer.classList.add('name-with-reply', 'floating-part');
} else {
nameDiv.classList.add('floating-part');
}
nameContainer.append(nameDiv);
if(isStandaloneMedia && replyContainer) {
nameContainer.append(replyContainer);
}
} else if(isStandaloneMedia && replyContainer) {
replyContainer.classList.add('floating-part');
}
if(title && !isHidden && !fwdFromId && this.canShowRanks) {
const processRank = () => {
const rank = this.ranks.get(message.fromId);
if(!rank) {
return;
}
(title as HTMLElement).after(this.createBubbleNameRank(rank));
};
if(this.ranks) {
processRank();
} else {
const processRanks = this.processRanks;
processRanks.add(processRank);
middleware.onDestroy(() => {
processRanks.delete(processRank);
});
}
} else if(isForwardFromChannel) {
(title as HTMLElement).after(this.createBubbleNameRank(0));
}
if(topicNameButtonContainer && isStandaloneMedia) {
if(!attachmentDiv) {
this.log.error('no attachment div?', bubble, message);
debugger;
} else {
attachmentDiv.after(topicNameButtonContainer);
}
}
} else {
bubble.classList.add('hide-name');
}
if(this.chat.type === 'pinned') {
savedFrom = `${this.chat.peerId}_${message.mid}`;
}
const isThreadStarter = messageWithReplies && messageWithReplies.mid === this.chat.threadId;
if(isThreadStarter) {
bubble.classList.add('is-thread-starter', 'is-group-last');
}
if(savedFrom && (this.chat.type === 'pinned' || fwdFrom.saved_from_msg_id) && this.peerId !== REPLIES_PEER_ID) {
const goto = document.createElement('div');
goto.classList.add('bubble-beside-button', 'goto-original', 'tgico-arrow_next');
bubbleContainer.append(goto);
bubble.dataset.savedFrom = savedFrom;
bubble.classList.add('with-beside-button');
}
bubble.classList.add(isOut ? 'is-out' : 'is-in');
if(withReplies) {
const isFooter = MessageRender.renderReplies({
bubble,
bubbleContainer,
message: messageWithReplies,
messageDiv,
loadPromises,
lazyLoadQueue: this.lazyLoadQueue
});
if(isFooter) {
canHaveTail = true;
} else {
bubble.classList.add('with-beside-replies');
}
}
if(isMessage) {
this.appendReactionsElementToBubble(bubble, message, reactionsMessage);
}
/* if(isMessage) {
const reactionHover = document.createElement('div');
reactionHover.classList.add('bubble-reaction-hover');
contentWrapper.append(reactionHover);
} */
if(canHaveTail) {
bubble.classList.add('can-have-tail');
bubbleContainer.append(generateTail());
}
return ret;
}
private appendReactionsElementToBubble(
bubble: HTMLElement,
message: Message.message,
reactionsMessage: Message.message,
changedResults?: ReactionCount[]
) {
if(this.peerId.isUser() && USER_REACTIONS_INLINE/* || true */) {
return;
}
if(!reactionsMessage?.reactions || !reactionsMessage.reactions.results.length) {
return;
}
// message = this.appMessagesManager.getMessageWithReactions(message);
const reactionsElement = new ReactionsElement();
reactionsElement.init(reactionsMessage, 'block', bubble.middlewareHelper.get());
reactionsElement.render(changedResults);
if(bubble.classList.contains('is-message-empty')) {
bubble.querySelector('.bubble-content-wrapper').append(reactionsElement);
} else {
const messageDiv = bubble.querySelector('.message');
if(bubble.classList.contains('is-multiple-documents')) {
const documentContainer = messageDiv.lastElementChild as HTMLElement;
let documentMessageDiv = documentContainer.querySelector('.document-message');
let timeSpan: HTMLElement = documentMessageDiv && documentMessageDiv.querySelector('.time');
if(!timeSpan) {
timeSpan = MessageRender.setTime({
chatType: this.chat.type,
message,
reactionsMessage
});
}
reactionsElement.append(timeSpan);
if(!documentMessageDiv) {
documentMessageDiv = document.createElement('div');
documentMessageDiv.classList.add('document-message');
documentContainer.querySelector('.document-wrapper').prepend(documentMessageDiv);
}
documentMessageDiv.append(reactionsElement);
} else {
const timeSpan = Array.from(bubble.querySelectorAll('.time')).pop();
reactionsElement.append(timeSpan);
messageDiv.append(reactionsElement);
}
}
}
private createBubbleNameRank(rank: ReturnType<typeof getParticipantRank> | 0) {
const span = document.createElement('span');
span.classList.add('bubble-name-rank');
span.append(wrapParticipantRank(rank));
return span;
}
private prepareToSaveScroll(reverse?: boolean) {
const isMounted = !!this.chatInner.parentElement;
if(!isMounted) {
return {};
}
const log = this.log.bindPrefix('prepareToSaveScroll');
log('save');
const scrollSaver = this.createScrollSaver(reverse);
scrollSaver.save(); // * let's save scroll position by point before the slicing, not after
if(this.getRenderedLength() && !this.chat.setPeerPromise) {
const viewportSlice = this.getViewportSlice();
this.deleteViewportSlice(viewportSlice, true);
}
// scrollSaver.save(); // ! slicing will corrupt scroll position
// const saved = scrollSaver.getSaved();
// const hadScroll = saved.scrollHeight !== saved.clientHeight;
return {
restoreScroll: () => {
log('restore');
// scrollSaver.restore(_history.length === 1 && !reverse ? false : true);
scrollSaver.restore(reverse);
this.onRenderScrollSet(scrollSaver.getSaved());
},
scrollSaver
};
}
public async performHistoryResult(
historyResult: HistoryResult | {history: (Message.message | Message.messageService | number)[]},
reverse: boolean
) {
const log = false ? this.log.bindPrefix('perform-' + (Math.random() * 1000 | 0)) : undefined;
log?.('start', this.chatInner.parentElement);
let history = historyResult.history;
history = history.slice(); // need
if(this.needReflowScroll) {
reflowScrollableElement(this.scrollable.container);
this.needReflowScroll = false;
}
const cb = (message: Message.message | Message.messageService) => {
if(!message) {
return;
} else if(message.pFlags.local) {
return this.processLocalMessageRender(message);
} else {
return this.safeRenderMessage(message, reverse);
}
};
const messages = await Promise.all(history.map((mid) => {
return typeof(mid) === 'number' ? this.chat.getMessage(mid) : mid;
}));
const setLoadedPromises: Promise<any>[] = [];
if(!this.scrollable.loadedAll['bottom'] || !this.scrollable.loadedAll['top']) {
let isEnd = (historyResult as HistoryResult).isEnd;
if(!isEnd) {
const historyStorage = await this.chat.getHistoryStorage();
const firstSlice = historyStorage.history.first;
const lastSlice = historyStorage.history.last;
isEnd = {top: false, bottom: false, both: false};
if(firstSlice.isEnd(SliceEnd.Bottom) && (!firstSlice.length || history.includes(firstSlice[0]))) {
isEnd.bottom = true;
}
if(lastSlice.isEnd(SliceEnd.Top) && (!lastSlice.length || history.includes(lastSlice[lastSlice.length - 1]))) {
isEnd.top = true;
}
}
if(!isEnd.bottom && this.setPeerOptions) {
const {lastMsgId, topMessage, savedPosition} = this.setPeerOptions;
this.setPeerOptions = undefined;
if((!lastMsgId && !savedPosition) || this.bubbles[topMessage] || lastMsgId === topMessage) {
isEnd.bottom = true;
}
}
if(isEnd.top) setLoadedPromises.push(this.setLoaded('top', true));
if(isEnd.bottom) setLoadedPromises.push(this.setLoaded('bottom', true));
}
await Promise.all(setLoadedPromises);
// ! it is important to insert bubbles to group reversed way
// const length = history.length, promises: Promise<any>[] = [];
// if(reverse) for(let i = 0; i < length; ++i) promises.push(cb(messages[i]));
// else for(let i = length - 1; i >= 0; --i) promises.push(cb(messages[i]));
const promises = messages.map(cb);
// cannot combine them into one promise
await Promise.all(promises);
await this.messagesQueuePromise;
if(this.scrollable.loadedAll.top && this.messagesQueueOnRenderAdditional) {
this.messagesQueueOnRenderAdditional();
this.messagesQueueOnRenderAdditional?.(); // * can set it second time
}
log?.('performHistoryResult end');
}
private onRenderScrollSet(state?: {scrollHeight: number, clientHeight: number}) {
const className = 'has-sticky-dates';
if(!this.container.classList.contains(className)) {
const isLoading = !this.preloader.detached;
if(isLoading ||
(
state ??= {
scrollHeight: this.scrollable.scrollHeight,
clientHeight: this.scrollable.container.clientHeight
},
state.scrollHeight !== state.clientHeight
)
) {
/* for(const timestamp in this.dateMessages) {
const dateMessage = this.dateMessages[timestamp];
dateMessage.div.classList.add('is-sticky');
} */
const middleware = this.getMiddleware();
const callback = () => {
if(!middleware()) return;
this.container.classList.add(className);
};
if(this.willScrollOnLoad) {
callback();
} else {
setTimeout(callback, 600);
}
return;
}
}
this.willScrollOnLoad = undefined;
}
public onDatePick = (timestamp: number) => {
const peerId = this.peerId;
this.managers.appMessagesManager.requestHistory(peerId, 0, 2, -1, timestamp, this.chat.threadId).then((history) => {
if(!history?.messages?.length) {
this.log.error('no history!');
return;
} else if(this.peerId !== peerId) {
return;
}
this.chat.setMessageId((history.messages[0] as MyMessage).mid);
// console.log('got history date:', history);
});
};
public requestHistory(maxId: number, loadCount: number, backLimit: number) {
// const middleware = this.getMiddleware();
if(this.chat.type === 'chat' || this.chat.type === 'discussion') {
return this.managers.acknowledged.appMessagesManager.getHistory(this.peerId, maxId, loadCount, backLimit, this.chat.threadId);
} else if(this.chat.type === 'pinned') {
return this.managers.acknowledged.appMessagesManager.getSearch({
peerId: this.peerId,
inputFilter: {_: 'inputMessagesFilterPinned'},
maxId,
limit: loadCount,
backLimit
}).then((ackedResult) => {
return {
cached: ackedResult.cached,
result: Promise.resolve(ackedResult.result).then((value) => {
return {history: value.history.map((m) => m.mid)};
})
};
});
} else if(this.chat.type === 'scheduled') {
return this.managers.acknowledged.appMessagesManager.getScheduledMessages(this.peerId).then((ackedResult) => {
// this.setLoaded('top', true);
// this.setLoaded('bottom', true);
return {
cached: ackedResult.cached,
result: Promise.resolve(ackedResult.result).then((mids) => ({history: mids.slice().reverse()}))
};
});
}
}
private async animateAsLadder(additionMsgId: number, additionMsgIds: number[], isAdditionRender: boolean, backLimit: number, maxId: number) {
/* const middleware = this.getMiddleware();
await this.ladderDeferred; */
const log = this.log.bindPrefix('ladder');
if(this.chat.setPeerPromise && !this.resolveLadderAnimation) {
log.warn('will be delayed');
// @ts-ignore
this.resolveLadderAnimation = this.animateAsLadder.bind(this, additionMsgId, additionMsgIds, isAdditionRender, backLimit, maxId);
return;
}
/* if(!middleware()) {
return;
} */
if(!Object.keys(this.bubbles).length) {
log.warn('no bubbles');
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 || Math.max(...sortedMids); // * on discussion enter
} 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();
if(DEBUG) {
log('targeting mid:', targetMid, maxId, additionMsgId,
topIds.map((m) => getServerMessageId(m)),
bottomIds.map((m) => getServerMessageId(m)));
}
const setBubbles: HTMLElement[] = [];
this.chatInner.classList.add('zoom-fading');
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) => {
const bubble = this.bubbles[mid];
if(!bubble || this.skippedMids.has(mid)) {
log.warn('no bubble by mid:', mid);
return;
}
lastMsDelay = ((idx + offsetIndex) || 0.1) * delay;
// lastMsDelay = (idx + offsetIndex) * delay;
// lastMsDelay = (idx || 0.1) * 1000;
const contentWrapper = bubble.lastElementChild as HTMLElement;
const elementsToAnimate: HTMLElement[] = [contentWrapper];
const item = this.bubbleGroups.getItemByBubble(bubble);
if(item && item.group.avatar && item.group.lastItem === item) {
elementsToAnimate.push(item.group.avatar);
}
elementsToAnimate.forEach((element) => {
element.classList.add('zoom-fade', 'can-zoom-fade');
element.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);
}
setBubbles.push(...elementsToAnimate);
});
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];
if(this.onAnimateLadder) {
await this.onAnimateLadder();
}
fastRaf(() => {
this.setStickyDateManually(); // ! maybe it's not efficient
setBubbles.forEach((element) => {
element.classList.remove('zoom-fade');
});
});
let promise: Promise<any>;
if(topIds.length || middleIds.length || bottomIds.length) {
promise = Promise.all(promises);
dispatchHeavyAnimationEvent(promise, Math.max(...delays) + 200) // * 200 - transition time
.then(() => {
fastRaf(() => {
setBubbles.forEach((element) => {
element.style.transitionDelay = '';
element.classList.remove('can-zoom-fade');
});
this.chatInner.classList.remove('zoom-fading');
});
// ! в хроме, каким-то образом из-за zoom-fade класса начинает прыгать скролл при подгрузке сообщений вверх,
// ! т.е. скролл не ставится, так же, как в сафари при translateZ на блок выше scrollable
// if(!IS_SAFARI) {
// this.needReflowScroll = true;
// }
});
}
return promise;
}
private async renderEmptyPlaceholder(
type: 'group' | 'saved' | 'noMessages' | 'noScheduledMessages' | 'greeting' | 'restricted',
bubble: HTMLElement,
message: any,
elements: (Node | string)[]
) {
const BASE_CLASS = 'empty-bubble-placeholder';
bubble.classList.add(BASE_CLASS, BASE_CLASS + '-' + type);
let title: HTMLElement;
if(type === 'group') title = i18n('GroupEmptyTitle1');
else if(type === 'saved') title = i18n('ChatYourSelfTitle');
else if(type === 'noMessages' || type === 'greeting') title = i18n('NoMessages');
else if(type === 'noScheduledMessages') title = i18n('NoScheduledMessages');
else if(type === 'restricted') {
title = document.createElement('span');
title.innerText = await this.managers.appPeersManager.getRestrictionReasonText(this.peerId);
}
title.classList.add('center', BASE_CLASS + '-title');
elements.push(title);
let listElements: HTMLElement[];
if(type === 'group') {
elements.push(i18n('GroupEmptyTitle2'));
listElements = [
i18n('GroupDescription1'),
i18n('GroupDescription2'),
i18n('GroupDescription3'),
i18n('GroupDescription4')
];
} else if(type === 'saved') {
listElements = [
i18n('ChatYourSelfDescription1'),
i18n('ChatYourSelfDescription2'),
i18n('ChatYourSelfDescription3'),
i18n('ChatYourSelfDescription4')
];
} else if(type === 'greeting') {
const subtitle = i18n('NoMessagesGreetingsDescription');
subtitle.classList.add('center', BASE_CLASS + '-subtitle');
// findAndSplice(this.messagesQueue, q => q.bubble === bubble);
const stickerDiv = document.createElement('div');
stickerDiv.classList.add(BASE_CLASS + '-sticker');
const middleware = this.getMiddleware();
await this.managers.appStickersManager.getGreetingSticker().then(async(doc) => {
if(!middleware()) return;
const loadPromises: Promise<any>[] = [];
await wrapSticker({
doc,
// doc: appDocsManager.getDoc("5431607541660389336"), // cubigator mockup
div: stickerDiv,
middleware,
lazyLoadQueue: this.lazyLoadQueue,
group: this.chat.animationGroup,
// play: !!message.pending || !multipleRender,
play: true,
loop: true,
withThumb: true,
loadPromises,
liteModeKey: 'stickers_chat'
});
attachClickEvent(stickerDiv, (e) => {
cancelEvent(e);
EmoticonsDropdown.onMediaClick({target: e.target});
});
return Promise.all(loadPromises);
});
// this.renderMessagesQueue({
// message,
// bubble,
// reverse: false,
// promises: [loadPromise]
// });
elements.push(subtitle, stickerDiv);
}
if(listElements) {
elements.push(
...listElements.map((elem) => {
const span = document.createElement('span');
span.classList.add(BASE_CLASS + '-list-item');
span.append(elem);
return span;
})
);
if(type === 'group') {
listElements.forEach((elem) => {
const i = document.createElement('span');
i.classList.add('tgico-check');
elem.prepend(i);
});
} else if(type === 'saved') {
listElements.forEach((elem) => {
const i = document.createElement('span');
i.classList.add(BASE_CLASS + '-list-bullet');
i.innerText = '•';
elem.prepend(i);
});
}
}
if(elements.length > 1) {
bubble.classList.add('has-description');
}
elements.forEach((element: any) => element.classList.add(BASE_CLASS + '-line'));
}
private async processLocalMessageRender(
message: Message.message | Message.messageService,
animate?: boolean,
middleware = this.getMiddleware()
) {
const isSponsored = !!(message as Message.message).pFlags.sponsored;
const m = middlewarePromise(middleware);
const p: Parameters<ChatBubbles['safeRenderMessage']>[4] = async(result) => {
const {bubble} = await m(result);
if(!bubble) {
return result;
}
(bubble as any).message = message;
bubble.classList.add('is-group-last', 'is-group-first');
const updatePosition = () => {
if(this.updatePlaceholderPosition === updatePosition) {
this.updatePlaceholderPosition = undefined;
}
appendTo[method](bubble);
};
if(!isSponsored) {
bubble.classList.add('bubble-first');
bubble.classList.remove('can-have-tail', 'is-in');
}
const elements: (Node | string)[] = [];
const isBot = this.chat.isBot;
let renderPromise: Promise<any>, appendTo = this.container, method: 'append' | 'prepend' = 'append';
if(this.chat.isRestricted) {
renderPromise = this.renderEmptyPlaceholder('restricted', bubble, message, elements);
} else if(isSponsored) {
let text: LangPackKey, mid: number, startParam: string, callback: () => void;
bubble.classList.add('avoid-selection');
bubble.style.order = '999999';
const sponsoredMessage = this.sponsoredMessage = (message as Message.message).sponsoredMessage;
const peerId = getPeerId(sponsoredMessage.from_id);
// const peer = this.appPeersManager.getPeer(peerId);
if(sponsoredMessage.channel_post) {
text = 'OpenChannelPost';
mid = generateMessageId(sponsoredMessage.channel_post);
} else if(sponsoredMessage.start_param || isBot) {
text = 'Chat.Message.ViewBot';
startParam = sponsoredMessage.start_param;
} else {
text = await this.managers.appPeersManager.isAnyGroup(peerId) ? 'Chat.Message.ViewGroup' : 'Chat.Message.ViewChannel';
}
if(sponsoredMessage.chat_invite) {
callback = () => {
PopupElement.createPopup(PopupJoinChatInvite, sponsoredMessage.chat_invite_hash, sponsoredMessage.chat_invite as ChatInvite.chatInvite);
};
} else if(sponsoredMessage.chat_invite_hash) {
callback = () => {
const link: InternalLink = {
_: INTERNAL_LINK_TYPE.JOIN_CHAT,
invite: sponsoredMessage.chat_invite_hash
};
this.chat.appImManager.processInternalLink(link);
};
} else {
callback = () => {
this.chat.appImManager.setInnerPeer({
peerId,
lastMsgId: mid,
startParam
});
};
}
const button = this.makeViewButton({text});
this.observer.observe(button, this.viewsObserverCallback);
if(callback) {
attachClickEvent(button, callback);
}
bubble.querySelector('.bubble-content').prepend(button);
appendTo = this.chatInner;
method = 'append';
animate = false;
// return result;
} else if(isBot && message._ === 'message') {
const b = document.createElement('b');
b.append(i18n('BotInfoTitle'));
elements.push(b, '\n\n');
appendTo = this.chatInner;
method = 'prepend';
} else if(this.chat.isAnyGroup && ((await m(this.managers.appPeersManager.getPeer(this.peerId))) as MTChat.chat).pFlags.creator) {
renderPromise = this.renderEmptyPlaceholder('group', bubble, message, elements);
} else if(this.chat.type === 'scheduled') {
renderPromise = this.renderEmptyPlaceholder('noScheduledMessages', bubble, message, elements);
} else if(rootScope.myId === this.peerId) {
renderPromise = this.renderEmptyPlaceholder('saved', bubble, message, elements);
} else if(this.peerId.isUser() && !isBot && await m(this.chat.canSend()) && this.chat.type === 'chat') {
renderPromise = this.renderEmptyPlaceholder('greeting', bubble, message, elements);
} else {
renderPromise = this.renderEmptyPlaceholder('noMessages', bubble, message, elements);
}
if(renderPromise) {
await renderPromise;
}
if(elements.length) {
const messageDiv = bubble.querySelector('.message, .service-msg');
messageDiv.prepend(...elements);
}
const isWaitingForAnimation = !!this.messagesQueueOnRenderAdditional;
const noTransition = this.setPeerCached && !isWaitingForAnimation;
if(noTransition) {
const setOn = bubble.firstElementChild;
setOn.classList.add('no-transition');
if(this.chat.setPeerPromise) {
this.chat.setPeerPromise.catch(noop).finally(() => {
setOn.classList.remove('no-transition');
});
}
}
if(animate === undefined && !noTransition) {
animate = true;
}
if(isWaitingForAnimation || animate) {
this.updatePlaceholderPosition = updatePosition;
this.onAnimateLadder = () => {
// appendTo[method](bubble);
this.onAnimateLadder = undefined;
// need raf here because animation won't fire if this message is single
if(!this.messagesQueuePromise) {
return fastRafPromise();
}
};
} else if(this.chat.setPeerPromise) {
this.attachPlaceholderOnRender = () => {
this.attachPlaceholderOnRender = undefined;
updatePosition();
// appendTo[method](bubble);
};
} else {
this.updatePlaceholderPosition = updatePosition;
// appendTo[method](bubble);
}
if(!isWaitingForAnimation && animate) {
await m(getHeavyAnimationPromise());
const additionMsgIds = getObjectKeysAndSort(this.bubbles);
indexOfAndSplice(additionMsgIds, message.mid);
this.animateAsLadder(message.mid, additionMsgIds, false, 0, 0);
}
bubble.middlewareHelper.onDestroy(() => {
if(this.emptyPlaceholderBubble === bubble) {
this.emptyPlaceholderBubble = undefined;
}
});
this.emptyPlaceholderBubble = bubble;
return result;
};
return this.safeRenderMessage(
message,
!isSponsored,
undefined,
false,
p
);
}
private makeViewButton<T extends Parameters<typeof Button>[1]>(options: T) {
return Button('btn-primary btn-primary-transparent bubble-view-button', options);
}
private generateLocalMessageId(addOffset = 0) {
// const INCREMENT = 0x10;
const offset = (this.chat.type === 'scheduled' ? -1 : 0) + addOffset;
// offset = generateMessageId(offset);
// id: -Math.abs(+this.peerId * INCREMENT + offset),
const id = -Math.abs(offset);
const mid = -Math.abs(generateMessageId(id));
return {id, mid};
}
private async generateLocalFirstMessage<T extends boolean>(service?: T, fill?: (message: GenerateLocalMessageType<T>) => void, addOffset = 0): Promise<GenerateLocalMessageType<T>> {
const {id, mid} = this.generateLocalMessageId(addOffset);
let message: Omit<Message.message | Message.messageService, 'message'> & {message?: string} = {
_: service ? 'messageService' : 'message',
date: 0,
id,
mid,
peer_id: await this.managers.appPeersManager.getOutputPeer(this.peerId),
pFlags: {
local: true
}
};
if(!service) {
message.message = '';
}/* else {
(message as Message.messageService).action = {} as any;
} */
assumeType<GenerateLocalMessageType<T>>(message);
fill && fill(message);
const savedMessages = await this.managers.appMessagesManager.saveMessages([message], {storage: new Map() as any});
message = savedMessages[0];
message.mid = mid;
return message as any;
}
public getViewportSlice() {
// this.log.trace('viewport slice');
return getViewportSlice({
overflowElement: this.scrollable.container,
selector: '.bubbles-date-group .bubble:not(.is-date)',
extraSize: Math.max(700, windowSize.height) * 2
});
}
public deleteViewportSlice(slice: ReturnType<ChatBubbles['getViewportSlice']>, ignoreScrollSaving?: boolean) {
if(DO_NOT_SLICE_VIEWPORT_ON_RENDER) {
return;
}
const {invisibleTop, invisibleBottom} = slice;
const invisible = invisibleTop.concat(invisibleBottom);
if(!invisible.length) {
return;
}
if(invisibleTop.length) {
this.setLoaded('top', false);
this.getHistoryTopPromise = undefined;
}
if(invisibleBottom.length) {
this.setLoaded('bottom', false);
this.getHistoryBottomPromise = undefined;
}
const mids = invisible.map(({element}) => +element.dataset.mid);
let scrollSaver: ScrollSaver;
if(/* !!invisibleTop.length !== !!invisibleBottom.length && */!ignoreScrollSaving) {
scrollSaver = this.createScrollSaver(!!invisibleTop.length);
scrollSaver.save();
}
this.deleteMessagesByIds(mids, false, true);
if(scrollSaver) {
scrollSaver.restore();
} else if(invisibleTop.length) {
this.scrollable.lastScrollPosition = this.scrollable.scrollTop;
}
}
public sliceViewport(ignoreHeavyAnimation?: boolean) {
// Safari cannot reset the scroll.
if(IS_SAFARI || (this.isHeavyAnimationInProgress && !ignoreHeavyAnimation) || DO_NOT_SLICE_VIEWPORT) {
return;
}
// const scrollSaver = new ScrollSaver(this.scrollable, true);
// scrollSaver.save();
const slice = this.getViewportSlice();
// if(IS_SAFARI) slice.invisibleTop = [];
this.deleteViewportSlice(slice);
// scrollSaver.restore();
}
private async setLoaded(side: SliceSides, value: boolean, checkPlaceholders = true) {
const willChange = this.scrollable.loadedAll[side] !== value;
if(!willChange) {
return;
}
const log = this.log.bindPrefix('setLoaded');
log('change', side, value);
this.scrollable.loadedAll[side] = value;
this.scrollable.onScroll(); // ! WARNING
// return;
if(!checkPlaceholders) {
return;
}
if(!this.chat.isRestricted) {
if(side === 'bottom' && this.chat.isBroadcast/* && false */) {
this.toggleSponsoredMessage(value);
}
if(side === 'top' && value && this.chat.isBot) {
return this.renderBotPlaceholder();
}
}
return this.checkIfEmptyPlaceholderNeeded();
}
private async toggleSponsoredMessage(value: boolean) {
const log = this.log.bindPrefix('sponsored');
log('checking');
const {mid} = this.generateLocalMessageId(SPONSORED_MESSAGE_ID_OFFSET);
if(value) {
const middleware = this.getMiddleware(() => {
return this.scrollable.loadedAll.bottom && this.getSponsoredMessagePromise === promise;
});
const promise = this.getSponsoredMessagePromise = this.managers.appChatsManager.getSponsoredMessage(this.peerId.toChatId())
.then(async(sponsoredMessages) => {
if(!middleware() || sponsoredMessages._ === 'messages.sponsoredMessagesEmpty') {
return;
}
const sponsoredMessage = sponsoredMessages.messages[0];
if(!sponsoredMessage) {
log('no message');
return;
}
const messagePromise = this.generateLocalFirstMessage(false, (message) => {
message.message = sponsoredMessage.message;
message.from_id = sponsoredMessage.from_id;
message.entities = sponsoredMessage.entities;
message.pFlags.sponsored = true;
message.sponsoredMessage = sponsoredMessage;
}, SPONSORED_MESSAGE_ID_OFFSET);
return Promise.all([
messagePromise,
this.getHistoryTopPromise, // wait for top load and execute rendering after or with it
this.messagesQueuePromise
]).then(([message]) => {
if(!middleware()) return;
// this.processLocalMessageRender(message);
log('rendering', message);
return this.performHistoryResult({history: [message]}, false);
});
}).finally(() => {
if(this.getSponsoredMessagePromise === promise) {
this.getSponsoredMessagePromise = undefined;
}
});
} else {
log('clearing rendered', mid);
this.getSponsoredMessagePromise = undefined;
this.deleteMessagesByIds([mid]);
}
}
private async renderBotPlaceholder() {
const _log = this.log.bindPrefix('bot placeholder');
const middleware = this.getMiddleware();
const result = await this.managers.acknowledged.appProfileManager.getProfile(this.peerId.toUserId());
_log('getting profile, cached:', result.cached);
const processPromise = result.result.then(async(userFull) => {
if(!middleware()) {
return;
}
if(!userFull.bot_info?.description) {
_log.warn('no description');
return this.checkIfEmptyPlaceholderNeeded();
}
const message = await this.generateLocalFirstMessage(false, (message) => {
const botInfo = userFull.bot_info;
message.message = botInfo.description;
if(botInfo.description_document) message.media = {_: 'messageMediaDocument', document: botInfo.description_document, pFlags: {}};
if(botInfo.description_photo) message.media = {_: 'messageMediaPhoto', photo: botInfo.description_photo, pFlags: {}};
});
if(!middleware()) {
return;
}
_log('rendering');
const renderPromise = this.processLocalMessageRender(message, !result.cached).then(() => {
_log('done');
});
return {renderPromise};
});
if(!result.cached) {
return;
}
return processPromise;
}
public async checkIfEmptyPlaceholderNeeded() {
if(this.scrollable.loadedAll.top &&
this.scrollable.loadedAll.bottom &&
this.emptyPlaceholderBubble === undefined &&
(
this.chat.isRestricted ||
!(await this.chat.getHistoryStorage()).count ||
(
// !Object.keys(this.bubbles).length ||
// ! WARNING ! ! ! ! ! ! REPLACE LINE ABOVE WITH THESE
Object.keys(this.bubbles).length &&
!this.getRenderedLength()
) ||
(this.chat.type === 'scheduled' && !Object.keys(this.bubbles).length)
)
) {
this.log('inject empty peer placeholder');
const message = await this.generateLocalFirstMessage(true);
return {renderPromise: this.processLocalMessageRender(message)};
}
}
public getHistory1(maxId?: number, reverse?: boolean, isBackLimit?: boolean, additionMsgId?: number, justLoad?: boolean) {
const middleware = this.getMiddleware(justLoad ? undefined : () => {
return (reverse ? this.getHistoryTopPromise : this.getHistoryBottomPromise) === waitPromise;
});
const result = this.getHistory(maxId, reverse, isBackLimit, additionMsgId, justLoad, middleware);
const waitPromise = result.then((res) => res && (res.waitPromise || res.promise));
(reverse ? this.getHistoryTopPromise = waitPromise : this.getHistoryBottomPromise = waitPromise);
waitPromise.then(() => {
if(!middleware()) {
return;
}
(reverse ? this.getHistoryTopPromise = undefined : this.getHistoryBottomPromise = undefined);
if(!justLoad) {
// preload more
// if(!isFirstMessageRender) {
if(this.chat.type === 'chat'/* || this.chat.type === 'discussion' */) {
/* const storage = this.appMessagesManager.getHistoryStorage(peerId, this.chat.threadId);
const isMaxIdInHistory = storage.history.indexOf(maxId) !== -1;
if(isMaxIdInHistory || true) { // * otherwise it is a search or jump */
setTimeout(() => {
if(reverse) {
this.loadMoreHistory(true, true);
} else {
this.loadMoreHistory(false, true);
}
}, 0);
// }
}
// }
// this.scrollable.onScroll();
}
});
return result;
}
// private async getDiscussionMessages() {
// const mids = await this.chat.getMidsByMid(this.chat.threadId);
// return Promise.all(mids.map((mid) => this.chat.getMessage(mid)));
// }
/**
* Load and render history
* @param maxId max message id
* @param reverse 'true' means up
* @param isBackLimit is search
* @param additionMsgId for the last message
* @param justLoad do not render
*/
public async getHistory(
maxId = 0,
reverse = false,
isBackLimit = false,
additionMsgId = 0,
justLoad = false,
middleware?: () => boolean
): Promise<{cached: boolean, promise: Promise<void>, waitPromise: Promise<any>}> {
const peerId = this.peerId;
const isBroadcast = this.chat.isBroadcast;
// console.time('appImManager call getHistory');
const pageCount = Math.min(30, windowSize.height / 40/* * 1.25 */ | 0);
// const loadCount = Object.keys(this.bubbles).length > 0 ? 50 : pageCount;
const realLoadCount = isBroadcast ? 20 : (Object.keys(this.bubbles).length > 0 ? Math.max(35, pageCount) : pageCount);
// const realLoadCount = pageCount;//const realLoadCount = 50;
let loadCount = realLoadCount;
/* if(TEST_SCROLL) {
//loadCount = 1;
if(Object.keys(this.bubbles).length > 0)
return {cached: false, promise: Promise.resolve(true)};
} */
if(TEST_SCROLL !== undefined) {
if(TEST_SCROLL) {
if(Object.keys(this.bubbles).length > 0) {
--TEST_SCROLL;
}
} else {
return {cached: false, promise: Promise.resolve(), waitPromise: Promise.resolve()};
}
}
// //console.time('render history total');
let backLimit = 0;
if(isBackLimit) {
backLimit = loadCount;
if(!reverse) { // if not jump
loadCount = 0;
// maxId = this.appMessagesManager.incrementMessageId(maxId, 1);
}
}
let additionMsgIds: number[];
if(additionMsgId && !isBackLimit) {
if(this.chat.type === 'pinned') {
additionMsgIds = [additionMsgId];
} else {
const historyStorage = await this.chat.getHistoryStorage();
const slice = historyStorage.history.slice;
if(slice.length < loadCount && !slice.isEnd(SliceEnd.Both)) {
additionMsgIds = slice.slice();
// * filter last album, because we don't know is it the last item
for(let i = additionMsgIds.length - 1; i >= 0; --i) {
const message = await this.chat.getMessage(additionMsgIds[i]);
if((message as Message.message)?.grouped_id) additionMsgIds.splice(i, 1);
else break;
}
maxId = additionMsgIds[additionMsgIds.length - 1] || maxId;
}
}
}
/* const result = additionMsgID ?
{history: [additionMsgID]} :
appMessagesManager.getHistory(this.peerId, maxId, loadCount, backLimit); */
let result: AckedResult<MyHistoryResult> = await this.requestHistory(maxId, loadCount, backLimit) as any;
let resultPromise: typeof result['result'];
this.log('i vin brehnya', result, maxId, loadCount, backLimit);
// const isFirstMessageRender = !!additionMsgID && result.cached && !appMessagesManager.getMessage(additionMsgID).grouped_id;
const isAdditionRender = additionMsgIds?.length && !result.cached;
const isFirstMessageRender = (this.isFirstLoad && backLimit && !result.cached) || isAdditionRender;
if(isAdditionRender) {
resultPromise = result.result;
result = {
cached: true,
result: Promise.resolve({history: additionMsgIds})
};
// additionMsgID = 0;
}
this.isFirstLoad = false;
const processResult = async(historyResult: Awaited<typeof result['result']>) => {
if((historyResult as HistoryResult).isEnd?.top) {
if(this.chat.type === 'discussion') { // * inject discussion start
const serviceStartMessageId = await this.managers.appMessagesManager.getThreadServiceMessageId(this.peerId, this.chat.threadId);
if(serviceStartMessageId) historyResult.history.push(serviceStartMessageId);
const mids = await this.chat.getMidsByMid(this.chat.threadId);
historyResult.history.push(...mids.reverse());
}
// synchronize bot placeholder appearance
await this.managers.appProfileManager.getProfileByPeerId(peerId);
// await this.setLoaded('top', true);
}
};
const sup = (historyResult: Awaited<typeof result['result']>) => {
return getHeavyAnimationPromise().then(() => {
return processResult(historyResult);
}).then(() => {
if(!isAdditionRender && additionMsgId) {
historyResult.history.unshift(additionMsgId);
}
return this.performHistoryResult(historyResult, reverse);
});
};
const processPromise = (_promise: typeof result['result']) => {
const promise = Promise.resolve(_promise).then((result) => {
if(middleware && !middleware()) {
throw PEER_CHANGED_ERROR;
}
if(justLoad) {
// нужно делать из-за ранней прогрузки
this.scrollable.onScroll();
// fastRaf(() => {
// this.scrollable.checkForTriggers();
// });
return;
}
return sup(result);
}, (err) => {
this.log.error('getHistory error:', err);
throw err;
});
return promise;
};
let promise: Promise<void>, cached: boolean;
if(!result.cached) {
cached = false;
promise = processPromise(result.result);
} else if(justLoad) {
// нужно делать из-за ранней прогрузки
this.scrollable.onScroll();
return null;
} else {
cached = true;
promise = sup(await result.result);
}
const waitPromise = isAdditionRender ? processPromise(resultPromise) : promise;
if(isFirstMessageRender && liteMode.isAvailable('animations')/* && false */) {
let times = isAdditionRender ? 2 : 1;
this.messagesQueueOnRenderAdditional = () => {
this.log('messagesQueueOnRenderAdditional');
if(--times) return;
this.messagesQueueOnRenderAdditional = undefined;
const promise = this.animateAsLadder(additionMsgId, additionMsgIds, isAdditionRender, backLimit, maxId);
promise.then(() => {
setTimeout(() => { // preload messages
this.loadMoreHistory(reverse, true);
}, 0);
});
};
} else {
this.messagesQueueOnRenderAdditional = undefined;
}
if(justLoad) {
return null;
}
return {cached, promise, waitPromise};
}
public async setUnreadDelimiter() {
if(!(this.chat.type === 'chat' || this.chat.type === 'discussion')) {
return;
}
if(this.attachedUnreadBubble) {
return;
}
const historyMaxId = await this.chat.getHistoryMaxId();
let readMaxId = await this.managers.appMessagesManager.getReadMaxIdIfUnread(this.peerId, this.chat.threadId);
if(!readMaxId) return;
readMaxId = Object.keys(this.bubbles)
.filter((mid) => !this.bubbles[mid].classList.contains('is-out'))
.map((i) => +i)
.sort((a, b) => a - b)
.find((i) => i > readMaxId);
if(readMaxId && this.bubbles[readMaxId]) {
const bubble = this.bubbles[readMaxId];
if(this.firstUnreadBubble && this.firstUnreadBubble !== bubble) {
this.firstUnreadBubble.classList.remove('is-first-unread');
this.firstUnreadBubble = null;
}
if(readMaxId !== historyMaxId) {
bubble.classList.add('is-first-unread');
}
this.firstUnreadBubble = bubble;
this.attachedUnreadBubble = true;
}
}
public deleteEmptyDateGroups() {
const mustBeCount = this.stickyIntersector ? STICKY_OFFSET : 1;
let deleted = false;
for(const i in this.dateMessages) {
const dateMessage = this.dateMessages[i];
if(dateMessage.container.childElementCount === mustBeCount) { // only date div + sentinel div
dateMessage.container.remove();
if(this.stickyIntersector) {
this.stickyIntersector.unobserve(dateMessage.container, dateMessage.div);
}
delete this.dateMessages[i];
deleted = true;
// * no sense in it
/* if(dateMessage.div === this.previousStickyDate) {
this.previousStickyDate = undefined;
} */
}
}
if(!deleted) {
return;
}
if(!Object.keys(this.dateMessages).length) {
this.container.classList.remove('has-groups');
}
this.checkIfEmptyPlaceholderNeeded();
this.setStickyDateManually();
}
}
export function generateTail() {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttributeNS(null, 'viewBox', '0 0 11 20');
svg.setAttributeNS(null, 'width', '11');
svg.setAttributeNS(null, 'height', '20');
svg.classList.add('bubble-tail');
const use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
use.setAttributeNS(null, 'href', '#message-tail-filled');
svg.append(use);
return svg;
}