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.
2208 lines
72 KiB
2208 lines
72 KiB
/* |
|
* https://github.com/morethanwords/tweb |
|
* Copyright (C) 2019-2021 Eduard Kuzmenko |
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE |
|
*/ |
|
|
|
import type {MyDialogFilter as DialogFilter, MyDialogFilter} from '../storages/filters'; |
|
import type LazyLoadQueue from '../../components/lazyLoadQueue'; |
|
import type {Dialog, MyMessage} from './appMessagesManager'; |
|
import type {MyPhoto} from './appPhotosManager'; |
|
import type {MyDocument} from './appDocsManager'; |
|
import type {State} from '../../config/state'; |
|
import AvatarElement from '../../components/avatar'; |
|
import DialogsContextMenu from '../../components/dialogsContextMenu'; |
|
import {horizontalMenu} from '../../components/horizontalMenu'; |
|
import ripple from '../../components/ripple'; |
|
import Scrollable, {ScrollableX, SliceSides} from '../../components/scrollable'; |
|
import {formatDateAccordingToTodayNew} from '../../helpers/date'; |
|
import {IS_MOBILE_SAFARI, IS_SAFARI} from '../../environment/userAgent'; |
|
import {logger, LogTypes} from '../logger'; |
|
import rootScope from '../rootScope'; |
|
import appImManager from './appImManager'; |
|
import Button from '../../components/button'; |
|
import SetTransition from '../../components/singleTransition'; |
|
import {MyDraftMessage} from './appDraftsManager'; |
|
import DEBUG, {MOUNT_CLASS_TO} from '../../config/debug'; |
|
import PeerTitle from '../../components/peerTitle'; |
|
import I18n, {FormatterArguments, i18n, LangPackKey, _i18n} from '../langPack'; |
|
import findUpTag from '../../helpers/dom/findUpTag'; |
|
import lottieLoader from '../rlottie/lottieLoader'; |
|
import {wrapPhoto} from '../../components/wrappers'; |
|
import AppEditFolderTab from '../../components/sidebarLeft/tabs/editFolder'; |
|
import appSidebarLeft, {SettingSection} from '../../components/sidebarLeft'; |
|
import {attachClickEvent} from '../../helpers/dom/clickEvent'; |
|
import positionElementByIndex from '../../helpers/dom/positionElementByIndex'; |
|
import replaceContent from '../../helpers/dom/replaceContent'; |
|
import ConnectionStatusComponent from '../../components/connectionStatus'; |
|
import {renderImageFromUrlPromise} from '../../helpers/dom/renderImageFromUrl'; |
|
import {fastRafConventional, fastRafPromise} from '../../helpers/schedulers'; |
|
import SortedUserList from '../../components/sortedUserList'; |
|
import IS_TOUCH_SUPPORTED from '../../environment/touchSupport'; |
|
import handleTabSwipe from '../../helpers/dom/handleTabSwipe'; |
|
import windowSize from '../../helpers/windowSize'; |
|
import isInDOM from '../../helpers/dom/isInDOM'; |
|
import {setSendingStatus} from '../../components/sendingStatus'; |
|
import SortedList, {SortedElementBase} from '../../helpers/sortedList'; |
|
import debounce from '../../helpers/schedulers/debounce'; |
|
import {FOLDER_ID_ALL, FOLDER_ID_ARCHIVE, NULL_PEER_ID, REAL_FOLDERS, REAL_FOLDER_ID} from '../mtproto/mtproto_config'; |
|
import groupCallActiveIcon from '../../components/groupCallActiveIcon'; |
|
import {Chat, Message, NotifyPeer} from '../../layer'; |
|
import IS_GROUP_CALL_SUPPORTED from '../../environment/groupCallSupport'; |
|
import mediaSizes from '../../helpers/mediaSizes'; |
|
import appNavigationController, {NavigationItem} from '../../components/appNavigationController'; |
|
import assumeType from '../../helpers/assumeType'; |
|
import appMediaPlaybackController from '../../components/appMediaPlaybackController'; |
|
import setInnerHTML from '../../helpers/dom/setInnerHTML'; |
|
import {AppManagers} from './managers'; |
|
import appSidebarRight from '../../components/sidebarRight'; |
|
import PopupElement from '../../components/popups'; |
|
import choosePhotoSize from './utils/photos/choosePhotoSize'; |
|
import wrapEmojiText from '../richTextProcessor/wrapEmojiText'; |
|
import wrapMessageForReply from '../../components/wrappers/messageForReply'; |
|
import isMessageRestricted from './utils/messages/isMessageRestricted'; |
|
import getMediaFromMessage from './utils/messages/getMediaFromMessage'; |
|
import getMessageSenderPeerIdOrName from './utils/messages/getMessageSenderPeerIdOrName'; |
|
import wrapStickerEmoji from '../../components/wrappers/stickerEmoji'; |
|
import getDialogIndexKey from './utils/dialogs/getDialogIndexKey'; |
|
import getProxiedManagers from './getProxiedManagers'; |
|
import getDialogIndex from './utils/dialogs/getDialogIndex'; |
|
import {attachContextMenuListener} from '../../helpers/dom/attachContextMenuListener'; |
|
import deferredPromise, {CancellablePromise} from '../../helpers/cancellablePromise'; |
|
import wrapPeerTitle from '../../components/wrappers/peerTitle'; |
|
import middlewarePromise from '../../helpers/middlewarePromise'; |
|
import appDownloadManager from './appDownloadManager'; |
|
import groupCallsController from '../calls/groupCallsController'; |
|
import callsController from '../calls/callsController'; |
|
import cancelEvent from '../../helpers/dom/cancelEvent'; |
|
import noop from '../../helpers/noop'; |
|
import DialogsPlaceholder from '../../helpers/dialogsPlaceholder'; |
|
import pause from '../../helpers/schedulers/pause'; |
|
import apiManagerProxy from '../mtproto/mtprotoworker'; |
|
import filterAsync from '../../helpers/array/filterAsync'; |
|
import forEachReverse from '../../helpers/array/forEachReverse'; |
|
import indexOfAndSplice from '../../helpers/array/indexOfAndSplice'; |
|
import whichChild from '../../helpers/dom/whichChild'; |
|
|
|
export const DIALOG_LIST_ELEMENT_TAG = 'A'; |
|
|
|
export type DialogDom = { |
|
avatarEl: AvatarElement, |
|
captionDiv: HTMLDivElement, |
|
titleSpan: HTMLSpanElement, |
|
titleSpanContainer: HTMLSpanElement, |
|
statusSpan: HTMLSpanElement, |
|
lastTimeSpan: HTMLSpanElement, |
|
unreadBadge: HTMLElement, |
|
callIcon?: ReturnType<typeof groupCallActiveIcon>, |
|
mentionsBadge?: HTMLElement, |
|
lastMessageSpan: HTMLSpanElement, |
|
containerEl: HTMLElement, |
|
listEl: HTMLElement, |
|
subtitleEl: HTMLElement, |
|
|
|
setLastMessagePromise?: CancellablePromise<void>, |
|
setUnreadMessagePromise?: CancellablePromise<void> |
|
}; |
|
|
|
interface SortedDialog extends SortedElementBase { |
|
dom: DialogDom, |
|
loadPromises?: Promise<any>[] |
|
} |
|
|
|
function setPromiseMiddleware<T extends {[smth in K as K]?: CancellablePromise<void>}, K extends keyof T>(obj: T, key: K) { |
|
const oldPromise: CancellablePromise<void> = obj[key] as any; |
|
if(oldPromise) { |
|
oldPromise.reject(); |
|
} |
|
|
|
// @ts-ignore |
|
const deferred = obj[key] = deferredPromise<void>(); |
|
deferred.catch(() => {}).finally(() => { |
|
if((obj[key] as any) === deferred) { |
|
delete obj[key]; |
|
} |
|
}); |
|
|
|
const middleware = middlewarePromise(() => (obj[key] as any) === deferred); |
|
return {deferred, middleware}; |
|
} |
|
|
|
class SortedDialogList extends SortedList<SortedDialog> { |
|
constructor( |
|
public managers: AppManagers, |
|
public list: HTMLUListElement, |
|
public indexKey: ReturnType<typeof getDialogIndexKey>, |
|
public onListLengthChange?: () => void |
|
) { |
|
super({ |
|
getIndex: (element) => managers.dialogsStorage.getDialogIndex(element.id, this.indexKey), |
|
onDelete: (element) => { |
|
element.dom.listEl.remove(); |
|
this.onListLengthChange && this.onListLengthChange(); |
|
}, |
|
onSort: (element, idx) => { |
|
const willChangeLength = element.dom.listEl.parentElement !== this.list; |
|
positionElementByIndex(element.dom.listEl, this.list, idx); |
|
|
|
if(willChangeLength) { |
|
this.onListLengthChange && this.onListLengthChange(); |
|
} |
|
}, |
|
onElementCreate: (base, batch) => { |
|
const loadPromises: Promise<any>[] = batch ? [] : undefined; |
|
|
|
const {dom} = appDialogsManager.addListDialog({peerId: base.id, loadPromises, isBatch: batch}); |
|
(base as SortedDialog).dom = dom; |
|
|
|
if(loadPromises?.length) { |
|
(base as SortedDialog).loadPromises = loadPromises; |
|
Promise.all(loadPromises).finally(() => { |
|
delete (base as SortedDialog).loadPromises; |
|
}); |
|
} |
|
|
|
return base as SortedDialog; |
|
}, |
|
updateElementWith: fastRafConventional |
|
}); |
|
} |
|
|
|
public clear() { |
|
this.list.textContent = ''; |
|
super.clear(); |
|
} |
|
} |
|
|
|
// const testScroll = false; |
|
// let testTopSlice = 1; |
|
|
|
export class AppDialogsManager { |
|
private chatsContainer = document.getElementById('chatlist-container') as HTMLDivElement; |
|
|
|
private loadDialogsPromise: Promise<{cached: boolean, renderPromise: AppDialogsManager['loadDialogsRenderPromise']}>; |
|
private loadDialogsRenderPromise: Promise<void>; |
|
|
|
private scroll: Scrollable = null; |
|
|
|
private log = logger('DIALOGS', LogTypes.Log | LogTypes.Error | LogTypes.Warn | LogTypes.Debug); |
|
|
|
private contextMenu: DialogsContextMenu; |
|
|
|
private sortedList: SortedDialogList; |
|
public placeholders: {[filterId: number]: DialogsPlaceholder} = {}; |
|
public sortedLists: {[filterId: number]: SortedDialogList} = {}; |
|
public scrollables: {[filterId: number]: Scrollable} = {}; |
|
public filterId: number; |
|
private folders: {[k in 'menu' | 'container' | 'menuScrollContainer']: HTMLElement} = { |
|
menu: document.getElementById('folders-tabs'), |
|
menuScrollContainer: null, |
|
container: document.getElementById('folders-container') |
|
}; |
|
private filtersRendered: { |
|
[filterId: string]: { |
|
menu: HTMLElement, |
|
container: HTMLElement, |
|
unread: HTMLElement, |
|
title: HTMLElement |
|
} |
|
} = {}; |
|
private showFiltersPromise: Promise<void>; |
|
|
|
private sliceTimeout: number; |
|
|
|
private lastActiveElements: Set<HTMLElement> = new Set(); |
|
|
|
private offsets: {top: number, bottom: number} = {top: 0, bottom: 0}; |
|
|
|
private loadContacts: () => void; |
|
private processContact: (peerId: PeerId) => void; |
|
|
|
private indexKey: ReturnType<typeof getDialogIndexKey>; |
|
|
|
private initedListeners = false; |
|
|
|
private onListLengthChange: () => Promise<void>; |
|
private loadedDialogsAtLeastOnce = false; |
|
private allChatsIntlElement: I18n.IntlElement; |
|
|
|
private emptyDialogsPlaceholderSubtitle: I18n.IntlElement; |
|
private updateContactsLengthPromise: Promise<number>; |
|
|
|
private filtersNavigationItem: NavigationItem; |
|
|
|
private managers: AppManagers; |
|
private selectTab: ReturnType<typeof horizontalMenu>; |
|
|
|
constructor() { |
|
const managers = this.managers = getProxiedManagers(); |
|
|
|
this.contextMenu = new DialogsContextMenu(managers); |
|
|
|
this.folders.menuScrollContainer = this.folders.menu.parentElement; |
|
|
|
this.onListLengthChange = debounce(this._onListLengthChange, 100, false, true); |
|
|
|
const bottomPart = document.createElement('div'); |
|
bottomPart.classList.add('connection-status-bottom'); |
|
bottomPart.append(this.folders.container); |
|
|
|
/* if(isTouchSupported && isSafari) { |
|
let allowUp: boolean, allowDown: boolean, slideBeginY: number; |
|
const container = this.scroll.container; |
|
container.addEventListener('touchstart', (event) => { |
|
allowUp = container.scrollTop > 0; |
|
allowDown = (container.scrollTop < container.scrollHeight - container.clientHeight); |
|
// @ts-ignore |
|
slideBeginY = event.pageY; |
|
}); |
|
|
|
container.addEventListener('touchmove', (event: any) => { |
|
var up = (event.pageY > slideBeginY); |
|
var down = (event.pageY < slideBeginY); |
|
slideBeginY = event.pageY; |
|
if((up && allowUp) || (down && allowDown)) { |
|
event.stopPropagation(); |
|
} else if(up || down) { |
|
event.preventDefault(); |
|
} |
|
}); |
|
} */ |
|
|
|
if(IS_TOUCH_SUPPORTED) { |
|
handleTabSwipe({ |
|
element: this.folders.container, |
|
onSwipe: (xDiff) => { |
|
const prevId = selectTab.prevId(); |
|
selectTab(xDiff > 0 ? prevId + 1 : prevId - 1); |
|
} |
|
}); |
|
} |
|
|
|
this.allChatsIntlElement = new I18n.IntlElement({ |
|
key: 'FilterAllChatsShort' |
|
}); |
|
|
|
/* if(testScroll) { |
|
let i = 0; |
|
let add = () => { |
|
let li = document.createElement('li'); |
|
li.dataset.id = '' + i; |
|
li.id = '' + i; |
|
li.innerHTML = `<div class="rp"><avatar-element style="background-color: rgb(166, 149, 231); font-size: 0px;"><img src="assets/img/pepe.jpg"></avatar-element><div class="user-caption"><p><span class="user-title">${i}</span><span><span class="message-status"></span><span class="message-time">18:33</span></span></p><p><span class="user-last-message"><b>-_-_-_-: </b>qweasd</span><span></span></p></div></div>`; |
|
i++; |
|
this.scroll.append(li); |
|
}; |
|
for(let i = 0; i < 500; ++i) { |
|
add(); |
|
} |
|
(window as any).addElement = add; |
|
} */ |
|
|
|
rootScope.addEventListener('premium_toggle', async(isPremium) => { |
|
if(isPremium) { |
|
return; |
|
} |
|
|
|
const isFolderAvailable = await this.managers.filtersStorage.isFilterIdAvailable(this.filterId); |
|
if(!isFolderAvailable) { |
|
selectTab(whichChild(this.filtersRendered[FOLDER_ID_ALL].menu), false); |
|
} |
|
}); |
|
|
|
rootScope.addEventListener('state_cleared', () => { |
|
const clearCurrent = REAL_FOLDERS.has(this.filterId); |
|
// setTimeout(() => |
|
apiManagerProxy.getState().then(async(state) => { |
|
this.loadedDialogsAtLeastOnce = false; |
|
|
|
/* const clearPromises: Promise<any>[] = []; |
|
for(const name in this.managers.appStateManager.storagesResults) { |
|
const results = this.managers.appStateManager.storagesResults[name as keyof AppStateManager['storages']]; |
|
const storage = this.managers.appStateManager.storages[name as keyof AppStateManager['storages']]; |
|
results.length = 0; |
|
clearPromises.push(storage.clear()); |
|
} */ |
|
|
|
if(clearCurrent) { |
|
this.sortedList.clear(); |
|
this.onTabChange(); |
|
} |
|
|
|
this.onStateLoaded(state); |
|
})// , 5000); |
|
}); |
|
|
|
this.setFilterId(FOLDER_ID_ALL, FOLDER_ID_ALL); |
|
this.addFilter({ |
|
id: FOLDER_ID_ALL, |
|
title: '', |
|
localId: FOLDER_ID_ALL |
|
}); |
|
|
|
const foldersScrollable = new ScrollableX(this.folders.menuScrollContainer); |
|
bottomPart.prepend(this.folders.menuScrollContainer); |
|
const selectTab = this.selectTab = horizontalMenu(this.folders.menu, this.folders.container, async(id, tabContent) => { |
|
/* if(id !== 0) { |
|
id += 1; |
|
} */ |
|
|
|
const _id = id; |
|
id = +tabContent.dataset.filterId || FOLDER_ID_ALL; |
|
|
|
const isFilterAvailable = REAL_FOLDERS.has(id) || await this.managers.filtersStorage.isFilterIdAvailable(id); |
|
if(!isFilterAvailable) { |
|
return false; |
|
} |
|
|
|
const wasFilterId = this.filterId; |
|
if(!IS_MOBILE_SAFARI) { |
|
if(_id) { |
|
if(!this.filtersNavigationItem) { |
|
this.filtersNavigationItem = { |
|
type: 'filters', |
|
onPop: () => { |
|
selectTab(0); |
|
this.filtersNavigationItem = undefined; |
|
} |
|
}; |
|
|
|
appNavigationController.spliceItems(1, 0, this.filtersNavigationItem); |
|
} |
|
} else if(this.filtersNavigationItem) { |
|
appNavigationController.removeItem(this.filtersNavigationItem); |
|
this.filtersNavigationItem = undefined; |
|
} |
|
} |
|
|
|
if(wasFilterId === id) return; |
|
|
|
this.sortedLists[id].clear(); |
|
const promise = this.setFilterIdAndChangeTab(id).then(({cached, renderPromise}) => { |
|
if(cached) { |
|
return renderPromise; |
|
} |
|
}); |
|
|
|
if(wasFilterId !== -1) { |
|
return promise; |
|
} |
|
}, () => { |
|
for(const folderId in this.sortedLists) { |
|
if(+folderId !== this.filterId) { |
|
this.sortedLists[folderId].clear(); |
|
const placeholder = this.placeholders[folderId]; |
|
if(placeholder) { |
|
placeholder.remove(); |
|
} |
|
} |
|
} |
|
}, undefined, foldersScrollable); |
|
|
|
apiManagerProxy.getState().then((state) => { |
|
// * it should've had a better place :( |
|
appMediaPlaybackController.setPlaybackParams(state.playbackParams); |
|
appMediaPlaybackController.addEventListener('playbackParams', (params) => { |
|
this.managers.appStateManager.pushToState('playbackParams', params); |
|
}); |
|
|
|
return this.onStateLoaded(state); |
|
})/* .then(() => { |
|
const isLoadedMain = this.managers.appMessagesManager.dialogsStorage.isDialogsLoaded(0); |
|
const isLoadedArchive = this.managers.appMessagesManager.dialogsStorage.isDialogsLoaded(1); |
|
const wasLoaded = isLoadedMain || isLoadedArchive; |
|
const a: Promise<any> = isLoadedMain ? Promise.resolve() : this.managers.appMessagesManager.getConversationsAll('', 0); |
|
const b: Promise<any> = isLoadedArchive ? Promise.resolve() : this.managers.appMessagesManager.getConversationsAll('', 1); |
|
a.finally(() => { |
|
b.then(() => { |
|
if(wasLoaded) { |
|
(apiUpdatesManager.updatesState.syncLoading || Promise.resolve()).then(() => { |
|
this.managers.appMessagesManager.refreshConversations(); |
|
}); |
|
} |
|
}); |
|
}); |
|
}) */; |
|
|
|
mediaSizes.addEventListener('resize', () => { |
|
this.changeFiltersAllChatsKey(); |
|
}); |
|
|
|
new ConnectionStatusComponent(this.managers, this.chatsContainer); |
|
this.chatsContainer.append(bottomPart); |
|
|
|
setTimeout(() => { |
|
lottieLoader.loadLottieWorkers(); |
|
}, 200); |
|
|
|
PopupElement.MANAGERS = rootScope.managers = managers; |
|
appDownloadManager.construct(managers); |
|
appSidebarLeft.construct(managers); |
|
appSidebarRight.construct(managers); |
|
groupCallsController.construct(managers); |
|
callsController.construct(managers); |
|
appImManager.construct(managers); |
|
|
|
// start |
|
|
|
this.sortedList = this.sortedLists[this.filterId]; |
|
this.scroll = this.scrollables[this.filterId]; |
|
|
|
// selectTab(0, false); |
|
} |
|
|
|
public get chatList() { |
|
return this.sortedList.list; |
|
} |
|
|
|
public setFilterId(filterId: number, localId: MyDialogFilter['localId']) { |
|
this.filterId = filterId; |
|
this.indexKey = getDialogIndexKey(REAL_FOLDERS.has(filterId) ? filterId as REAL_FOLDER_ID : localId); |
|
} |
|
|
|
public async setFilterIdAndChangeTab(filterId: number) { |
|
this.filterId = filterId; |
|
this.indexKey = await this.managers.dialogsStorage.getDialogIndexKeyByFilterId(filterId); |
|
return this.onTabChange(); |
|
} |
|
|
|
private setOnlineStatus(element: HTMLElement, online: boolean) { |
|
const className = 'is-online'; |
|
const hasClassName = element.classList.contains(className); |
|
!hasClassName && online && element.classList.add(className); |
|
SetTransition(element, 'is-visible', online, 250, online ? undefined : () => { |
|
element.classList.remove(className); |
|
}, online && !hasClassName ? 2 : 0); |
|
} |
|
|
|
private initListeners() { |
|
rootScope.addEventListener('user_update', async(userId) => { |
|
// console.log('updating user:', user, dialog); |
|
|
|
const peerId = userId.toPeerId(); |
|
const dom = this.getDialogDom(peerId); |
|
if(dom && peerId !== rootScope.myId && !(await this.managers.appUsersManager.isBot(userId))) { |
|
const user = await this.managers.appUsersManager.getUser(userId); |
|
const online = user.status?._ === 'userStatusOnline'; |
|
this.setOnlineStatus(dom.avatarEl, online); |
|
} |
|
}); |
|
|
|
rootScope.addEventListener('chat_update', async(chatId) => { |
|
const peerId = chatId.toPeerId(true); |
|
const dialog = await this.managers.appMessagesManager.getDialogOnly(peerId); |
|
if(dialog) { |
|
this.processDialogForCallStatus(dialog); |
|
} |
|
}); |
|
|
|
rootScope.addEventListener('folder_unread', (folder) => { |
|
this.setFilterUnreadCount(folder.id); |
|
}); |
|
|
|
rootScope.addEventListener('contacts_update', (userId) => { |
|
this.processContact && this.processContact(userId.toPeerId()); |
|
}); |
|
|
|
rootScope.addEventListener('dialog_flush', ({dialog}) => { |
|
if(!dialog) { |
|
return; |
|
} |
|
|
|
this.setLastMessageN({ |
|
dialog, |
|
setUnread: true |
|
}); |
|
this.validateDialogForFilter(dialog); |
|
this.setFiltersUnreadCount(); |
|
}); |
|
|
|
rootScope.addEventListener('dialogs_multiupdate', (dialogs) => { |
|
for(const [peerId, dialog] of dialogs) { |
|
this.updateDialog(dialog); |
|
|
|
if(this.processContact) { |
|
this.processContact(peerId.toPeerId()); |
|
} |
|
|
|
this.validateDialogForFilter(dialog); |
|
} |
|
}); |
|
|
|
rootScope.addEventListener('dialog_drop', ({peerId}) => { |
|
this.deleteDialog(peerId); |
|
|
|
if(this.processContact) { |
|
this.processContact(peerId); |
|
} |
|
}); |
|
|
|
rootScope.addEventListener('dialog_unread', ({dialog}) => { |
|
if(!dialog) { |
|
return; |
|
} |
|
|
|
this.setUnreadMessagesN({dialog}); |
|
this.validateDialogForFilter(dialog); |
|
}); |
|
|
|
rootScope.addEventListener('dialog_notify_settings', (dialog) => { |
|
this.validateDialogForFilter(dialog); |
|
this.setUnreadMessagesN({dialog}); // возможно это не нужно, но нужно менять is-muted |
|
}); |
|
|
|
rootScope.addEventListener('dialog_draft', ({dialog, drop, peerId}) => { |
|
if(drop) { |
|
this.sortedList.delete(peerId); |
|
} else { |
|
this.updateDialog(dialog); |
|
} |
|
|
|
if(this.processContact) { |
|
this.processContact(peerId); |
|
} |
|
}); |
|
|
|
appImManager.addEventListener('peer_changed', (peerId) => { |
|
// const perf = performance.now(); |
|
for(const element of this.lastActiveElements) { |
|
if(element.dataset.peerId.toPeerId() !== peerId) { |
|
this.setDialogActive(element, false); |
|
} |
|
} |
|
|
|
const elements = Array.from(document.querySelectorAll(`[data-autonomous="0"] .chatlist-chat[data-peer-id="${peerId}"]`)) as HTMLElement[]; |
|
elements.forEach((element) => { |
|
this.setDialogActive(element, true); |
|
}); |
|
// this.log('peer_changed total time:', performance.now() - perf); |
|
}); |
|
|
|
rootScope.addEventListener('filter_update', async(filter) => { |
|
if(REAL_FOLDERS.has(filter.id)) { |
|
return; |
|
} |
|
|
|
if(!this.filtersRendered[filter.id]) { |
|
this.addFilter(filter); |
|
return; |
|
} else if(filter.id === this.filterId) { // это нет тут смысла вызывать, так как будет dialogs_multiupdate |
|
const dialogs = await this.managers.dialogsStorage.getCachedDialogs(true); |
|
await this.validateListForFilter(); |
|
for(let i = 0, length = dialogs.length; i < length; ++i) { |
|
const dialog = dialogs[i]; |
|
this.updateDialog(dialog); |
|
} |
|
} |
|
|
|
const elements = this.filtersRendered[filter.id]; |
|
setInnerHTML(elements.title, wrapEmojiText(filter.title)); |
|
}); |
|
|
|
rootScope.addEventListener('filter_delete', (filter) => { |
|
const elements = this.filtersRendered[filter.id]; |
|
if(!elements) return; |
|
|
|
// set tab |
|
// (this.folders.menu.firstElementChild.children[Math.max(0, filter.id - 2)] as HTMLElement).click(); |
|
elements.container.remove(); |
|
elements.menu.remove(); |
|
|
|
delete this.sortedLists[filter.id]; |
|
delete this.scrollables[filter.id]; |
|
delete this.filtersRendered[filter.id]; |
|
|
|
this.onFiltersLengthChange(); |
|
|
|
if(this.filterId === filter.id) { |
|
this.selectTab(0, false); |
|
} |
|
}); |
|
|
|
rootScope.addEventListener('filter_order', async(order) => { |
|
order = order.slice(); |
|
indexOfAndSplice(order, FOLDER_ID_ARCHIVE); |
|
|
|
const containerToAppend = this.folders.menu as HTMLElement; |
|
const r = await Promise.all(order.map(async(filterId) => { |
|
return { |
|
indexKey: await this.managers.dialogsStorage.getDialogIndexKeyByFilterId(filterId), |
|
filter: await this.managers.filtersStorage.getFilter(filterId) |
|
}; |
|
})); |
|
|
|
order.forEach((filterId, idx) => { |
|
const {indexKey, filter} = r[idx]; |
|
const renderedFilter = this.filtersRendered[filterId]; |
|
|
|
const sortedList = this.sortedLists[filterId]; |
|
sortedList.indexKey = indexKey; |
|
|
|
positionElementByIndex(renderedFilter.menu, containerToAppend, filter.localId); |
|
positionElementByIndex(renderedFilter.container, this.folders.container, filter.localId); |
|
}); |
|
|
|
this.indexKey = await this.managers.dialogsStorage.getDialogIndexKeyByFilterId(this.filterId); |
|
|
|
/* if(this.filterId) { |
|
const tabIndex = order.indexOf(this.filterId) + 1; |
|
selectTab.prevId = tabIndex; |
|
} */ |
|
}); |
|
|
|
rootScope.addEventListener('peer_typings', async({peerId, typings}) => { |
|
const dialog = await this.managers.appMessagesManager.getDialogOnly(peerId); |
|
if(!dialog) return; |
|
|
|
if(typings.length) { |
|
this.setTyping(dialog); |
|
} else { |
|
this.unsetTyping(dialog); |
|
} |
|
}); |
|
} |
|
|
|
private setDialogActive(listEl: HTMLElement, active: boolean) { |
|
// @ts-ignore |
|
const dom = listEl.dialogDom as DialogDom; |
|
listEl.classList.toggle('active', active); |
|
if(active) { |
|
this.lastActiveElements.add(listEl); |
|
} else { |
|
this.lastActiveElements.delete(listEl); |
|
} |
|
|
|
if(dom?.callIcon) { |
|
dom.callIcon.setActive(active); |
|
} |
|
} |
|
|
|
private async onStateLoaded(state: State) { |
|
const filtersArr = state.filtersArr; |
|
const haveFilters = filtersArr.length > REAL_FOLDERS.size; |
|
// const filter = filtersArr.find((filter) => filter.id !== FOLDER_ID_ARCHIVE); |
|
|
|
const addFilters = (filters: MyDialogFilter[]) => { |
|
for(const filter of filters) { |
|
this.addFilter(filter); |
|
} |
|
}; |
|
|
|
let addFiltersPromise: Promise<any>; |
|
if(haveFilters) { |
|
addFilters(filtersArr); |
|
} else { |
|
addFiltersPromise = this.managers.filtersStorage.getDialogFilters().then(addFilters); |
|
} |
|
|
|
const loadDialogsPromise = this.onChatsScroll(); |
|
await loadDialogsPromise; |
|
this.loadDialogsRenderPromise = undefined; |
|
|
|
addFiltersPromise && await addFiltersPromise; |
|
// this.folders.menu.children[0].classList.add('active'); |
|
|
|
this.filterId = -1; |
|
this.selectTab(0, false); |
|
|
|
if(!this.initedListeners) { |
|
this.initListeners(); |
|
this.initedListeners = true; |
|
} |
|
|
|
haveFilters && this.showFiltersPromise && await this.showFiltersPromise; |
|
|
|
this.managers.appNotificationsManager.getNotifyPeerTypeSettings(); |
|
|
|
await (await loadDialogsPromise).renderPromise.catch(noop); |
|
this.managers.appMessagesManager.fillConversations(); |
|
} |
|
|
|
/* private getOffset(side: 'top' | 'bottom'): {index: number, pos: number} { |
|
if(!this.scroll.loadedAll[side]) { |
|
const element = (side === 'top' ? this.chatList.firstElementChild : this.chatList.lastElementChild) as HTMLElement; |
|
if(element) { |
|
const peerId = element.dataset.peerId; |
|
const dialog = this.managers.appMessagesManager.getDialogByPeerId(peerId); |
|
return {index: dialog[0].index, pos: dialog[1]}; |
|
} |
|
} |
|
|
|
return {index: 0, pos: -1}; |
|
} */ |
|
private getOffsetIndex(side: 'top' | 'bottom') { |
|
return {index: this.scroll.loadedAll[side] ? 0 : this.offsets[side]}; |
|
} |
|
|
|
private isDialogMustBeInViewport(dialog: Dialog) { |
|
if(dialog.migratedTo !== undefined || !this.testDialogForFilter(dialog)) return false; |
|
// return true; |
|
const topOffset = this.getOffsetIndex('top'); |
|
const bottomOffset = this.getOffsetIndex('bottom'); |
|
|
|
if(!topOffset.index && !bottomOffset.index) { |
|
return true; |
|
} |
|
|
|
const index = getDialogIndex(dialog, this.indexKey); |
|
return (!topOffset.index || index <= topOffset.index) && (!bottomOffset.index || index >= bottomOffset.index); |
|
} |
|
|
|
private deleteDialog(peerId: PeerId) { |
|
this.sortedList.delete(peerId); |
|
} |
|
|
|
private updateDialog(dialog: Dialog) { |
|
if(this.isDialogMustBeInViewport(dialog)) { |
|
if(!this.sortedList.has(dialog.peerId)) { |
|
this.sortedList.add(dialog.peerId); |
|
return; |
|
} |
|
} else { |
|
this.deleteDialog(dialog.peerId); |
|
return; |
|
} |
|
|
|
const dom = this.getDialogDom(dialog.peerId); |
|
if(dom) { |
|
this.setLastMessageN({ |
|
dialog, |
|
dom, |
|
setUnread: true |
|
}); |
|
this.sortedList.update(dialog.peerId); |
|
} |
|
} |
|
|
|
public onTabChange = () => { |
|
this.scroll = this.scrollables[this.filterId]; |
|
this.scroll.loadedAll.top = true; |
|
this.scroll.loadedAll.bottom = false; |
|
this.offsets.top = this.offsets.bottom = 0; |
|
this.loadDialogsRenderPromise = undefined; |
|
this.loadDialogsPromise = undefined; |
|
this.sortedList = this.sortedLists[this.filterId]; |
|
return this.onChatsScroll(); |
|
}; |
|
|
|
private async setFilterUnreadCount(filterId: number) { |
|
if(filterId === FOLDER_ID_ALL) { |
|
return; |
|
} |
|
|
|
const unreadSpan = this.filtersRendered[filterId]?.unread; |
|
if(!unreadSpan) { |
|
return; |
|
} |
|
|
|
const {unreadUnmutedCount, unreadCount} = await this.managers.dialogsStorage.getFolderUnreadCount(filterId); |
|
unreadSpan.classList.toggle('badge-gray', !unreadUnmutedCount); |
|
unreadSpan.innerText = unreadCount ? '' + unreadCount : ''; |
|
} |
|
|
|
private setFiltersUnreadCount() { |
|
for(const filterId in this.filtersRendered) { |
|
this.setFilterUnreadCount(+filterId); |
|
} |
|
} |
|
|
|
/** |
|
* Удалит неподходящие чаты из списка, но не добавит их(!) |
|
*/ |
|
private async validateListForFilter() { |
|
this.sortedList.getAll().forEach(async(element) => { |
|
const dialog = await this.managers.appMessagesManager.getDialogOnly(element.id); |
|
if(!this.testDialogForFilter(dialog)) { |
|
this.deleteDialog(element.id); |
|
} |
|
}); |
|
} |
|
|
|
/** |
|
* Удалит неподходящий чат из списка, но не добавит его(!) |
|
*/ |
|
private validateDialogForFilter(dialog: Dialog) { |
|
if(!this.getDialogDom(dialog.peerId)) { |
|
return; |
|
} |
|
|
|
if(!this.testDialogForFilter(dialog)) { |
|
this.deleteDialog(dialog.peerId); |
|
} |
|
} |
|
|
|
public testDialogForFilter(dialog: Dialog) { |
|
if( |
|
!dialog || |
|
(!REAL_FOLDERS.has(this.filterId) ? getDialogIndex(dialog, this.indexKey) === undefined : this.filterId !== dialog.folder_id) |
|
// (filter && !(await this.managers.filtersStorage.testDialogForFilter(dialog, filter))) |
|
) { |
|
return false; |
|
} |
|
|
|
return true; |
|
} |
|
|
|
public generateScrollable(list: HTMLUListElement, filter: Parameters<AppDialogsManager['addFilter']>[0]) { |
|
const filterId = filter.id; |
|
const scrollable = new Scrollable(null, 'CL', 500); |
|
scrollable.container.addEventListener('scroll', this.onChatsRegularScroll); |
|
scrollable.container.dataset.filterId = '' + filterId; |
|
scrollable.onScrolledTop = this.onChatsScrollTop; |
|
scrollable.onScrolledBottom = this.onChatsScroll; |
|
scrollable.setVirtualContainer(list); |
|
|
|
const sortedDialogList = new SortedDialogList( |
|
this.managers, |
|
list, |
|
getDialogIndexKey(filter.localId), |
|
this.onListLengthChange |
|
); |
|
|
|
this.scrollables[filterId] = scrollable; |
|
this.sortedLists[filterId] = sortedDialogList; |
|
|
|
// list.classList.add('hide'); |
|
// scrollable.container.style.backgroundColor = '#' + (Math.random() * (16 ** 6 - 1) | 0).toString(16); |
|
|
|
return scrollable; |
|
} |
|
|
|
private addFilter(filter: Pick<DialogFilter, 'title' | 'id' | 'localId'>) { |
|
if(filter.id === FOLDER_ID_ARCHIVE) { |
|
return; |
|
} |
|
|
|
const containerToAppend = this.folders.menu as HTMLElement; |
|
const renderedFilter = this.filtersRendered[filter.id]; |
|
if(renderedFilter) { |
|
positionElementByIndex(renderedFilter.menu, containerToAppend, filter.localId); |
|
positionElementByIndex(renderedFilter.container, this.folders.container, filter.localId); |
|
return; |
|
} |
|
|
|
const menuTab = document.createElement('div'); |
|
menuTab.classList.add('menu-horizontal-div-item'); |
|
const span = document.createElement('span'); |
|
const titleSpan = document.createElement('span'); |
|
titleSpan.classList.add('text-super'); |
|
if(filter.id === FOLDER_ID_ALL) titleSpan.append(this.allChatsIntlElement.element); |
|
else setInnerHTML(titleSpan, wrapEmojiText(filter.title)); |
|
const unreadSpan = document.createElement('div'); |
|
unreadSpan.classList.add('badge', 'badge-20', 'badge-primary'); |
|
const i = document.createElement('i'); |
|
span.append(titleSpan, unreadSpan, i); |
|
ripple(menuTab); |
|
menuTab.append(span); |
|
|
|
menuTab.dataset.filterId = '' + filter.id; |
|
|
|
positionElementByIndex(menuTab, containerToAppend, filter.localId); |
|
// containerToAppend.append(li); |
|
|
|
const ul = this.createChatList(); |
|
const scrollable = this.generateScrollable(ul, filter); |
|
|
|
scrollable.container.classList.add('tabs-tab', 'chatlist-parts'); |
|
|
|
/* const parts = document.createElement('div'); |
|
parts.classList.add('chatlist-parts'); */ |
|
|
|
const top = document.createElement('div'); |
|
top.classList.add('chatlist-top'); |
|
|
|
const bottom = document.createElement('div'); |
|
bottom.classList.add('chatlist-bottom'); |
|
|
|
top.append(ul); |
|
scrollable.container.append(top, bottom); |
|
/* parts.append(top, bottom); |
|
scrollable.container.append(parts); */ |
|
|
|
const div = scrollable.container; |
|
// this.folders.container.append(div); |
|
positionElementByIndex(scrollable.container, this.folders.container, filter.localId); |
|
|
|
this.setListClickListener(ul, null, true); |
|
|
|
this.filtersRendered[filter.id] = { |
|
menu: menuTab, |
|
container: div, |
|
unread: unreadSpan, |
|
title: titleSpan |
|
}; |
|
|
|
this.onFiltersLengthChange(); |
|
} |
|
|
|
private changeFiltersAllChatsKey() { |
|
const scrollable = this.folders.menuScrollContainer.firstElementChild; |
|
const key: LangPackKey = scrollable.scrollWidth > scrollable.clientWidth ? 'FilterAllChatsShort' : 'FilterAllChats'; |
|
this.allChatsIntlElement.compareAndUpdate({key}); |
|
} |
|
|
|
private onFiltersLengthChange() { |
|
if(!this.showFiltersPromise) { |
|
this.showFiltersPromise = new Promise<void>((resolve) => { |
|
window.setTimeout(() => { |
|
const length = Object.keys(this.filtersRendered).length; |
|
const show = length > 1; |
|
const wasShowing = !this.folders.menuScrollContainer.classList.contains('hide'); |
|
|
|
if(show !== wasShowing) { |
|
this.folders.menuScrollContainer.classList.toggle('hide', !show); |
|
if(show && !wasShowing) { |
|
this.setFiltersUnreadCount(); |
|
} |
|
|
|
this.chatsContainer.classList.toggle('has-filters', show); |
|
} |
|
|
|
this.changeFiltersAllChatsKey(); |
|
|
|
this.showFiltersPromise = undefined; |
|
resolve(); |
|
}, 0); |
|
}); |
|
} |
|
|
|
return this.showFiltersPromise; |
|
} |
|
|
|
private loadDialogs(side: SliceSides) { |
|
/* if(testScroll) { |
|
return; |
|
} */ |
|
|
|
this.log.warn('load', side); |
|
|
|
if(this.loadDialogsPromise || this.loadDialogsRenderPromise/* || 1 === 1 */) return this.loadDialogsPromise; |
|
else if(this.scroll.loadedAll[side]) { |
|
return Promise.resolve({ |
|
cached: true, |
|
renderPromise: Promise.resolve() |
|
}); |
|
} |
|
|
|
const cachedInfoPromise = deferredPromise<boolean>(); |
|
const renderPromise = new Promise<void>(async(resolve, reject) => { |
|
const {chatList, filterId, indexKey} = this; |
|
|
|
// return; |
|
|
|
// let loadCount = 30/*this.chatsLoadCount */; |
|
let loadCount = windowSize.height / 72 * 1.25 | 0; |
|
let offsetIndex = 0; |
|
|
|
const {index: currentOffsetIndex} = this.getOffsetIndex(side); |
|
if(currentOffsetIndex) { |
|
if(side === 'top') { |
|
const storage = await this.managers.dialogsStorage.getFolderDialogs(filterId, true); |
|
const index = storage.findIndex((dialog) => getDialogIndex(dialog, indexKey) <= currentOffsetIndex); |
|
const needIndex = Math.max(0, index - loadCount); |
|
loadCount = index - needIndex; |
|
offsetIndex = getDialogIndex(storage[needIndex], indexKey) + 1; |
|
} else { |
|
offsetIndex = currentOffsetIndex; |
|
} |
|
} |
|
|
|
// let offset = storage[storage.length - 1]?.index || 0; |
|
|
|
let placeholder = this.placeholders[filterId]; |
|
try { |
|
const getConversationsResult = this.managers.acknowledged.appMessagesManager.getConversations('', offsetIndex, loadCount, filterId, true); |
|
if( |
|
!chatList.childElementCount && |
|
!placeholder && |
|
( |
|
!this.loadedDialogsAtLeastOnce || |
|
!(await getConversationsResult).cached |
|
) |
|
) { |
|
placeholder = this.placeholders[filterId] = new DialogsPlaceholder(); |
|
const getRectFrom = filterId === FOLDER_ID_ARCHIVE ? this.chatsContainer : this.folders.container; |
|
placeholder.attach({ |
|
container: chatList.parentElement, |
|
getRectFrom, |
|
onRemove: () => { |
|
delete this.placeholders[filterId]; |
|
}, |
|
blockScrollable: this.scroll |
|
}); |
|
|
|
cachedInfoPromise.resolve(false); |
|
} |
|
|
|
const a = await getConversationsResult; |
|
const result = await a.result; |
|
if(this.loadDialogsRenderPromise !== renderPromise) { |
|
reject(); |
|
cachedInfoPromise.reject(); |
|
return; |
|
} |
|
|
|
cachedInfoPromise.resolve(a.cached); |
|
|
|
// console.timeEnd('getDialogs time'); |
|
|
|
// * loaded all |
|
// if(!result.dialogs.length || chatList.childElementCount === result.count) { |
|
// !result.dialogs.length не подходит, так как при супердревном диалоге getConversations его не выдаст. |
|
// if(chatList.childElementCount === result.count) { |
|
if(side === 'bottom') { |
|
if(result.isEnd) { |
|
this.scroll.loadedAll[side] = true; |
|
} |
|
} else if(result.isTopEnd) { |
|
this.scroll.loadedAll[side] = true; |
|
} |
|
|
|
this.loadedDialogsAtLeastOnce = true; |
|
|
|
if(result.dialogs.length) { |
|
const dialogs = side === 'top' ? result.dialogs.slice().reverse() : result.dialogs; |
|
|
|
const loadPromises: Promise<any>[] = []; |
|
|
|
const callbacks: (() => void)[] = []; |
|
const cccc = (callback: () => void) => { |
|
callbacks.push(callback); |
|
}; |
|
|
|
dialogs.forEach((dialog) => { |
|
// :( |
|
// const isBuggedDialog = !this.managers.appMessagesManager.getDialogOnly(dialog.peerId); |
|
// if(isBuggedDialog) { |
|
// return; |
|
// } |
|
|
|
const element = this.sortedList.add(dialog.peerId, true, /* undefined, false, */cccc, false); |
|
if(element.loadPromises) { |
|
loadPromises.push(...element.loadPromises); |
|
} |
|
}); |
|
|
|
loadPromises.push(fastRafPromise()); // it is needed here |
|
await Promise.all(loadPromises).finally(); |
|
if(this.loadDialogsRenderPromise !== renderPromise) { |
|
reject(); |
|
cachedInfoPromise.reject(); |
|
return; |
|
} |
|
|
|
callbacks.forEach((callback) => callback()); |
|
} else { |
|
this.onListLengthChange(); |
|
} |
|
|
|
const offsetDialog = result.dialogs[side === 'top' ? 0 : result.dialogs.length - 1]; |
|
if(offsetDialog) { |
|
this.offsets[side] = getDialogIndex(offsetDialog, indexKey); |
|
} |
|
|
|
this.log.debug('getDialogs ' + loadCount + ' dialogs by offset:', offsetIndex, result, chatList.childElementCount); |
|
|
|
setTimeout(() => { |
|
this.scroll.onScroll(); |
|
}, 0); |
|
} catch(err) { |
|
this.log.error(err); |
|
} |
|
|
|
if(placeholder) { |
|
// await pause(500); |
|
placeholder.detach(chatList.childElementCount); |
|
} |
|
|
|
resolve(); |
|
}).finally(() => { |
|
if(this.loadDialogsRenderPromise === renderPromise) { |
|
this.loadDialogsRenderPromise = undefined; |
|
this.loadDialogsPromise = undefined; |
|
} |
|
}); |
|
|
|
this.loadDialogsRenderPromise = renderPromise; |
|
return this.loadDialogsPromise = cachedInfoPromise.then((cached) => ({ |
|
cached, |
|
renderPromise |
|
})); |
|
} |
|
|
|
private generateEmptyPlaceholder(options: { |
|
title: LangPackKey, |
|
subtitle?: LangPackKey, |
|
subtitleArgs?: FormatterArguments, |
|
classNameType: string |
|
}) { |
|
const BASE_CLASS = 'empty-placeholder'; |
|
const container = document.createElement('div'); |
|
container.classList.add(BASE_CLASS, BASE_CLASS + '-' + options.classNameType); |
|
|
|
const header = document.createElement('div'); |
|
header.classList.add(BASE_CLASS + '-header'); |
|
_i18n(header, options.title); |
|
|
|
const subtitle = document.createElement('div'); |
|
subtitle.classList.add(BASE_CLASS + '-subtitle'); |
|
if(options.subtitle) { |
|
_i18n(subtitle, options.subtitle, options.subtitleArgs); |
|
} |
|
|
|
container.append(header, subtitle); |
|
|
|
return {container, header, subtitle}; |
|
} |
|
|
|
private checkIfPlaceholderNeeded() { |
|
if(this.filterId === FOLDER_ID_ARCHIVE) { |
|
return; |
|
} |
|
|
|
const chatList = this.chatList; |
|
const part = chatList.parentElement as HTMLElement; |
|
let placeholderContainer = (Array.from(part.children) as HTMLElement[]).find((el) => el.matches('.empty-placeholder')); |
|
const needPlaceholder = this.scroll.loadedAll.bottom && !chatList.childElementCount/* || true */; |
|
// chatList.style.display = 'none'; |
|
|
|
if(needPlaceholder && placeholderContainer) { |
|
return; |
|
} else if(!needPlaceholder) { |
|
if(placeholderContainer) { |
|
part.classList.remove('with-placeholder'); |
|
placeholderContainer.remove(); |
|
} |
|
|
|
return; |
|
} |
|
|
|
let placeholder: ReturnType<AppDialogsManager['generateEmptyPlaceholder']>, type: 'dialogs' | 'folder'; |
|
if(!this.filterId) { |
|
placeholder = this.generateEmptyPlaceholder({ |
|
title: 'ChatList.Main.EmptyPlaceholder.Title', |
|
classNameType: type = 'dialogs' |
|
}); |
|
|
|
placeholderContainer = placeholder.container; |
|
|
|
const img = document.createElement('img'); |
|
img.classList.add('empty-placeholder-dialogs-icon'); |
|
|
|
this.emptyDialogsPlaceholderSubtitle = new I18n.IntlElement({ |
|
element: placeholder.subtitle |
|
}); |
|
|
|
Promise.all([ |
|
this.updateContactsLength(false), |
|
renderImageFromUrlPromise(img, 'assets/img/EmptyChats.svg'), |
|
fastRafPromise() |
|
]).then(([usersLength]) => { |
|
placeholderContainer.classList.add('visible'); |
|
part.classList.toggle('has-contacts', !!usersLength); |
|
}); |
|
|
|
placeholderContainer.prepend(img); |
|
} else { |
|
placeholder = this.generateEmptyPlaceholder({ |
|
title: 'FilterNoChatsToDisplay', |
|
subtitle: 'FilterNoChatsToDisplayInfo', |
|
classNameType: type = 'folder' |
|
}); |
|
|
|
placeholderContainer = placeholder.container; |
|
|
|
const div = document.createElement('div'); |
|
const emoji = '📂'; |
|
const size = 128; |
|
wrapStickerEmoji({ |
|
div, |
|
emoji: emoji, |
|
width: size, |
|
height: size |
|
}); |
|
|
|
placeholderContainer.prepend(div); |
|
|
|
const button = Button('btn-primary btn-color-primary btn-control tgico', { |
|
text: 'FilterHeaderEdit', |
|
icon: 'settings' |
|
}); |
|
|
|
attachClickEvent(button, async() => { |
|
appSidebarLeft.createTab(AppEditFolderTab).open(await this.managers.filtersStorage.getFilter(this.filterId)); |
|
}); |
|
|
|
placeholderContainer.append(button); |
|
} |
|
|
|
part.append(placeholderContainer); |
|
part.classList.add('with-placeholder'); |
|
part.dataset.placeholderType = type; |
|
} |
|
|
|
private updateContactsLength(updatePartClassName: boolean) { |
|
if(this.updateContactsLengthPromise) return this.updateContactsLengthPromise; |
|
return this.updateContactsLengthPromise = this.managers.appUsersManager.getContacts().then((users) => { |
|
const subtitle = this.emptyDialogsPlaceholderSubtitle; |
|
if(subtitle) { |
|
let key: LangPackKey, args: FormatterArguments; |
|
|
|
if(users.length/* && false */) { |
|
key = 'ChatList.Main.EmptyPlaceholder.Subtitle'; |
|
args = [i18n('Contacts.Count', [users.length])]; |
|
} else { |
|
key = 'ChatList.Main.EmptyPlaceholder.SubtitleNoContacts'; |
|
args = []; |
|
} |
|
|
|
subtitle.compareAndUpdate({ |
|
key, |
|
args |
|
}); |
|
} |
|
|
|
if(updatePartClassName) { |
|
const chatList = this.chatList; |
|
const part = chatList.parentElement as HTMLElement; |
|
part.classList.toggle('has-contacts', !!users.length); |
|
} |
|
|
|
this.updateContactsLengthPromise = undefined; |
|
|
|
return users.length; |
|
}); |
|
} |
|
|
|
private removeContactsPlaceholder() { |
|
const chatList = this.chatList; |
|
const parts = chatList.parentElement.parentElement; |
|
const bottom = chatList.parentElement.nextElementSibling as HTMLElement; |
|
parts.classList.remove('with-contacts'); |
|
bottom.innerHTML = ''; |
|
this.loadContacts = undefined; |
|
this.processContact = undefined; |
|
} |
|
|
|
private _onListLengthChange = () => { |
|
if(!this.loadedDialogsAtLeastOnce) { |
|
return; |
|
} |
|
|
|
this.checkIfPlaceholderNeeded(); |
|
|
|
if(this.filterId !== FOLDER_ID_ALL) return; |
|
|
|
const chatList = this.chatList; |
|
const count = chatList.childElementCount; |
|
|
|
const parts = chatList.parentElement.parentElement; |
|
const bottom = chatList.parentElement.nextElementSibling as HTMLElement; |
|
const hasContacts = !!bottom.childElementCount; |
|
if(count >= 10) { |
|
if(hasContacts) { |
|
this.removeContactsPlaceholder(); |
|
} |
|
|
|
return; |
|
} else if(hasContacts) return; |
|
|
|
parts.classList.add('with-contacts'); |
|
|
|
const section = new SettingSection({ |
|
name: 'Contacts', |
|
noDelimiter: true, |
|
fakeGradientDelimiter: true |
|
}); |
|
|
|
section.container.classList.add('hide'); |
|
|
|
this.managers.appUsersManager.getContactsPeerIds(undefined, undefined, 'online').then((contacts) => { |
|
let ready = false; |
|
const onListLengthChange = () => { |
|
if(ready) { |
|
section.container.classList.toggle('hide', !sortedUserList.list.childElementCount); |
|
} |
|
|
|
this.updateContactsLength(true); |
|
}; |
|
|
|
const sortedUserList = new SortedUserList({ |
|
avatarSize: 42, |
|
createChatListOptions: { |
|
dialogSize: 48, |
|
new: true |
|
}, |
|
autonomous: false, |
|
onListLengthChange, |
|
managers: this.managers |
|
}); |
|
|
|
this.loadContacts = () => { |
|
const pageCount = windowSize.height / 60 | 0; |
|
const promise = filterAsync(contacts.splice(0, pageCount), this.verifyPeerIdForContacts); |
|
|
|
promise.then((arr) => { |
|
arr.forEach((peerId) => { |
|
sortedUserList.add(peerId); |
|
}); |
|
}); |
|
|
|
if(!contacts.length) { |
|
this.loadContacts = undefined; |
|
} |
|
}; |
|
|
|
this.loadContacts(); |
|
|
|
this.processContact = async(peerId) => { |
|
if(peerId.isAnyChat()) { |
|
return; |
|
} |
|
|
|
const good = await this.verifyPeerIdForContacts(peerId); |
|
const added = sortedUserList.has(peerId); |
|
if(!added && good) sortedUserList.add(peerId); |
|
else if(added && !good) sortedUserList.delete(peerId); |
|
}; |
|
|
|
const list = sortedUserList.list; |
|
list.classList.add('chatlist-new'); |
|
this.setListClickListener(list); |
|
section.content.append(list); |
|
|
|
ready = true; |
|
onListLengthChange(); |
|
}); |
|
|
|
bottom.append(section.container); |
|
}; |
|
|
|
private verifyPeerIdForContacts = async(peerId: PeerId) => { |
|
return await this.managers.appPeersManager.isContact(peerId) && !(await this.managers.appMessagesManager.getDialogOnly(peerId)); |
|
}; |
|
|
|
public onChatsRegularScroll = () => { |
|
// return; |
|
|
|
if(this.sliceTimeout) clearTimeout(this.sliceTimeout); |
|
this.sliceTimeout = window.setTimeout(() => { |
|
this.sliceTimeout = undefined; |
|
|
|
if(!this.chatList.childElementCount || this.processContact) { |
|
return; |
|
} |
|
|
|
/* const observer = new IntersectionObserver((entries) => { |
|
const |
|
}); |
|
|
|
Array.from(this.chatList.children).forEach((el) => { |
|
observer.observe(el); |
|
}); */ |
|
|
|
fastRafConventional(() => { |
|
const perf = performance.now(); |
|
|
|
const scrollTopWas = this.scroll.scrollTop; |
|
|
|
const firstElementChild = this.chatList.firstElementChild; |
|
const rectContainer = this.scroll.container.getBoundingClientRect(); |
|
const rectTarget = firstElementChild.getBoundingClientRect(); |
|
const children = Array.from(this.scroll.splitUp.children) as HTMLElement[]; |
|
|
|
// const padding = 8; |
|
// const offsetTop = this.folders.container.offsetTop; |
|
let offsetTop = this.scroll.splitUp.offsetTop; |
|
if(offsetTop && scrollTopWas < offsetTop) offsetTop -= scrollTopWas; |
|
// const offsetTop = scrollTopWas < padding ? padding - scrollTopWas : 0; |
|
const firstY = rectContainer.y + offsetTop; |
|
const lastY = rectContainer.y/* - 8 */; // 8px - .chatlist padding-bottom |
|
|
|
const firstElement = findUpTag(document.elementFromPoint(Math.ceil(rectTarget.x), Math.ceil(firstY + 1)), firstElementChild.tagName) as HTMLElement; |
|
const lastElement = findUpTag(document.elementFromPoint(Math.ceil(rectTarget.x), Math.floor(lastY + rectContainer.height - 1)), firstElementChild.tagName) as HTMLElement; |
|
|
|
// alert('got element:' + rect.y); |
|
|
|
if(!firstElement || !lastElement) { |
|
return; |
|
} |
|
|
|
// alert('got element:' + !!firstElement); |
|
|
|
const firstElementRect = firstElement.getBoundingClientRect(); |
|
const elementOverflow = firstElementRect.y - firstY; |
|
|
|
const sliced: HTMLElement[] = []; |
|
const firstIndex = children.indexOf(firstElement); |
|
const lastIndex = children.indexOf(lastElement); |
|
|
|
const saveLength = 10; |
|
|
|
const sliceFromStart = IS_SAFARI ? [] : children.slice(0, Math.max(0, firstIndex - saveLength)); |
|
const sliceFromEnd = children.slice(lastIndex + saveLength); |
|
|
|
/* if(sliceFromStart.length !== sliceFromEnd.length) { |
|
console.log('not equal', sliceFromStart.length, sliceFromEnd.length); |
|
} |
|
|
|
if(sliceFromStart.length > sliceFromEnd.length) { |
|
const diff = sliceFromStart.length - sliceFromEnd.length; |
|
sliceFromStart.splice(0, diff); |
|
} else if(sliceFromEnd.length > sliceFromStart.length) { |
|
const diff = sliceFromEnd.length - sliceFromStart.length; |
|
sliceFromEnd.splice(sliceFromEnd.length - diff, diff); |
|
} */ |
|
|
|
if(sliceFromStart.length) { |
|
this.scroll.loadedAll.top = false; |
|
} |
|
|
|
if(sliceFromEnd.length) { |
|
this.scroll.loadedAll.bottom = false; |
|
} |
|
|
|
sliced.push(...sliceFromStart); |
|
sliced.push(...sliceFromEnd); |
|
|
|
sliced.forEach((el) => { |
|
const peerId = el.dataset.peerId.toPeerId(); |
|
this.deleteDialog(peerId); |
|
}); |
|
|
|
this.setOffsets(); |
|
|
|
// this.log('[slicer] elements', firstElement, lastElement, rect, sliced, sliceFromStart.length, sliceFromEnd.length); |
|
|
|
// this.log('[slicer] reset scrollTop', this.scroll.scrollTop, firstElement.offsetTop, firstElementRect.y, rect.y, elementOverflow); |
|
|
|
// alert('left length:' + children.length); |
|
|
|
this.scroll.scrollTop = firstElement.offsetTop - elementOverflow; |
|
|
|
this.log('slice time', performance.now() - perf); |
|
/* const firstElementRect = firstElement.getBoundingClientRect(); |
|
const scrollTop = */ |
|
|
|
// this.scroll.scrollIntoView(firstElement, false); |
|
}); |
|
}, 200); |
|
}; |
|
|
|
private async setOffsets() { |
|
const chatList = this.chatList; |
|
const firstDialog = await this.getDialogFromElement(chatList.firstElementChild as HTMLElement); |
|
const lastDialog = await this.getDialogFromElement(chatList.lastElementChild as HTMLElement); |
|
|
|
const indexKey = this.indexKey; |
|
this.offsets.top = getDialogIndex(firstDialog, indexKey); |
|
this.offsets.bottom = getDialogIndex(lastDialog, indexKey); |
|
} |
|
|
|
private getDialogFromElement(element: HTMLElement) { |
|
return this.managers.appMessagesManager.getDialogOnly(element.dataset.peerId.toPeerId()); |
|
} |
|
|
|
public onChatsScrollTop = () => { |
|
return this.onChatsScroll('top'); |
|
}; |
|
|
|
public onChatsScroll = (side: SliceSides = 'bottom') => { |
|
if(this.scroll.loadedAll[side]) { |
|
if(this.loadContacts) { |
|
this.loadContacts(); |
|
} |
|
} |
|
|
|
this.log('onChatsScroll', side); |
|
return this.loadDialogs(side); |
|
}; |
|
|
|
public setListClickListener(list: HTMLUListElement, onFound?: () => void, withContext = false, autonomous = false, openInner = false) { |
|
let lastActiveListElement: HTMLElement; |
|
|
|
const setPeerFunc = (openInner ? appImManager.setInnerPeer : appImManager.setPeer).bind(appImManager); |
|
|
|
list.dataset.autonomous = '' + +autonomous; |
|
list.addEventListener('mousedown', (e) => { |
|
if(e.button !== 0) return; |
|
|
|
this.log('dialogs click list'); |
|
const target = e.target as HTMLElement; |
|
const elem = findUpTag(target, DIALOG_LIST_ELEMENT_TAG); |
|
|
|
if(!elem) { |
|
return; |
|
} |
|
|
|
const peerId = elem.dataset.peerId.toPeerId(); |
|
|
|
if(e.ctrlKey || e.metaKey) { |
|
window.open((elem as HTMLAnchorElement).href || ('#' + peerId), '_blank'); |
|
cancelEvent(e); |
|
return; |
|
} |
|
|
|
if(autonomous) { |
|
const sameElement = lastActiveListElement === elem; |
|
if(lastActiveListElement && !sameElement) { |
|
lastActiveListElement.classList.remove('active'); |
|
} |
|
|
|
if(elem) { |
|
elem.classList.add('active'); |
|
lastActiveListElement = elem; |
|
this.lastActiveElements.add(elem); |
|
} |
|
} |
|
|
|
if(elem) { |
|
if(onFound) onFound(); |
|
|
|
const lastMsgId = +elem.dataset.mid || undefined; |
|
|
|
setPeerFunc({ |
|
peerId, lastMsgId |
|
}); |
|
} else { |
|
setPeerFunc(); |
|
} |
|
}, {capture: true}); |
|
|
|
// cancel link click |
|
// ! do not change it to attachClickEvent |
|
list.addEventListener('click', (e) => { |
|
if(e.button === 0) { |
|
cancelEvent(e); |
|
} |
|
}, {capture: true}); |
|
|
|
if(DEBUG) { |
|
list.addEventListener('dblclick', (e) => { |
|
const li = findUpTag(e.target, DIALOG_LIST_ELEMENT_TAG); |
|
if(li) { |
|
const peerId = li.dataset.peerId.toPeerId(); |
|
this.log('debug dialog:', this.managers.appMessagesManager.getDialogByPeerId(peerId)); |
|
} |
|
}); |
|
} |
|
|
|
if(withContext) { |
|
attachContextMenuListener(list, this.contextMenu.onContextMenu); |
|
} |
|
} |
|
|
|
public createChatList(options: { |
|
// avatarSize?: number, |
|
// handheldsSize?: number, |
|
// size?: number, |
|
new?: boolean, |
|
dialogSize?: number, |
|
ignoreClick?: boolean |
|
} = {}) { |
|
const list = document.createElement('ul'); |
|
list.classList.add('chatlist'/* , |
|
'chatlist-avatar-' + (options.avatarSize || 54) *//* , 'chatlist-' + (options.size || 72) */); |
|
|
|
if(options.new) { |
|
list.classList.add('chatlist-new'); |
|
} |
|
|
|
if(options.dialogSize) { |
|
list.classList.add('chatlist-' + options.dialogSize); |
|
} |
|
|
|
// if(options.ignoreClick) { |
|
// list.classList.add('disable-hover'); |
|
// } |
|
|
|
/* if(options.handheldsSize) { |
|
list.classList.add('chatlist-handhelds-' + options.handheldsSize); |
|
} */ |
|
|
|
return list; |
|
} |
|
|
|
public setLastMessageN(options: { |
|
dialog: Dialog, |
|
lastMessage?: Message.message | Message.messageService, |
|
dom?: DialogDom, |
|
highlightWord?: string, |
|
isBatch?: boolean, |
|
setUnread?: boolean |
|
}) { |
|
const promise = this.setLastMessage(options.dialog, options.lastMessage, options.dom, options.highlightWord, options.isBatch, options.setUnread); |
|
return promise.catch(noop); |
|
} |
|
|
|
private async setLastMessage( |
|
dialog: Dialog, |
|
lastMessage: Message.message | Message.messageService, |
|
dom: DialogDom, |
|
highlightWord?: string, |
|
isBatch = false, |
|
setUnread = false |
|
) { |
|
if(!dom) { |
|
dom = this.getDialogDom(dialog.peerId); |
|
|
|
if(!dom) { |
|
return; |
|
} |
|
} |
|
|
|
const {deferred: promise, middleware} = setPromiseMiddleware(dom, 'setLastMessagePromise'); |
|
|
|
let draftMessage: MyDraftMessage; |
|
if(!lastMessage) { |
|
if(dialog.draft?._ === 'draftMessage') { |
|
draftMessage = dialog.draft; |
|
} |
|
|
|
lastMessage = dialog.topMessage; |
|
if(!lastMessage || lastMessage.mid !== dialog.top_message) { |
|
const promise = this.managers.appMessagesManager.getMessageByPeer(dialog.peerId, dialog.top_message); |
|
lastMessage = await middleware(promise); |
|
} |
|
} |
|
|
|
if(setUnread) { |
|
this.setUnreadMessagesN({dialog, dom, isBatch, setLastMessagePromise: promise}); |
|
} |
|
|
|
if(!lastMessage/* || (lastMessage._ === 'messageService' && !lastMessage.rReply) */) { |
|
dom.lastMessageSpan.textContent = ''; |
|
dom.lastTimeSpan.textContent = ''; |
|
delete dom.listEl.dataset.mid; |
|
|
|
promise.resolve(); |
|
return; |
|
} |
|
|
|
const peerId = dialog.peerId; |
|
const isRestricted = lastMessage && isMessageRestricted(lastMessage as Message.message); |
|
|
|
/* if(!dom.lastMessageSpan.classList.contains('user-typing')) */ { |
|
let mediaContainer: HTMLElement; |
|
const willPrepend: (Promise<any> | HTMLElement)[] = []; |
|
if(lastMessage && !draftMessage && !isRestricted) { |
|
const media: MyDocument | MyPhoto = getMediaFromMessage(lastMessage); |
|
const videoTypes: Set<MyDocument['type']> = new Set(['video', 'gif', 'round']); |
|
if(media && (media._ === 'photo' || videoTypes.has(media.type))) { |
|
const size = choosePhotoSize(media, 20, 20); |
|
|
|
if(size._ !== 'photoSizeEmpty') { |
|
mediaContainer = document.createElement('div'); |
|
mediaContainer.classList.add('dialog-subtitle-media'); |
|
|
|
if((media as MyDocument).type === 'round') { |
|
mediaContainer.classList.add('is-round'); |
|
} |
|
|
|
willPrepend.push(wrapPhoto({ |
|
photo: media, |
|
message: lastMessage, |
|
container: mediaContainer, |
|
withoutPreloader: true, |
|
size |
|
}).then(() => mediaContainer)); |
|
|
|
if(videoTypes.has((media as MyDocument).type)) { |
|
const playIcon = document.createElement('span'); |
|
playIcon.classList.add('tgico-play'); |
|
|
|
mediaContainer.append(playIcon); |
|
} |
|
} |
|
} |
|
} |
|
|
|
/* if(lastMessage.from_id === auth.id) { // You: */ |
|
if(draftMessage) { |
|
const bold = document.createElement('b'); |
|
bold.classList.add('danger'); |
|
bold.append(i18n('Draft'), ': '); |
|
willPrepend.unshift(bold); |
|
} else if(peerId.isAnyChat() && peerId !== lastMessage.fromId && !(lastMessage as Message.messageService).action) { |
|
const senderBold = document.createElement('b'); |
|
|
|
if(lastMessage.fromId === rootScope.myId) { |
|
senderBold.append(i18n('FromYou')); |
|
willPrepend.unshift(senderBold); |
|
} else { |
|
// str = sender.first_name || sender.last_name || sender.username; |
|
const p = middleware(wrapPeerTitle({ |
|
peerId: lastMessage.fromId, |
|
onlyFirstName: true |
|
})).then((element) => { |
|
senderBold.prepend(element); |
|
return senderBold; |
|
}, noop); |
|
|
|
willPrepend.unshift(p); |
|
} |
|
|
|
senderBold.append(': '); |
|
// console.log(sender, senderBold.innerText); |
|
} |
|
|
|
const withoutMediaType = !!mediaContainer && !!(lastMessage as Message.message)?.message; |
|
|
|
let fragment: DocumentFragment; |
|
if(highlightWord && (lastMessage as Message.message).message) { |
|
fragment = await middleware(wrapMessageForReply(lastMessage, undefined, undefined, false, highlightWord, withoutMediaType)); |
|
} else if(draftMessage) { |
|
fragment = await middleware(wrapMessageForReply(draftMessage)); |
|
} else if(lastMessage) { |
|
fragment = await middleware(wrapMessageForReply(lastMessage, undefined, undefined, false, undefined, withoutMediaType)); |
|
} else { // rare case |
|
fragment = document.createDocumentFragment(); |
|
} |
|
|
|
if(willPrepend.length) { |
|
const elements = await middleware(Promise.all(willPrepend)); |
|
fragment.prepend(...elements); |
|
} |
|
|
|
replaceContent(dom.lastMessageSpan, fragment); |
|
} |
|
|
|
if(lastMessage || draftMessage/* && lastMessage._ !== 'draftMessage' */) { |
|
const date = draftMessage ? Math.max(draftMessage.date, lastMessage.date || 0) : lastMessage.date; |
|
replaceContent(dom.lastTimeSpan, formatDateAccordingToTodayNew(new Date(date * 1000))); |
|
} else dom.lastTimeSpan.textContent = ''; |
|
|
|
if(setUnread !== null && !setUnread) { // means search |
|
dom.listEl.dataset.mid = '' + lastMessage.mid; |
|
} |
|
|
|
promise.resolve(); |
|
} |
|
|
|
private setUnreadMessagesN(options: { |
|
dialog: Dialog, |
|
dom?: DialogDom, |
|
isBatch?: boolean, |
|
setLastMessagePromise?: Promise<void> |
|
}) { |
|
return this.setUnreadMessages(options.dialog, options.dom, options.isBatch, options.setLastMessagePromise).catch(() => {}); |
|
} |
|
|
|
private async setUnreadMessages( |
|
dialog: Dialog, |
|
dom = this.getDialogDom(dialog.peerId), |
|
isBatch = false, |
|
setLastMessagePromise?: Promise<void> |
|
) { |
|
if(!dom) { |
|
// this.log.error('setUnreadMessages no dom!', dialog); |
|
return; |
|
} |
|
|
|
const {deferred, middleware} = setPromiseMiddleware(dom, 'setUnreadMessagePromise'); |
|
|
|
const isMuted = await middleware(this.managers.appNotificationsManager.isPeerLocalMuted(dialog.peerId, true)); |
|
const wasMuted = dom.listEl.classList.contains('is-muted'); |
|
|
|
let setStatusMessage: MyMessage; |
|
if(dialog.draft?._ !== 'draftMessage') { |
|
const lastMessage: MyMessage = await middleware(this.managers.appMessagesManager.getMessageByPeer(dialog.peerId, dialog.top_message)); |
|
if(lastMessage && lastMessage.pFlags.out && lastMessage.peerId !== rootScope.myId) { |
|
setStatusMessage = lastMessage; |
|
} |
|
} |
|
|
|
const filter = await middleware(this.managers.filtersStorage.getFilter(this.filterId)); |
|
let isPinned: boolean; |
|
if(filter) { |
|
isPinned = filter.pinnedPeerIds.indexOf(dialog.peerId) !== -1; |
|
} else { |
|
isPinned = !!dialog.pFlags.pinned; |
|
} |
|
|
|
const isDialogUnread = await middleware(this.managers.appMessagesManager.isDialogUnread(dialog)); |
|
const hasUnreadBadge = isPinned || isDialogUnread; |
|
// dom.messageEl.classList.toggle('has-badge', hasBadge); |
|
|
|
// * have to await all promises before modifying something |
|
|
|
if(setLastMessagePromise) { |
|
try { |
|
await middleware(setLastMessagePromise); |
|
} catch(err) { |
|
// return; |
|
} |
|
} |
|
|
|
const transitionDuration = isBatch ? 0 : 200; |
|
|
|
if(isMuted !== wasMuted) { |
|
SetTransition(dom.listEl, 'is-muted', isMuted, transitionDuration); |
|
} |
|
|
|
setSendingStatus(dom.statusSpan, setStatusMessage, true); |
|
|
|
const isUnreadBadgeMounted = isInDOM(dom.unreadBadge); |
|
if(hasUnreadBadge && !isUnreadBadgeMounted) { |
|
dom.subtitleEl.append(dom.unreadBadge); |
|
} |
|
|
|
const hasMentionsBadge = dialog.unread_mentions_count && (dialog.unread_mentions_count > 1 || dialog.unread_count > 1); |
|
const isMentionBadgeMounted = dom.mentionsBadge && isInDOM(dom.mentionsBadge); |
|
if(hasMentionsBadge) { |
|
if(!dom.mentionsBadge) { |
|
dom.mentionsBadge = document.createElement('div'); |
|
dom.mentionsBadge.className = 'dialog-subtitle-badge badge badge-24 mention mention-badge'; |
|
dom.mentionsBadge.innerText = '@'; |
|
dom.subtitleEl.insertBefore(dom.mentionsBadge, dom.lastMessageSpan.nextSibling); |
|
} |
|
} |
|
|
|
SetTransition(dom.unreadBadge, 'is-visible', hasUnreadBadge, transitionDuration, hasUnreadBadge ? undefined : () => { |
|
dom.unreadBadge.remove(); |
|
}, !isUnreadBadgeMounted ? 2 : 0); |
|
|
|
if(dom.mentionsBadge) { |
|
SetTransition(dom.mentionsBadge, 'is-visible', hasMentionsBadge, transitionDuration, hasMentionsBadge ? undefined : () => { |
|
dom.mentionsBadge.remove(); |
|
delete dom.mentionsBadge; |
|
}, !isMentionBadgeMounted ? 2 : 0); |
|
} |
|
|
|
if(!hasUnreadBadge) { |
|
deferred.resolve(); |
|
return; |
|
} |
|
|
|
if(isPinned) { |
|
dom.unreadBadge.classList.add('tgico-chatspinned', 'tgico'); |
|
} else { |
|
dom.unreadBadge.classList.remove('tgico-chatspinned', 'tgico'); |
|
} |
|
|
|
let isUnread = true, isMention = false; |
|
if(dialog.unread_mentions_count && dialog.unread_count === 1) { |
|
dom.unreadBadge.innerText = '@'; |
|
isMention = true; |
|
// dom.unreadBadge.classList.add('tgico-mention', 'tgico'); |
|
} else if(isDialogUnread) { |
|
// dom.unreadMessagesSpan.innerText = '' + (dialog.unread_count ? formatNumber(dialog.unread_count, 1) : ' '); |
|
dom.unreadBadge.innerText = '' + (dialog.unread_count || ' '); |
|
} else { |
|
dom.unreadBadge.innerText = ''; |
|
isUnread = false; |
|
} |
|
|
|
dom.unreadBadge.classList.toggle('unread', isUnread); |
|
dom.unreadBadge.classList.toggle('mention', isMention); |
|
deferred.resolve(); |
|
} |
|
|
|
private getDialogDom(peerId: PeerId) { |
|
// return this.doms[peerId]; |
|
const element = this.sortedList.get(peerId); |
|
return element?.dom; |
|
} |
|
|
|
private async getDialog(dialog: Dialog | PeerId) { |
|
if(typeof(dialog) !== 'object') { |
|
const originalDialog = await this.managers.appMessagesManager.getDialogOnly(dialog); |
|
if(!originalDialog) { |
|
const peerId = dialog || NULL_PEER_ID; |
|
return { |
|
peerId, |
|
peer: await this.managers.appPeersManager.getOutputPeer(peerId), |
|
pFlags: {} |
|
} as any as Dialog; |
|
} |
|
|
|
return originalDialog; |
|
} |
|
|
|
return dialog as Dialog; |
|
} |
|
|
|
private setCallStatus(dom: DialogDom, visible: boolean) { |
|
let {callIcon, listEl} = dom; |
|
if(!callIcon && visible) { |
|
const {canvas, startAnimation} = dom.callIcon = callIcon = groupCallActiveIcon(listEl.classList.contains('active')); |
|
canvas.classList.add('dialog-group-call-icon'); |
|
listEl.append(canvas); |
|
startAnimation(); |
|
} |
|
|
|
if(!callIcon) { |
|
return; |
|
} |
|
|
|
SetTransition(dom.callIcon.canvas, 'is-visible', visible, 200, visible ? undefined : () => { |
|
dom.callIcon.canvas.remove(); |
|
dom.callIcon = undefined; |
|
}, visible ? 2 : 0); |
|
} |
|
|
|
public addListDialog(options: Parameters<AppDialogsManager['addDialogNew']>[0] & {isBatch?: boolean}) { |
|
options.autonomous = false; |
|
|
|
const ret = this.addDialogNew(options); |
|
|
|
if(ret) { |
|
const promise = this.getDialog(options.peerId).then((dialog) => { |
|
const {peerId} = dialog; |
|
const promises: Promise<any>[] = []; |
|
if(!peerId.isUser()) { |
|
promises.push(this.processDialogForCallStatus(dialog, ret.dom)); |
|
} |
|
|
|
if(peerId !== rootScope.myId && peerId.isUser()) { |
|
promises.push(this.managers.appUsersManager.getUser(peerId).then((user) => { |
|
if(user.status?._ === 'userStatusOnline') { |
|
this.setOnlineStatus(ret.dom.avatarEl, true); |
|
} |
|
})); |
|
} |
|
|
|
promises.push(this.setLastMessageN({ |
|
dialog, |
|
dom: ret.dom, |
|
isBatch: options.isBatch, |
|
setUnread: true |
|
})); |
|
|
|
return Promise.all(promises); |
|
}); |
|
|
|
if(options.loadPromises) { |
|
options.loadPromises.push(promise); |
|
} |
|
} |
|
|
|
return ret; |
|
} |
|
|
|
private async processDialogForCallStatus(dialog: Dialog, dom?: DialogDom) { |
|
if(!IS_GROUP_CALL_SUPPORTED) { |
|
return; |
|
} |
|
|
|
if(!dom) dom = this.getDialogDom(dialog.peerId); |
|
if(!dom) return; |
|
|
|
const chat: Chat.chat | Chat.channel = await this.managers.appChatsManager.getChat(dialog.peerId.toChatId()); |
|
this.setCallStatus(dom, !!(chat.pFlags.call_active && chat.pFlags.call_not_empty)); |
|
} |
|
|
|
/** |
|
* use for rendering search result |
|
*/ |
|
public addDialogAndSetLastMessage(options: Omit<Parameters<AppDialogsManager['addDialogNew']>[0], 'dialog'> & { |
|
message: MyMessage, |
|
peerId: PeerId, |
|
query?: string |
|
}) { |
|
const {peerId, message, query} = options; |
|
const ret = this.addDialogNew({ |
|
...options, |
|
...getMessageSenderPeerIdOrName(message), |
|
peerId |
|
}); |
|
|
|
this.setLastMessage({_: 'dialog', peerId} as any, message, ret.dom, query); |
|
|
|
if(message.peerId !== peerId) { |
|
ret.dom.listEl.dataset.peerId = '' + message.peerId; |
|
} |
|
|
|
return ret; |
|
} |
|
|
|
public addDialogNew(options: { |
|
peerId: Parameters<AppDialogsManager['addDialog']>[0], |
|
container?: Parameters<AppDialogsManager['addDialog']>[1], |
|
rippleEnabled?: boolean, |
|
onlyFirstName?: boolean, |
|
meAsSaved?: boolean, |
|
append?: boolean, |
|
avatarSize?: number, |
|
autonomous?: boolean, |
|
lazyLoadQueue?: LazyLoadQueue, |
|
loadPromises?: Promise<any>[], |
|
fromName?: string, |
|
noIcons?: boolean |
|
}) { |
|
return this.addDialog(options.peerId, options.container, options.rippleEnabled, options.onlyFirstName, options.meAsSaved, options.append, options.avatarSize, options.autonomous, options.lazyLoadQueue, options.loadPromises, options.fromName, options.noIcons); |
|
} |
|
|
|
public addDialog( |
|
peerId: PeerId, |
|
container?: HTMLElement | Scrollable | DocumentFragment | false, |
|
rippleEnabled = true, |
|
onlyFirstName = false, |
|
meAsSaved = true, |
|
append = true, |
|
avatarSize = 54, |
|
autonomous = !!container, |
|
lazyLoadQueue?: LazyLoadQueue, |
|
loadPromises?: Promise<any>[], |
|
fromName?: string, |
|
noIcons?: boolean |
|
) { |
|
// const dialog = await this.getDialog(_dialog); |
|
const avatarEl = new AvatarElement(); |
|
avatarEl.classList.add('dialog-avatar', 'avatar-' + avatarSize); |
|
avatarEl.updateWithOptions({ |
|
loadPromises, |
|
lazyLoadQueue, |
|
isDialog: !!meAsSaved, |
|
peerId, |
|
peerTitle: fromName |
|
}); |
|
|
|
const captionDiv = document.createElement('div'); |
|
captionDiv.classList.add('user-caption'); |
|
|
|
const titleSpanContainer = document.createElement('span'); |
|
titleSpanContainer.classList.add('user-title'); |
|
|
|
const peerTitle = new PeerTitle(); |
|
const peerTitlePromise = peerTitle.update({ |
|
peerId, |
|
fromName, |
|
dialog: meAsSaved, |
|
onlyFirstName, |
|
plainText: false, |
|
withIcons: !noIcons |
|
}); |
|
|
|
if(loadPromises) { |
|
loadPromises.push(peerTitlePromise); |
|
} |
|
|
|
titleSpanContainer.append(peerTitle.element); |
|
// p.classList.add('') |
|
|
|
// в других случаях иконка верификации не нужна (а первый - это главные чатлисты) |
|
// if(!container) { |
|
|
|
// for muted icon |
|
titleSpanContainer.classList.add('tgico'); // * эта строка будет актуальна только для !container, но ладно |
|
|
|
// const titleIconsPromise = generateTitleIcons(peerId).then((elements) => { |
|
// titleSpanContainer.append(...elements); |
|
// }); |
|
|
|
// if(loadPromises) { |
|
// loadPromises.push(titleIconsPromise); |
|
// } |
|
// } |
|
|
|
const span = document.createElement('span'); |
|
span.classList.add('user-last-message'); |
|
span.setAttribute('dir', 'auto'); |
|
|
|
// captionDiv.append(titleSpan); |
|
// captionDiv.append(span); |
|
|
|
const li = document.createElement(DIALOG_LIST_ELEMENT_TAG); |
|
li.classList.add('chatlist-chat'); |
|
if(!autonomous) (li as HTMLAnchorElement).href = '#' + peerId; |
|
if(rippleEnabled) { |
|
ripple(li); |
|
} |
|
|
|
li.append(avatarEl, captionDiv); |
|
li.dataset.peerId = '' + peerId; |
|
|
|
const statusSpan = document.createElement('span'); |
|
statusSpan.classList.add('message-status', 'sending-status'/* , 'transition', 'reveal' */); |
|
|
|
const lastTimeSpan = document.createElement('span'); |
|
lastTimeSpan.classList.add('message-time'); |
|
|
|
const unreadBadge = document.createElement('div'); |
|
unreadBadge.className = 'dialog-subtitle-badge badge badge-24'; |
|
|
|
const titleP = document.createElement('p'); |
|
titleP.classList.add('dialog-title'); |
|
|
|
const rightSpan = document.createElement('span'); |
|
rightSpan.classList.add('dialog-title-details'); |
|
rightSpan.append(statusSpan, lastTimeSpan); |
|
titleP.append(titleSpanContainer, rightSpan); |
|
|
|
const subtitleEl = document.createElement('p'); |
|
subtitleEl.classList.add('dialog-subtitle'); |
|
subtitleEl.append(span); |
|
|
|
captionDiv.append(titleP, subtitleEl); |
|
|
|
const dom: DialogDom = { |
|
avatarEl, |
|
captionDiv, |
|
titleSpan: peerTitle.element, |
|
titleSpanContainer, |
|
statusSpan, |
|
lastTimeSpan, |
|
unreadBadge, |
|
lastMessageSpan: span, |
|
containerEl: li, |
|
listEl: li, |
|
subtitleEl |
|
}; |
|
|
|
/* let good = false; |
|
for(const folderId in this.chatLists) { |
|
if(this.chatLists[folderId] === container) { |
|
good = true; |
|
} |
|
} */ |
|
if(container) { |
|
const method = append ? 'append' : 'prepend'; |
|
container[method](li); |
|
} |
|
|
|
if(!autonomous) { |
|
// @ts-ignore |
|
li.dialogDom = dom; |
|
|
|
if(appImManager.chat?.peerId === peerId) { |
|
this.setDialogActive(li, true); |
|
} |
|
} |
|
|
|
return {dom}; |
|
} |
|
|
|
public async setTyping(dialog: Dialog) { |
|
const dom = this.getDialogDom(dialog.peerId); |
|
if(!dom) { |
|
return; |
|
} |
|
|
|
const oldTypingElement = dom.lastMessageSpan.querySelector('.peer-typing-container') as HTMLElement; |
|
const newTypingElement = await appImManager.getPeerTyping(dialog.peerId, oldTypingElement); |
|
if(!oldTypingElement && newTypingElement) { |
|
replaceContent(dom.lastMessageSpan, newTypingElement); |
|
dom.lastMessageSpan.classList.add('user-typing'); |
|
} |
|
} |
|
|
|
public unsetTyping(dialog: Dialog) { |
|
const dom = this.getDialogDom(dialog.peerId); |
|
if(!dom) { |
|
return; |
|
} |
|
|
|
dom.lastMessageSpan.classList.remove('user-typing'); |
|
this.setLastMessageN({ |
|
dialog, |
|
lastMessage: null, |
|
dom, |
|
setUnread: null |
|
}); |
|
} |
|
} |
|
|
|
const appDialogsManager = new AppDialogsManager(); |
|
MOUNT_CLASS_TO.appDialogsManager = appDialogsManager; |
|
export default appDialogsManager;
|
|
|