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.
609 lines
19 KiB
609 lines
19 KiB
/* |
|
* https://github.com/morethanwords/tweb |
|
* Copyright (C) 2019-2021 Eduard Kuzmenko |
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE |
|
*/ |
|
|
|
import type { ChatRights } from "../../lib/appManagers/appChatsManager"; |
|
import type { AppImManager } from "../../lib/appManagers/appImManager"; |
|
import type { MessagesStorageKey } from "../../lib/appManagers/appMessagesManager"; |
|
import EventListenerBase from "../../helpers/eventListenerBase"; |
|
import { logger, LogTypes } from "../../lib/logger"; |
|
import rootScope from "../../lib/rootScope"; |
|
import appSidebarRight from "../sidebarRight"; |
|
import ChatBubbles from "./bubbles"; |
|
import ChatContextMenu from "./contextMenu"; |
|
import ChatInput from "./input"; |
|
import ChatSelection from "./selection"; |
|
import ChatTopbar from "./topbar"; |
|
import { NULL_PEER_ID, REPLIES_PEER_ID } from "../../lib/mtproto/mtproto_config"; |
|
import SetTransition from "../singleTransition"; |
|
import AppPrivateSearchTab from "../sidebarRight/tabs/search"; |
|
import renderImageFromUrl from "../../helpers/dom/renderImageFromUrl"; |
|
import mediaSizes from "../../helpers/mediaSizes"; |
|
import ChatSearch from "./search"; |
|
import IS_TOUCH_SUPPORTED from "../../environment/touchSupport"; |
|
import getAutoDownloadSettingsByPeerId, { ChatAutoDownloadSettings } from "../../helpers/autoDownload"; |
|
import ChatBackgroundGradientRenderer from "./gradientRenderer"; |
|
import ChatBackgroundPatternRenderer from "./patternRenderer"; |
|
import pause from "../../helpers/schedulers/pause"; |
|
import { AppManagers } from "../../lib/appManagers/managers"; |
|
import SlicedArray from "../../helpers/slicedArray"; |
|
import themeController from "../../helpers/themeController"; |
|
import AppSharedMediaTab from "../sidebarRight/tabs/sharedMedia"; |
|
import noop from "../../helpers/noop"; |
|
import middlewarePromise from "../../helpers/middlewarePromise"; |
|
import indexOfAndSplice from "../../helpers/array/indexOfAndSplice"; |
|
import { Message } from "../../layer"; |
|
|
|
export type ChatType = 'chat' | 'pinned' | 'replies' | 'discussion' | 'scheduled'; |
|
|
|
export default class Chat extends EventListenerBase<{ |
|
setPeer: (mid: number, isTopMessage: boolean) => void |
|
}> { |
|
public container: HTMLElement; |
|
public backgroundEl: HTMLElement; |
|
|
|
public topbar: ChatTopbar; |
|
public bubbles: ChatBubbles; |
|
public input: ChatInput; |
|
public selection: ChatSelection; |
|
public contextMenu: ChatContextMenu; |
|
public search: ChatSearch; |
|
|
|
public wasAlreadyUsed: boolean; |
|
// public initPeerId = 0; |
|
public peerId: PeerId; |
|
public threadId: number; |
|
public setPeerPromise: Promise<void>; |
|
public peerChanged: boolean; |
|
|
|
public log: ReturnType<typeof logger>; |
|
|
|
public type: ChatType; |
|
public messagesStorageKey: MessagesStorageKey; |
|
|
|
public noForwards: boolean; |
|
|
|
public inited: boolean; |
|
|
|
public isRestricted: boolean; |
|
public autoDownload: ChatAutoDownloadSettings; |
|
|
|
public gradientRenderer: ChatBackgroundGradientRenderer; |
|
public patternRenderer: ChatBackgroundPatternRenderer; |
|
public gradientCanvas: HTMLCanvasElement; |
|
public patternCanvas: HTMLCanvasElement; |
|
public backgroundTempId: number; |
|
public setBackgroundPromise: Promise<void>; |
|
public sharedMediaTab: AppSharedMediaTab; |
|
public sharedMediaTabs: AppSharedMediaTab[]; |
|
// public renderDarkPattern: () => Promise<void>; |
|
|
|
public isAnyGroup: boolean; |
|
public isMegagroup: boolean; |
|
|
|
constructor( |
|
public appImManager: AppImManager, |
|
public managers: AppManagers |
|
) { |
|
super(); |
|
|
|
this.type = 'chat'; |
|
|
|
this.container = document.createElement('div'); |
|
this.container.classList.add('chat', 'tabs-tab'); |
|
|
|
this.backgroundEl = document.createElement('div'); |
|
this.backgroundEl.classList.add('chat-background'); |
|
|
|
// * constructor end |
|
|
|
this.log = logger('CHAT', LogTypes.Log | LogTypes.Warn | LogTypes.Debug | LogTypes.Error); |
|
//this.log.error('Chat construction'); |
|
|
|
this.peerId = NULL_PEER_ID; |
|
|
|
this.container.append(this.backgroundEl); |
|
this.appImManager.chatsContainer.append(this.container); |
|
|
|
this.backgroundTempId = 0; |
|
this.sharedMediaTabs = []; |
|
} |
|
|
|
public setBackground(url: string, skipAnimation?: boolean): Promise<void> { |
|
const theme = themeController.getTheme(); |
|
|
|
let item: HTMLElement; |
|
const isColorBackground = !!theme.background.color && !theme.background.slug && !theme.background.intensity; |
|
if( |
|
isColorBackground && |
|
document.documentElement.style.cursor === 'grabbing' && |
|
this.gradientRenderer && |
|
!this.patternRenderer |
|
) { |
|
this.gradientCanvas.dataset.colors = theme.background.color; |
|
this.gradientRenderer.init(this.gradientCanvas); |
|
return Promise.resolve(); |
|
} |
|
|
|
const tempId = ++this.backgroundTempId; |
|
|
|
const previousGradientRenderer = this.gradientRenderer; |
|
const previousPatternRenderer = this.patternRenderer; |
|
const previousGradientCanvas = this.gradientCanvas; |
|
const previousPatternCanvas = this.patternCanvas; |
|
|
|
this.gradientRenderer = |
|
this.patternRenderer = |
|
this.gradientCanvas = |
|
this.patternCanvas = |
|
// this.renderDarkPattern = |
|
undefined; |
|
|
|
const intensity = theme.background.intensity && theme.background.intensity / 100; |
|
const isDarkPattern = !!intensity && intensity < 0; |
|
|
|
let patternRenderer: ChatBackgroundPatternRenderer; |
|
let patternCanvas = item?.firstElementChild as HTMLCanvasElement; |
|
let gradientCanvas: HTMLCanvasElement; |
|
if(!item) { |
|
item = document.createElement('div'); |
|
item.classList.add('chat-background-item'); |
|
|
|
if(url) { |
|
if(intensity) { |
|
item.classList.add('is-pattern'); |
|
|
|
const rect = this.appImManager.chatsContainer.getBoundingClientRect(); |
|
patternRenderer = this.patternRenderer = ChatBackgroundPatternRenderer.getInstance({ |
|
url, |
|
width: rect.width, |
|
height: rect.height, |
|
mask: isDarkPattern |
|
}); |
|
|
|
patternCanvas = this.patternCanvas = patternRenderer.createCanvas(); |
|
patternCanvas.classList.add('chat-background-item-canvas', 'chat-background-item-pattern-canvas'); |
|
|
|
if(isDarkPattern) { |
|
item.classList.add('is-dark'); |
|
} |
|
|
|
// if(isDarkPattern) { |
|
// this.renderDarkPattern = () => { |
|
// return patternRenderer.exportCanvasPatternToImage(patternCanvas).then((url) => { |
|
// if(this.backgroundTempId !== tempId) { |
|
// return; |
|
// } |
|
|
|
// gradientCanvas.style.webkitMaskImage = `url(${url})`; |
|
// }); |
|
// }; |
|
// } |
|
} else if(theme.background.slug) { |
|
item.classList.add('is-image'); |
|
} |
|
} else if(theme.background.color) { |
|
item.classList.add('is-color'); |
|
} |
|
} |
|
|
|
let gradientRenderer: ChatBackgroundGradientRenderer; |
|
const color = theme.background.color; |
|
if(color) { |
|
// if(color.includes(',')) { |
|
const {canvas, gradientRenderer: _gradientRenderer} = ChatBackgroundGradientRenderer.create(color); |
|
gradientRenderer = this.gradientRenderer = _gradientRenderer; |
|
gradientCanvas = this.gradientCanvas = canvas; |
|
gradientCanvas.classList.add('chat-background-item-canvas', 'chat-background-item-color-canvas'); |
|
|
|
if(rootScope.settings.animationsEnabled) { |
|
gradientRenderer.scrollAnimate(true); |
|
} |
|
// } else { |
|
// item.style.backgroundColor = color; |
|
// item.style.backgroundImage = 'none'; |
|
// } |
|
} |
|
|
|
if(patternRenderer) { |
|
const setOpacityTo = isDarkPattern ? gradientCanvas : patternCanvas; |
|
setOpacityTo.style.setProperty('--opacity-max', '' + Math.abs(intensity)); |
|
} |
|
|
|
const promise = new Promise<void>((resolve) => { |
|
const cb = () => { |
|
if(this.backgroundTempId !== tempId) { |
|
if(patternRenderer) { |
|
patternRenderer.cleanup(patternCanvas); |
|
} |
|
|
|
if(gradientRenderer) { |
|
gradientRenderer.cleanup(); |
|
} |
|
|
|
return; |
|
} |
|
|
|
const prev = this.backgroundEl.lastElementChild as HTMLElement; |
|
|
|
if(prev === item) { |
|
resolve(); |
|
return; |
|
} |
|
|
|
const append = [ |
|
gradientCanvas, |
|
// isDarkPattern && this.renderDarkPattern ? undefined : patternCanvas |
|
patternCanvas |
|
].filter(Boolean); |
|
if(append.length) { |
|
item.append(...append); |
|
} |
|
|
|
this.backgroundEl.append(item); |
|
|
|
SetTransition(item, 'is-visible', true, !skipAnimation ? 200 : 0, prev ? () => { |
|
if(previousPatternRenderer) { |
|
previousPatternRenderer.cleanup(previousPatternCanvas); |
|
} |
|
|
|
if(previousGradientRenderer) { |
|
previousGradientRenderer.cleanup(); |
|
} |
|
|
|
prev.remove(); |
|
} : null, 2); |
|
|
|
resolve(); |
|
}; |
|
|
|
if(patternRenderer) { |
|
const renderPatternPromise = patternRenderer.renderToCanvas(patternCanvas); |
|
renderPatternPromise.then(() => { |
|
if(this.backgroundTempId !== tempId) { |
|
return; |
|
} |
|
|
|
let promise: Promise<any>; |
|
// if(isDarkPattern && this.renderDarkPattern) { |
|
// promise = this.renderDarkPattern(); |
|
// } else { |
|
promise = Promise.resolve(); |
|
// } |
|
|
|
promise.then(cb); |
|
}); |
|
} else if(url) { |
|
renderImageFromUrl(item, url, cb); |
|
} else { |
|
cb(); |
|
} |
|
}); |
|
|
|
return this.setBackgroundPromise = Promise.race([ |
|
pause(500), |
|
promise |
|
]); |
|
} |
|
|
|
public setType(type: ChatType) { |
|
this.type = type; |
|
} |
|
|
|
public init(/* peerId: PeerId */) { |
|
// this.initPeerId = peerId; |
|
|
|
this.topbar = new ChatTopbar(this, appSidebarRight, this.managers); |
|
this.bubbles = new ChatBubbles(this, this.managers); |
|
this.input = new ChatInput(this, this.appImManager, this.managers); |
|
this.contextMenu = new ChatContextMenu(this, this.managers); |
|
this.selection = new ChatSelection(this, this.bubbles, this.input, this.managers); |
|
|
|
if(this.type === 'chat') { |
|
this.topbar.constructUtils(); |
|
this.topbar.constructPeerHelpers(); |
|
} else if(this.type === 'pinned') { |
|
this.topbar.constructPinnedHelpers(); |
|
} else if(this.type === 'discussion') { |
|
this.topbar.constructUtils(); |
|
this.topbar.constructDiscussionHelpers(); |
|
} |
|
|
|
this.topbar.construct(); |
|
this.input.construct(); |
|
|
|
if(this.type === 'chat') { // * гений в деле, разный порядок из-за разной последовательности действий |
|
this.bubbles.constructPeerHelpers(); |
|
this.input.constructPeerHelpers(); |
|
} else if(this.type === 'pinned') { |
|
this.bubbles.constructPinnedHelpers(); |
|
this.input.constructPinnedHelpers(); |
|
} else if(this.type === 'scheduled') { |
|
this.bubbles.constructScheduledHelpers(); |
|
this.input.constructPeerHelpers(); |
|
} else if(this.type === 'discussion') { |
|
this.bubbles.constructPeerHelpers(); |
|
this.input.constructPeerHelpers(); |
|
} |
|
|
|
if(this.type !== 'scheduled' && !IS_TOUCH_SUPPORTED) { |
|
this.bubbles.setReactionsHoverListeners(); |
|
} |
|
|
|
this.bubbles.attachContainerListeners(); |
|
|
|
this.container.classList.add('type-' + this.type); |
|
this.container.append(this.topbar.container, this.bubbles.container, this.input.chatInput); |
|
|
|
this.bubbles.listenerSetter.add(rootScope)('dialog_migrate', ({migrateFrom, migrateTo}) => { |
|
if(this.peerId === migrateFrom) { |
|
this.setPeer(migrateTo); |
|
} |
|
}); |
|
|
|
this.bubbles.listenerSetter.add(rootScope)('dialog_drop', (e) => { |
|
if(e.peerId === this.peerId) { |
|
this.appImManager.setPeer(); |
|
} |
|
}); |
|
} |
|
|
|
public beforeDestroy() { |
|
this.bubbles.cleanup(); |
|
} |
|
|
|
private cleanupBackground() { |
|
++this.backgroundTempId; |
|
if(this.patternRenderer) { |
|
this.patternRenderer.cleanup(this.patternCanvas); |
|
this.patternRenderer = undefined; |
|
} |
|
|
|
if(this.gradientRenderer) { |
|
this.gradientRenderer.cleanup(); |
|
this.gradientRenderer = undefined; |
|
} |
|
} |
|
|
|
public destroy() { |
|
//const perf = performance.now(); |
|
|
|
this.topbar.destroy(); |
|
this.bubbles.destroy(); |
|
this.input.destroy(); |
|
this.contextMenu && this.contextMenu.destroy(); |
|
this.selection && this.selection.attachListeners(undefined, undefined); |
|
|
|
this.cleanupBackground(); |
|
|
|
delete this.topbar; |
|
delete this.bubbles; |
|
delete this.input; |
|
delete this.selection; |
|
delete this.contextMenu; |
|
|
|
this.container.remove(); |
|
|
|
//this.log.error('Chat destroy time:', performance.now() - perf); |
|
} |
|
|
|
public cleanup(helperToo = true) { |
|
this.input.cleanup(helperToo); |
|
this.topbar.cleanup(); |
|
this.selection.cleanup(); |
|
} |
|
|
|
public async onChangePeer(m: ReturnType<typeof middlewarePromise>) { |
|
const {peerId} = this; |
|
|
|
const searchTab = appSidebarRight.getTab(AppPrivateSearchTab); |
|
if(searchTab) { |
|
searchTab.close(); |
|
} |
|
|
|
const [noForwards, isRestricted, isAnyGroup, _, isMegagroup] = await m(Promise.all([ |
|
this.managers.appPeersManager.noForwards(peerId), |
|
this.managers.appPeersManager.isRestricted(peerId), |
|
this._isAnyGroup(peerId), |
|
this.setAutoDownloadMedia(), |
|
this.managers.appPeersManager.isMegagroup(peerId) |
|
])); |
|
|
|
this.noForwards = noForwards; |
|
this.isRestricted = isRestricted; |
|
this.isAnyGroup = isAnyGroup; |
|
this.isMegagroup = isMegagroup; |
|
|
|
this.container.classList.toggle('no-forwards', this.noForwards); |
|
|
|
this.sharedMediaTab = appSidebarRight.createSharedMediaTab(); |
|
this.sharedMediaTabs.push(this.sharedMediaTab); |
|
|
|
this.sharedMediaTab.setPeer(peerId, this.threadId); |
|
this.input.clearHelper(); // костыль |
|
this.selection.cleanup(); // TODO: REFACTOR !!!!!! |
|
} |
|
|
|
public setPeer(peerId: PeerId, lastMsgId?: number, startParam?: string) { |
|
if(!peerId) { |
|
this.inited = undefined; |
|
} else if(!this.inited) { |
|
if(this.init) { |
|
this.init(/* peerId */); |
|
this.init = null; |
|
} |
|
|
|
this.inited = true; |
|
} |
|
|
|
const samePeer = this.peerId === peerId; |
|
if(!samePeer) { |
|
this.appImManager.dispatchEvent('peer_changing', this); |
|
this.peerId = peerId || NULL_PEER_ID; |
|
this.messagesStorageKey = `${this.peerId}_${this.type === 'scheduled' ? 'scheduled' : 'history'}`; |
|
} else if(this.setPeerPromise) { |
|
return; |
|
} |
|
|
|
if(!peerId) { |
|
appSidebarRight.toggleSidebar(false); |
|
this.cleanup(true); |
|
this.bubbles.setPeer(false, peerId); |
|
this.appImManager.dispatchEvent('peer_changed', peerId); |
|
return; |
|
} |
|
|
|
this.peerChanged = samePeer; |
|
|
|
const bubblesSetPeerPromise = this.bubbles.setPeer(samePeer, peerId, lastMsgId, startParam); |
|
const setPeerPromise = this.setPeerPromise = bubblesSetPeerPromise.then((result) => { |
|
return result.promise; |
|
}).catch(noop).finally(() => { |
|
if(this.setPeerPromise === setPeerPromise) { |
|
this.setPeerPromise = null; |
|
} |
|
}); |
|
|
|
return bubblesSetPeerPromise; |
|
} |
|
|
|
public destroySharedMediaTab(tab = this.sharedMediaTab) { |
|
indexOfAndSplice(this.sharedMediaTabs, tab); |
|
tab.destroy(); |
|
} |
|
|
|
public async setAutoDownloadMedia() { |
|
this.autoDownload = await getAutoDownloadSettingsByPeerId(this.peerId); |
|
} |
|
|
|
public setMessageId(messageId?: number) { |
|
return this.setPeer(this.peerId, messageId); |
|
} |
|
|
|
public async finishPeerChange(isTarget: boolean, isJump: boolean, lastMsgId: number, startParam?: string) { |
|
if(this.peerChanged) return; |
|
|
|
const peerId = this.peerId; |
|
this.peerChanged = true; |
|
this.wasAlreadyUsed = true; |
|
|
|
const middleware = this.bubbles.getMiddleware(); |
|
|
|
this.cleanup(false); |
|
|
|
const sharedMediaTab = this.sharedMediaTab; |
|
sharedMediaTab.loadSidebarMedia(true); |
|
|
|
const callbacksPromise = Promise.all([ |
|
this.topbar.finishPeerChange(isTarget), |
|
this.bubbles.finishPeerChange(), |
|
this.input.finishPeerChange(startParam), |
|
]); |
|
|
|
const [callbacks] = await Promise.all([ |
|
callbacksPromise, |
|
sharedMediaTab.fillProfileElements() |
|
]); |
|
|
|
if(!middleware()) { |
|
return; |
|
} |
|
|
|
callbacks.forEach((callback) => { |
|
callback(); |
|
}); |
|
|
|
appSidebarRight.replaceSharedMediaTab(sharedMediaTab); |
|
|
|
this.sharedMediaTabs.filter((tab) => tab !== sharedMediaTab).forEach((tab) => this.destroySharedMediaTab(tab)); |
|
|
|
this.log.setPrefix('CHAT-' + peerId + '-' + this.type); |
|
|
|
this.appImManager.dispatchEvent('peer_changed', peerId); |
|
} |
|
|
|
public getMessage(mid: number) { |
|
return this.managers.appMessagesManager.getMessageFromStorage(this.messagesStorageKey, mid); |
|
} |
|
|
|
public async getMidsByMid(mid: number) { |
|
return this.managers.appMessagesManager.getMidsByMessage(await this.getMessage(mid)); |
|
} |
|
|
|
public getHistoryStorage(ignoreThreadId?: boolean) { |
|
return this.managers.appMessagesManager.getHistoryStorageTransferable(this.peerId, ignoreThreadId ? undefined : this.threadId) |
|
.then((historyStorageTransferable) => { |
|
return { |
|
...historyStorageTransferable, |
|
history: SlicedArray.fromJSON<number>(historyStorageTransferable.historySerialized) |
|
} |
|
}); |
|
} |
|
|
|
public getHistoryMaxId() { |
|
return this.getHistoryStorage().then((historyStorage) => historyStorage.maxId); |
|
} |
|
|
|
public async _isAnyGroup(peerId: PeerId) { |
|
return peerId === rootScope.myId || peerId === REPLIES_PEER_ID || (await this.managers.appPeersManager.isAnyGroup(peerId)); |
|
} |
|
|
|
public initSearch(query?: string) { |
|
if(!this.peerId) return; |
|
|
|
if(mediaSizes.isMobile) { |
|
if(!this.search) { |
|
this.search = new ChatSearch(this.topbar, this, query); |
|
} else { |
|
this.search.setQuery(query); |
|
} |
|
} else { |
|
let tab = appSidebarRight.getTab(AppPrivateSearchTab); |
|
if(!tab) { |
|
tab = appSidebarRight.createTab(AppPrivateSearchTab); |
|
} |
|
|
|
tab.open(this.peerId, this.threadId, this.bubbles.onDatePick, query); |
|
} |
|
} |
|
|
|
public canSend(action?: ChatRights) { |
|
return this.managers.appMessagesManager.canSendToPeer(this.peerId, this.threadId, action); |
|
} |
|
|
|
public isStartButtonNeeded() { |
|
return Promise.all([ |
|
this.managers.appPeersManager.isBot(this.peerId), |
|
this.managers.appMessagesManager.getDialogOnly(this.peerId), |
|
this.getHistoryStorage(true) |
|
]).then(([isBot, dialog, historyStorage]) => { |
|
return isBot && !dialog && !historyStorage.history.length; |
|
}); |
|
} |
|
|
|
public getMessageSendingParams() { |
|
return { |
|
threadId: this.threadId, |
|
replyToMsgId: this.input.replyToMsgId, |
|
scheduleDate: this.input.scheduleDate, |
|
sendSilent: this.input.sendSilent, |
|
sendAsPeerId: this.input.sendAsPeerId |
|
}; |
|
} |
|
|
|
public isOurMessage(message: Message.message | Message.messageService) { |
|
return message.fromId === rootScope.myId || (message.pFlags.out && this.isMegagroup); |
|
} |
|
|
|
public isOutMessage(message: Message.message | Message.messageService) { |
|
const fwdFrom = (message as Message.message).fwd_from; |
|
const isOut = this.isOurMessage(message) && (!fwdFrom || this.peerId !== rootScope.myId); |
|
return isOut; |
|
} |
|
|
|
public isAvatarNeeded(message: Message.message | Message.messageService) { |
|
return this.isAnyGroup && !this.isOutMessage(message); |
|
} |
|
}
|
|
|