6348 lines
212 KiB
TypeScript
6348 lines
212 KiB
TypeScript
/*
|
||
* 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 {Chat as MTChat, ChatInvite, 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 confirmationPopup from '../confirmationPopup';
|
||
import wrapPeerTitle from '../wrappers/peerTitle';
|
||
import {PopupPeerCheckboxOptions} from '../popups/peer';
|
||
import toggleDisability from '../../helpers/dom/toggleDisability';
|
||
import {copyTextToClipboard} from '../../helpers/clipboard';
|
||
import liteMode from '../../helpers/liteMode';
|
||
|
||
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 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 any).message = message;
|
||
(element as any).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;
|
||
}
|
||
|
||
return {bubble: result.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 = (await this.chat.getMessage(mid)) as Message.message;
|
||
|
||
MessageRender.setReply({
|
||
chat: this.chat,
|
||
bubble,
|
||
message
|
||
});
|
||
});
|
||
});
|
||
});
|
||
|
||
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()
|
||
]);
|
||
|
||
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;
|
||
}
|
||
|
||
new 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);
|
||
new 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) {
|
||
new PopupStickers(doc.stickerSetInput).show();
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
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) {
|
||
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, targets[idx].element, 0, true, targets.slice(0, idx), targets.slice(idx + 1));
|
||
|
||
// appMediaViewer.openMedia(message, target as HTMLImageElement);
|
||
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);
|
||
new 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 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);
|
||
}
|
||
|
||
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
|
||
};
|
||
|
||
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;
|
||
|
||
if(!cached && !samePeer) {
|
||
await m(this.chat.finishPeerChange(isTarget, isJump, lastMsgId, startParam));
|
||
this.scrollable.container.textContent = '';
|
||
// 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(isTarget, isJump, lastMsgId, startParam)); // * костыль
|
||
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
|
||
scrollable.checkForTriggers();
|
||
|
||
// 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};
|
||
}
|
||
|
||
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 middleware = this.getMiddleware();
|
||
const peerId = this.peerId;
|
||
|
||
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?.();
|
||
|
||
// 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.innerHTML = '';
|
||
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 {
|
||
s.append(await wrapMessageActionTextNew({
|
||
message,
|
||
...wrapOptions
|
||
}));
|
||
}
|
||
}
|
||
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[];
|
||
if(isMessage) {
|
||
if(groupedId && albumMustBeRenderedFull) {
|
||
const t = 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;
|
||
|
||
const richText = wrapRichText(messageMessage, {
|
||
entities: totalEntities,
|
||
passEntities: this.passEntities,
|
||
loadPromises,
|
||
lazyLoadQueue: this.lazyLoadQueue,
|
||
customEmojiSize,
|
||
middleware,
|
||
animationGroup: this.chat.animationGroup
|
||
});
|
||
|
||
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 = new 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: richText,
|
||
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) {
|
||
// /////this.log('message to render hidden', message);
|
||
title = document.createElement('span');
|
||
setInnerHTML(title, wrapEmojiText(fwdFrom.from_name));
|
||
title.classList.add('peer-title');
|
||
// title = fwdFrom.from_name;
|
||
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(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 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 = () => {
|
||
new 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;
|
||
}
|
||
|
||
/**
|
||
* 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;
|
||
}
|