/* * https://github.com/morethanwords/tweb * Copyright (C) 2019-2021 Eduard Kuzmenko * https://github.com/morethanwords/tweb/blob/master/LICENSE */ import appChatsManager, { ChatRights } from "../lib/appManagers/appChatsManager"; import appDialogsManager from "../lib/appManagers/appDialogsManager"; import appMessagesManager, { Dialog } from "../lib/appManagers/appMessagesManager"; import appUsersManager from "../lib/appManagers/appUsersManager"; import rootScope from "../lib/rootScope"; import Scrollable from "./scrollable"; import { FocusDirection } from "../helpers/fastSmoothScroll"; import CheckboxField from "./checkboxField"; import appProfileManager from "../lib/appManagers/appProfileManager"; import { safeAssign } from "../helpers/object"; import { i18n, LangPackKey, _i18n } from "../lib/langPack"; import findUpAttribute from "../helpers/dom/findUpAttribute"; import findUpClassName from "../helpers/dom/findUpClassName"; import PeerTitle from "./peerTitle"; import { cancelEvent } from "../helpers/dom/cancelEvent"; import replaceContent from "../helpers/dom/replaceContent"; import { filterUnique, indexOfAndSplice } from "../helpers/array"; import debounce from "../helpers/schedulers/debounce"; import windowSize from "../helpers/windowSize"; import appPeersManager, { IsPeerType } from "../lib/appManagers/appPeersManager"; type SelectSearchPeerType = 'contacts' | 'dialogs' | 'channelParticipants'; // TODO: правильная сортировка для addMembers, т.е. для peerType: 'contacts', потому что там идут сначала контакты - потом неконтакты, а должно всё сортироваться по имени export default class AppSelectPeers { public container = document.createElement('div'); public list = appDialogsManager.createChatList(/* { handheldsSize: 66, avatarSize: 48 } */); public chatsContainer = document.createElement('div'); public scrollable: Scrollable; public selectedScrollable: Scrollable; public selectedContainer: HTMLElement; public input: HTMLInputElement; //public selected: {[peerId: PeerId]: HTMLElement} = {}; public selected = new Set(); public freezed = false; private folderId = 0; private offsetIndex = 0; private promise: Promise; private query = ''; private cachedContacts: PeerId[]; private loadedWhat: Partial<{[k in 'dialogs' | 'archived' | 'contacts' | 'channelParticipants']: true}> = {}; private renderedPeerIds: Set = new Set(); private appendTo: HTMLElement; private onChange: (length: number) => void; private peerType: SelectSearchPeerType[] = ['dialogs']; private renderResultsFunc: (peerIds: PeerId[]) => void; private chatRightsAction: ChatRights; private multiSelect = true; private rippleEnabled = true; private avatarSize = 48; private exceptSelf = false; private filterPeerTypeBy: IsPeerType[]; private tempIds: {[k in keyof AppSelectPeers['loadedWhat']]: number} = {}; private peerId: PeerId; private placeholder: LangPackKey; private selfPresence: LangPackKey = 'Presence.YourChat'; private needSwitchList = false; constructor(options: { appendTo: AppSelectPeers['appendTo'], onChange?: AppSelectPeers['onChange'], peerType?: AppSelectPeers['peerType'], peerId?: AppSelectPeers['peerId'], onFirstRender?: () => void, renderResultsFunc?: AppSelectPeers['renderResultsFunc'], chatRightsAction?: AppSelectPeers['chatRightsAction'], multiSelect?: AppSelectPeers['multiSelect'], rippleEnabled?: AppSelectPeers['rippleEnabled'], avatarSize?: AppSelectPeers['avatarSize'], placeholder?: AppSelectPeers['placeholder'], selfPresence?: AppSelectPeers['selfPresence'], exceptSelf?: AppSelectPeers['exceptSelf'], filterPeerTypeBy?: AppSelectPeers['filterPeerTypeBy'] }) { safeAssign(this, options); this.container.classList.add('selector'); const f = (this.renderResultsFunc || this.renderResults).bind(this); this.renderResultsFunc = (peerIds) => { if(this.needSwitchList) { this.scrollable.splitUp.replaceWith(this.list); this.scrollable.setVirtualContainer(this.list); this.needSwitchList = false; } peerIds = peerIds.filter(peerId => { const notRendered = !this.renderedPeerIds.has(peerId); if(notRendered) this.renderedPeerIds.add(peerId); return notRendered; }); if(this.filterPeerTypeBy) { peerIds = peerIds.filter(peerId => { if(peerId.isPeerId()) { const peer = appPeersManager.getPeer(peerId); if(!peer.deleted) { return this.filterPeerTypeBy.find(method => appPeersManager[method](peerId)); } } return true; }); } return f(peerIds); }; this.input = document.createElement('input'); this.input.classList.add('selector-search-input'); if(this.placeholder) { _i18n(this.input, this.placeholder, undefined, 'placeholder'); } else { _i18n(this.input, 'SendMessageTo', undefined, 'placeholder'); } this.input.type = 'text'; if(this.multiSelect) { let topContainer = document.createElement('div'); topContainer.classList.add('selector-search-container'); this.selectedContainer = document.createElement('div'); this.selectedContainer.classList.add('selector-search'); this.selectedContainer.append(this.input); topContainer.append(this.selectedContainer); this.selectedScrollable = new Scrollable(topContainer); let delimiter = document.createElement('hr'); this.selectedContainer.addEventListener('click', (e) => { if(this.freezed) return; let target = e.target as HTMLElement; target = findUpClassName(target, 'selector-user'); if(!target) return; const peerId = target.dataset.key; const li = this.chatsContainer.querySelector('[data-peer-id="' + peerId + '"]') as HTMLElement; if(!li) { this.remove(peerId.toPeerId()); } else { li.click(); } }); this.container.append(topContainer, delimiter); } this.chatsContainer.classList.add('chatlist-container'); this.chatsContainer.append(this.list); this.scrollable = new Scrollable(this.chatsContainer); this.scrollable.setVirtualContainer(this.list); this.chatsContainer.addEventListener('click', (e) => { const target = findUpAttribute(e.target, 'data-peer-id') as HTMLElement; cancelEvent(e); if(!target) return; if(this.freezed) return; let key: PeerId | string = target.dataset.peerId; key = key.isPeerId() ? key.toPeerId() : key; if(!this.multiSelect) { this.add(key); return; } //target.classList.toggle('active'); if(this.selected.has(key)) { this.remove(key); } else { this.add(key); } const checkbox = target.querySelector('input') as HTMLInputElement; checkbox.checked = !checkbox.checked; }); const debouncedInput = debounce(this.onInput, 200, false, true); this.input.addEventListener('input', debouncedInput); this.scrollable.onScrolledBottom = () => { this.getMoreResults(); }; this.container.append(this.chatsContainer); this.appendTo.append(this.container); // WARNING TIMEOUT setTimeout(() => { let getResultsPromise = this.getMoreResults() as Promise; if(options.onFirstRender) { getResultsPromise.then(() => { options.onFirstRender(); }); } }, 0); } private onInput = () => { const value = this.input.value; if(this.query !== value) { if(this.peerType.includes('contacts') || this.peerType.includes('dialogs')) { this.cachedContacts = null; } if(this.peerType.includes('dialogs')) { this.folderId = 0; this.offsetIndex = 0; } for(let i in this.tempIds) { // @ts-ignore ++this.tempIds[i]; } this.list = appDialogsManager.createChatList(); this.promise = null; this.loadedWhat = {}; this.query = value; this.renderedPeerIds.clear(); this.needSwitchList = true; //console.log('selectPeers input:', this.query); this.getMoreResults(); } }; private renderSaved() { if(!this.exceptSelf && !this.offsetIndex && this.folderId === 0 && this.peerType.includes('dialogs') && (!this.query || appUsersManager.testSelfSearch(this.query))) { this.renderResultsFunc([rootScope.myId]); } } private getTempId(type: keyof AppSelectPeers['tempIds']) { if(this.tempIds[type] === undefined) { this.tempIds[type] = 0; } return ++this.tempIds[type]; } private async getMoreDialogs(): Promise { if(this.promise) return this.promise; if(this.loadedWhat.dialogs && this.loadedWhat.archived) { return; } // в десктопе - сначала без группы, потом архивные, потом контакты без сообщений const pageCount = windowSize.height / 72 * 1.25 | 0; const tempId = this.getTempId('dialogs'); const promise = appMessagesManager.getConversations(this.query, this.offsetIndex, pageCount, this.folderId, true).promise; this.promise = promise; const value = await promise; if(this.tempIds.dialogs !== tempId) { return; } this.promise = null; let dialogs = value.dialogs as Dialog[]; if(dialogs.length) { const newOffsetIndex = dialogs[dialogs.length - 1].index || 0; dialogs = dialogs.slice(); dialogs.findAndSplice(d => d.peerId === rootScope.myId); // no my account if(this.chatRightsAction) { dialogs = dialogs.filter(d => this.filterByRights(d.peerId)); } this.renderSaved(); this.offsetIndex = newOffsetIndex; } this.renderResultsFunc(dialogs.map(dialog => dialog.peerId)); if(value.isEnd) { if(!this.loadedWhat.dialogs) { this.renderSaved(); this.loadedWhat.dialogs = true; this.offsetIndex = 0; this.folderId = 1; return this.getMoreDialogs(); } else { this.loadedWhat.archived = true; if(!this.loadedWhat.contacts/* && this.peerType.includes('contacts') */) { return this.getMoreContacts(); } } } } private filterByRights(peerId: PeerId) { return ( peerId.isUser() && (this.chatRightsAction !== 'send_messages' || appUsersManager.canSendToUser(peerId)) ) || appChatsManager.hasRights(peerId.toChatId(), this.chatRightsAction); } private async getMoreContacts() { if(this.promise) return this.promise; if(this.loadedWhat.contacts) { return; } const isGlobalSearch = this.peerType.includes('contacts'); if(!this.cachedContacts) { /* const promises: Promise[] = [appUsersManager.getContacts(this.query)]; if(!this.peerType.includes('dialogs')) { promises.push(appMessagesManager.getConversationsAll()); } this.promise = Promise.all(promises); this.cachedContacts = (await this.promise)[0].slice(); */ const tempId = this.getTempId('contacts'); const promise = Promise.all([ isGlobalSearch ? appUsersManager.getContactsPeerIds(this.query) : [], this.query ? appUsersManager.searchContacts(this.query) : undefined ]); this.promise = promise; let [cachedContacts, searchResult] = await promise; if(this.tempIds.contacts !== tempId) { return; } if(searchResult) { // do not add global result if only dialogs needed let resultPeerIds = isGlobalSearch ? searchResult.my_results.concat(searchResult.results) : searchResult.my_results; if(this.chatRightsAction) { resultPeerIds = resultPeerIds.filter(peerId => this.filterByRights(peerId)); } if(!this.peerType.includes('dialogs')) { resultPeerIds = resultPeerIds.filter(peerId => peerId.isUser()); } this.cachedContacts = filterUnique(cachedContacts.concat(resultPeerIds)); } else this.cachedContacts = cachedContacts.slice(); indexOfAndSplice(this.cachedContacts, rootScope.myId); // no my account this.promise = null; } // if(this.cachedContacts.length) { const pageCount = windowSize.height / 72 * 1.25 | 0; const arr = this.cachedContacts.splice(0, pageCount); this.renderResultsFunc(arr); // } if(!this.cachedContacts.length) { this.loadedWhat.contacts = true; // need to load non-contacts /* if(!this.peerType.includes('dialogs')) { return this.getMoreDialogs(); } */ } } private async getMoreChannelParticipants() { if(this.promise) return this.promise; if(this.loadedWhat.channelParticipants) { return; } const pageCount = 50; // same as in group permissions to use cache const tempId = this.getTempId('channelParticipants'); const promise = appProfileManager.getChannelParticipants(this.peerId.toChatId(), {_: 'channelParticipantsSearch', q: this.query}, pageCount, this.list.childElementCount); const participants = await promise; if(this.tempIds.channelParticipants !== tempId) { return; } const peerIds = participants.participants.map(participant => { return appChatsManager.getParticipantPeerId(participant); }); indexOfAndSplice(peerIds, rootScope.myId); this.renderResultsFunc(peerIds); if(this.list.childElementCount >= participants.count || participants.participants.length < pageCount) { this.loadedWhat.channelParticipants = true; } } checkForTriggers = () => { this.scrollable.checkForTriggers(); }; private getMoreResults() { const get = () => { const promises: Promise[] = []; // if(!loadedAllDialogs && (this.peerType.includes('dialogs')/* || this.peerType.includes('contacts') */)) { // if(!loadAllDialogsPromise) { // loadAllDialogsPromise = appMessagesManager.getConversationsAll() // .then(() => { // loadedAllDialogs = true; // }).finally(() => { // loadAllDialogsPromise = null; // }); // } // promises.push(loadAllDialogsPromise); // } if((this.peerType.includes('dialogs')/* || this.loadedWhat.contacts */) && !this.loadedWhat.archived) { // to load non-contacts promises.push(this.getMoreDialogs()); if(!this.loadedWhat.archived) { return promises; } } if((this.peerType.includes('contacts') || this.peerType.includes('dialogs')) && !this.loadedWhat.contacts) { promises.push(this.getMoreContacts()); } if(this.peerType.includes('channelParticipants') && !this.loadedWhat.channelParticipants) { promises.push(this.getMoreChannelParticipants()); } return promises; }; const promises = get(); const promise = Promise.all(promises); if(promises.length) { promise.then(this.checkForTriggers); } return promise; } private renderResults(peerIds: PeerId[]) { //console.log('will renderResults:', peerIds); // оставим только неконтакты с диалогов if(!this.peerType.includes('dialogs') && this.loadedWhat.contacts) { peerIds = peerIds.filter(peerId => { return appUsersManager.isNonContactUser(peerId); }); } peerIds.forEach(peerId => { const {dom} = appDialogsManager.addDialogNew({ dialog: peerId, container: this.scrollable, drawStatus: false, rippleEnabled: this.rippleEnabled, avatarSize: this.avatarSize }); if(this.multiSelect) { const selected = this.selected.has(peerId); const checkboxField = new CheckboxField(); if(selected) { //dom.listEl.classList.add('active'); checkboxField.input.checked = true; } dom.containerEl.prepend(checkboxField.label); } let subtitleEl: HTMLElement; if(peerId.isAnyChat()) { subtitleEl = appProfileManager.getChatMembersString(peerId.toChatId()); } else if(peerId === rootScope.myId) { subtitleEl = i18n(this.selfPresence); } else { subtitleEl = appUsersManager.getUserStatusString(peerId); } dom.lastMessageSpan.append(subtitleEl); }); } public add(key: PeerId | string, title?: string | HTMLElement, scroll = true) { //console.trace('add'); this.selected.add(key); if(!this.multiSelect) { this.onChange(this.selected.size); return; } if(this.query.trim()) { this.input.value = ''; this.onInput(); } const div = document.createElement('div'); div.classList.add('selector-user', 'scale-in'); const avatarEl = document.createElement('avatar-element'); avatarEl.classList.add('selector-user-avatar', 'tgico'); avatarEl.setAttribute('dialog', '1'); avatarEl.classList.add('avatar-32'); div.dataset.key = '' + key; if(key.isPeerId()) { if(title === undefined) { title = new PeerTitle({peerId: key.toPeerId(), dialog: true}).element; } avatarEl.setAttribute('peer', '' + key); } if(title) { if(typeof(title) === 'string') { div.innerHTML = title; } else { replaceContent(div, title); div.append(title); } } div.insertAdjacentElement('afterbegin', avatarEl); this.selectedContainer.insertBefore(div, this.input); //this.selectedScrollable.scrollTop = this.selectedScrollable.scrollHeight; this.onChange && this.onChange(this.selected.size); if(scroll) { this.selectedScrollable.scrollIntoViewNew(this.input, 'center'); } return div; } public remove(key: PeerId | string) { if(!this.multiSelect) return; //const div = this.selected[peerId]; const div = this.selectedContainer.querySelector(`[data-key="${key}"]`) as HTMLElement; div.classList.remove('scale-in'); void div.offsetWidth; div.classList.add('scale-out'); const onAnimationEnd = () => { this.selected.delete(key); div.remove(); this.onChange && this.onChange(this.selected.size); }; if(rootScope.settings.animationsEnabled) { div.addEventListener('animationend', onAnimationEnd, {once: true}); } else { onAnimationEnd(); } } public getSelected() { return [...this.selected]; } public addInitial(values: any[]) { values.forEach(value => { this.add(value, undefined, false); }); window.requestAnimationFrame(() => { // ! not the best place for this raf though it works this.selectedScrollable.scrollIntoViewNew(this.input, 'center', undefined, undefined, FocusDirection.Static); }); } }