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
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(); |
|
} |
|
}
|
|
|