/* * https://github.com/morethanwords/tweb * Copyright (C) 2019-2021 Eduard Kuzmenko * https://github.com/morethanwords/tweb/blob/master/LICENSE */ import rootScope from "../../lib/rootScope"; //import { generatePathData } from "../../helpers/dom"; import { MyMessage } from "../../lib/appManagers/appMessagesManager"; import type Chat from "./chat"; import indexOfAndSplice from "../../helpers/array/indexOfAndSplice"; import insertInDescendSortedArray from "../../helpers/array/insertInDescendSortedArray"; import positionElementByIndex from "../../helpers/dom/positionElementByIndex"; import AvatarElement from "../avatar"; import { Message } from "../../layer"; import { NULL_PEER_ID, REPLIES_PEER_ID } from "../../lib/mtproto/mtproto_config"; import { SERVICE_AS_REGULAR, STICKY_OFFSET } from "./bubbles"; import forEachReverse from "../../helpers/array/forEachReverse"; type GroupItem = { bubble: HTMLElement, fromId: PeerId, mid: number, timestamp: number, dateTimestamp: number, mounted: boolean, single: boolean, group?: BubbleGroup }; class BubbleGroup { container: HTMLElement; chat: Chat; groups: BubbleGroups; items: GroupItem[]; // descend sorted avatarContainer: HTMLElement; avatarLoadPromise: ReturnType; avatar: AvatarElement; mounted: boolean; dateTimestamp: number; constructor(chat: Chat, groups: BubbleGroups, dateTimestamp: number) { this.container = document.createElement('div'); this.container.classList.add('bubbles-group'); this.chat = chat; this.groups = groups; this.items = []; this.dateTimestamp = dateTimestamp; } createAvatar(message: Message.message | Message.messageService) { if(this.avatarLoadPromise) { return this.avatarLoadPromise; } else if(message._ === 'messageService') { return; } this.avatarContainer = document.createElement('div'); this.avatarContainer.classList.add('bubbles-group-avatar-container'); const fwdFrom = message.fwd_from; const fwdFromId = message.fwdFromId; const isForwardFromChannel = message.from_id && message.from_id._ === 'peerChannel' && message.fromId === fwdFromId; const currentPeerId = this.chat.peerId; this.avatar = new AvatarElement(); this.avatar.classList.add('bubbles-group-avatar', 'user-avatar', 'avatar-40'/* , 'can-zoom-fade' */); this.avatarLoadPromise = this.avatar.updateWithOptions({ lazyLoadQueue: this.chat.bubbles.lazyLoadQueue, peerId: ((fwdFrom && (currentPeerId === rootScope.myId || currentPeerId === REPLIES_PEER_ID)) || isForwardFromChannel ? fwdFromId : message.fromId) || NULL_PEER_ID, peerTitle: !fwdFromId && fwdFrom && fwdFrom.from_name ? /* '🔥 FF 🔥' */fwdFrom.from_name : undefined, }); this.avatarContainer.append(this.avatar); this.container.append(this.avatarContainer); return this.avatarLoadPromise; } get firstTimestamp() { return this.firstItem.timestamp; } get firstMid() { return this.firstItem.mid; } get firstItem() { return this.items[this.items.length - 1]; } get lastItem() { return this.items[0]; } updateClassNames() { const items = this.items; if(!items.length) { return; } const length = items.length; const first = items[length - 1].bubble; if(items.length === 1) { first.classList.add('is-group-first', 'is-group-last'); //this.setClipIfNeeded(first); return; } else { first.classList.remove('is-group-last'); first.classList.add('is-group-first'); //this.setClipIfNeeded(first, true); } for(let i = length - 2; i > 0; --i) { const bubble = items[i].bubble; bubble.classList.remove('is-group-last', 'is-group-first'); //this.setClipIfNeeded(bubble, true); } const last = items[0].bubble; last.classList.remove('is-group-first'); last.classList.add('is-group-last'); //this.setClipIfNeeded(last); } insertItem(item: GroupItem) { const {items} = this; const {timestamp, mid} = item; if(this.chat.type === 'scheduled') { let foundMidOnSameTimestamp = 0; let i = 0, length = items.length; for(; i < length; ++i) { const {timestamp: _timestamp, mid: _mid} = items[i]; if(timestamp < _timestamp) { break; } else if(timestamp === _timestamp) { foundMidOnSameTimestamp = _mid; } if(foundMidOnSameTimestamp && mid < foundMidOnSameTimestamp) { break; } } items.splice(i, 0, item); } else { insertInDescendSortedArray(items, item, 'mid'); } if(items.length === 1) { insertInDescendSortedArray(this.groups.groups, this, 'firstMid'); } } mount() { if(!this.groups.groups.includes(this)) { // group can be already removed return; } const offset = this.avatar ? 1 : 0; const items = this.items; this.updateClassNames(); const {length} = items; forEachReverse(items, (item, idx) => { this.mountItem(item, length - 1 - idx, offset); }); this.onItemMount(); } mountItem(item: GroupItem, idx = this.items.indexOf(item), offset = this.avatar ? 1 : 0) { if(item.mounted) { return; } positionElementByIndex(item.bubble, this.container, offset + idx); item.mounted = true; } unmountItem(item: GroupItem) { if(item.mounted) { item.bubble.remove(); item.mounted = false; this.onItemUnmount(); } } onItemMount() { if(this.mounted) { return; } const dateContainer = this.chat.bubbles.getDateContainerByTimestamp(this.firstTimestamp); // const idx = this.groups.indexOf(group); const dateGroups = this.groups.groups.filter((_group) => _group.dateTimestamp === this.dateTimestamp); const dateGroupsLength = dateGroups.length; const idx = dateGroups.indexOf(this); const unmountedLength = dateGroups.slice(idx + 1).reduce((acc, v) => acc + (v.mounted ? 0 : 1), 0); positionElementByIndex(this.container, dateContainer.container, STICKY_OFFSET + dateGroupsLength - 1 - idx - unmountedLength); this.mounted = true; } onItemUnmount() { if(!this.mounted) { return; } if(!this.items.length) { this.container.remove(); this.chat.bubbles.deleteEmptyDateGroups(); this.mounted = false; } else { this.updateClassNames(); } } } // class BubbleGroupItem implements GroupItem { // bubble: HTMLElement; // fromId: PeerId; // mid: number; // timestamp: number; // dateTimestamp: number; // mounted: boolean; // single: boolean; // group: BubbleGroup; // constructor(details: GroupItem) { // Object.assign(this, details); // } // } export default class BubbleGroups { private itemsArr: Array = []; // descend sorted private itemsMap: Map = new Map(); public groups: Array = []; // descend sorted private newGroupDiff = 121; // * 121 in scheduled messages constructor(private chat: Chat) { } removeBubble(bubble: HTMLElement) { const item = this.getItemByBubble(bubble); if(!item) { return; } const group = item.group; const items = group.items; if(items.length) { indexOfAndSplice(items, item); if(!items.length) { indexOfAndSplice(this.groups, group); } } indexOfAndSplice(this.itemsArr, item); this.itemsMap.delete(bubble); return item; } removeAndUnmountBubble(bubble: HTMLElement) { const item = this.removeBubble(bubble); if(item) { item.group.unmountItem(item); } } getItemByBubble(bubble: HTMLElement) { return this.itemsMap.get(bubble); } getLastGroup() { return this.groups[0]; } // changeBubbleMid(bubble: HTMLElement, mid: number) { // const item = this.getItemByBubble(bubble); // if(!item) { // return; // } // item.mid = mid; // // indexOfAndSplice(item.group.items, item); // // item.group.insertItem(item); // indexOfAndSplice(this.itemsArr, item); // insertInDescendSortedArray(this.itemsArr, item, 'mid'); // } changeItemBubble(item: GroupItem, bubble: HTMLElement) { this.itemsMap.delete(item.bubble); item.bubble = bubble; this.itemsMap.set(bubble, item); } changeBubbleByBubble(from: HTMLElement, to: HTMLElement) { const item = this.getItemByBubble(from); if(!item) { return; } this.changeItemBubble(item, to); } /** * * @param item * @param items expect descend sorted array * @returns */ findIndexForItemInItems(item: GroupItem, items: GroupItem[]) { let foundAtIndex = -1; for(let i = 0, length = items.length; i < length; ++i) { const _item = items[i]; const diff = Math.abs(_item.timestamp - item.timestamp); const good = _item.fromId === item.fromId && diff <= this.newGroupDiff && item.dateTimestamp === _item.dateTimestamp && !item.single && !_item.single; if(good) { foundAtIndex = i; if(this.chat.type === 'scheduled') { break; } } else { foundAtIndex = -1; } if(this.chat.type !== 'scheduled') { if(item.mid > _item.mid) { break; } } } return foundAtIndex; } addItemToGroup(item: GroupItem, group: BubbleGroup) { item.group = group; group.insertItem(item); this.addItemToCache(item); } addItemToCache(item: GroupItem) { insertInDescendSortedArray(this.itemsArr, item, 'mid'); this.itemsMap.set(item.bubble, item); } getMessageFromId(message: MyMessage) { let fromId = message.viaBotId || message.fromId; // fix for saved messages forward to self if(fromId === rootScope.myId && message.peerId === rootScope.myId && (message as Message.message).fwdFromId === fromId) { fromId = fromId.toPeerId(true); } return fromId; } createItem(bubble: HTMLElement, message: MyMessage) { const single = !(message._ === 'message' || (message.action && SERVICE_AS_REGULAR.has(message.action._))); const {mid, date: timestamp} = message; const {dateTimestamp} = this.chat.bubbles.getDateForDateContainer(timestamp); const item: GroupItem = { bubble, fromId: this.getMessageFromId(message), mid, timestamp, dateTimestamp, mounted: false, single: single }; return item; } // prepareForGrouping(bubble: HTMLElement, message: MyMessage) { // const item = this.createItem(bubble, message); // this.addItemToCache(item); // } // groupUngrouped() { // const items = this.itemsArr; // const length = items.length; // for(let i = length - 1; i >= 0; --i) { // const item = items[i]; // if(item.gr) // } // } addBubble(bubble: HTMLElement, message: MyMessage, unmountIfFound?: boolean) { const oldItem = this.getItemByBubble(bubble); if(unmountIfFound) { // updating position this.removeAndUnmountBubble(bubble); } else if(oldItem) { // editing const group = oldItem.group; this.changeItemBubble(oldItem, bubble); oldItem.mounted = false; return {item: oldItem, group}; } const item = this.createItem(bubble, message); const foundAtIndex = this.findIndexForItemInItems(item, this.itemsArr); const foundItem = this.itemsArr[foundAtIndex]; const group = foundItem?.group ?? new BubbleGroup(this.chat, this, item.dateTimestamp); this.addItemToGroup(item, group); return {item, group}; } /* setClipIfNeeded(bubble: HTMLDivElement, remove = false) { //console.log('setClipIfNeeded', bubble, remove); const className = bubble.className; if(className.includes('is-message-empty') && (className.includes('photo') || className.includes('video'))) { let container = bubble.querySelector('.bubble__media-container') as SVGSVGElement; //console.log('setClipIfNeeded', bubble, remove, container); if(!container) return; try { Array.from(container.children).forEach((object) => { if(object instanceof SVGDefsElement) return; if(remove) { object.removeAttributeNS(null, 'clip-path'); } else { let clipId = container.dataset.clipId; let path = container.firstElementChild.firstElementChild.lastElementChild as SVGPathElement; let width = +object.getAttributeNS(null, 'width'); let height = +object.getAttributeNS(null, 'height'); let isOut = className.includes('is-out'); let isReply = className.includes('is-reply'); let d = ''; //console.log('setClipIfNeeded', object, width, height, isOut); let tr: number, tl: number; if(className.includes('forwarded') || isReply) { tr = tl = 0; } else if(isOut) { tr = className.includes('is-group-first') ? 12 : 6; tl = 12; } else { tr = 12; tl = className.includes('is-group-first') ? 12 : 6; } if(isOut) { d = generatePathData(0, 0, width - 9, height, tl, tr, 0, 12); } else { d = generatePathData(9, 0, width - 9, height, tl, tr, 12, 0); } path.setAttributeNS(null, 'd', d); object.setAttributeNS(null, 'clip-path', 'url(#' + clipId + ')'); } }); } catch(err) {} } } */ // updateGroupByMessageId(mid: number) { // const item = this.itemsArr.find((g) => g.mid === mid); // if(item) { // item.group.updateGroup(); // } // } cleanup() { this.itemsArr = []; this.groups = []; this.itemsMap.clear(); } }