Telegram Web K with changes to work inside I2P https://web.telegram.i2p/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

484 lines
14 KiB

/*
* 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<AvatarElement['updateWithOptions']>;
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 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, 'firstTimestamp');
}
}
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<GroupItem> = []; // descend sorted
private itemsMap: Map<HTMLElement, GroupItem> = new Map();
public groups: Array<BubbleGroup> = []; // 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();
}
}