diff --git a/src/components/appSearch.ts b/src/components/appSearch.ts index e050f821..5cbba219 100644 --- a/src/components/appSearch.ts +++ b/src/components/appSearch.ts @@ -7,7 +7,6 @@ import appMessagesManager from "../lib/appManagers/appMessagesManager"; import { formatPhoneNumber } from "./misc"; import appChatsManager from "../lib/appManagers/appChatsManager"; import SearchInput from "./searchInput"; -import { Peer } from "../layer"; import rootScope from "../lib/rootScope"; import { escapeRegExp } from "../helpers/string"; import searchIndexManager from "../lib/searchIndexManager"; @@ -237,7 +236,7 @@ export default class AppSearch { }); } - return this.searchPromise = appMessagesManager.getSearch(this.peerID, query, null, maxID, 20, this.offsetRate).then(res => { + return this.searchPromise = appMessagesManager.getSearch(this.peerID, query, {_: 'inputMessagesFilterEmpty'}, maxID, 20, this.offsetRate).then(res => { this.searchPromise = null; if(this.searchInput.value != query) { diff --git a/src/components/appSelectPeers.ts b/src/components/appSelectPeers.ts index 3d672994..c8e38d4d 100644 --- a/src/components/appSelectPeers.ts +++ b/src/components/appSelectPeers.ts @@ -320,7 +320,7 @@ export default class AppSelectPeers { if(this.multiSelect) { const selected = this.selected.has(peerID); - dom.containerEl.insertAdjacentHTML('afterbegin', `
`); + dom.containerEl.insertAdjacentHTML('afterbegin', `
`); if(selected) dom.listEl.classList.add('active'); } diff --git a/src/components/avatar.ts b/src/components/avatar.ts index c57d2326..5f19d73e 100644 --- a/src/components/avatar.ts +++ b/src/components/avatar.ts @@ -3,6 +3,7 @@ import appProfileManager from "../lib/appManagers/appProfileManager"; import rootScope from "../lib/rootScope"; import { cancelEvent } from "../helpers/dom"; import AppMediaViewer, { AppMediaViewerAvatar } from "./appMediaViewer"; +import { Photo } from "../layer"; rootScope.on('avatar_update', (e) => { let peerID = e.detail; @@ -66,6 +67,8 @@ export default class AvatarElement extends HTMLElement { _: 'messageMediaPhoto', photo: photo }, + peerID, + date: (photo as Photo.photo).date, fromID: peerID }; diff --git a/src/components/bubbleGroups.ts b/src/components/bubbleGroups.ts index cd57409b..1d291243 100644 --- a/src/components/bubbleGroups.ts +++ b/src/components/bubbleGroups.ts @@ -1,8 +1,9 @@ import rootScope from "../lib/rootScope"; import { generatePathData } from "../helpers/dom"; +type BubbleGroup = {timestamp: number, fromID: number, mid: number, group: HTMLDivElement[]}; export default class BubbleGroups { - bubblesByGroups: Array<{timestamp: number, fromID: number, mid: number, group: HTMLDivElement[]}> = []; // map to group + bubblesByGroups: Array = []; // map to group groups: Array = []; //updateRAFs: Map = new Map(); newGroupDiff = 120; @@ -62,8 +63,9 @@ export default class BubbleGroups { setClipIfNeeded(bubble: HTMLDivElement, remove = false) { //console.log('setClipIfNeeded', bubble, remove); - if(bubble.classList.contains('is-message-empty')/* && !bubble.classList.contains('is-reply') */ - && (bubble.classList.contains('photo') || bubble.classList.contains('video'))) { + const className = bubble.className; + if(className.includes('is-message-empty')/* && !className.includes('is-reply') */ + && (className.includes('photo') || className.includes('video'))) { let container = bubble.querySelector('.bubble__media-container') as SVGSVGElement; //console.log('setClipIfNeeded', bubble, remove, container); if(!container) return; @@ -78,21 +80,21 @@ export default class BubbleGroups { let path = container.firstElementChild.firstElementChild.lastElementChild as SVGPathElement; let width = +object.getAttributeNS(null, 'width'); let height = +object.getAttributeNS(null, 'height'); - let isOut = bubble.classList.contains('is-out'); - let isReply = bubble.classList.contains('is-reply'); + let isOut = className.includes('is-out'); + let isReply = className.includes('is-reply'); let d = ''; //console.log('setClipIfNeeded', object, width, height, isOut); let tr: number, tl: number; - if(bubble.classList.contains('forwarded') || isReply) { + if(className.includes('forwarded') || isReply) { tr = tl = 0; } else if(isOut) { - tr = bubble.classList.contains('is-group-first') ? 12 : 6; + tr = className.includes('is-group-first') ? 12 : 6; tl = 12; } else { tr = 12; - tl = bubble.classList.contains('is-group-first') ? 12 : 6; + tl = className.includes('is-group-first') ? 12 : 6; } if(isOut) { diff --git a/src/components/chat/audio.ts b/src/components/chat/audio.ts index 0ca025c5..d0058521 100644 --- a/src/components/chat/audio.ts +++ b/src/components/chat/audio.ts @@ -14,7 +14,7 @@ export default class ChatAudio extends PinnedContainer { private toggleEl: HTMLElement; constructor(protected topbar: ChatTopbar, protected chat: Chat, protected appMessagesManager: AppMessagesManager, protected appPeersManager: AppPeersManager) { - super(topbar, chat, 'audio', new DivAndCaption('pinned-audio', (title: string, subtitle: string) => { + super(topbar, chat, topbar.listenerSetter, 'audio', new DivAndCaption('pinned-audio', (title: string, subtitle: string) => { this.divAndCaption.title.innerHTML = title; this.divAndCaption.subtitle.innerHTML = subtitle; }), () => { diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index 37272cf7..0450a12d 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -1,4 +1,4 @@ -import { AppImManager, CHAT_ANIMATION_GROUP } from "../../lib/appManagers/appImManager"; +import { CHAT_ANIMATION_GROUP } from "../../lib/appManagers/appImManager"; import type { AppMessagesManager, Dialog, HistoryResult } from "../../lib/appManagers/appMessagesManager"; import type { AppSidebarRight } from "../sidebarRight"; import type { AppStickersManager } from "../../lib/appManagers/appStickersManager"; @@ -7,22 +7,19 @@ import type { AppInlineBotsManager } from "../../lib/appManagers/AppInlineBotsMa import type { AppPhotosManager } from "../../lib/appManagers/appPhotosManager"; import type { AppDocsManager } from "../../lib/appManagers/appDocsManager"; import type { AppPeersManager } from "../../lib/appManagers/appPeersManager"; -import { findUpClassName, cancelEvent, findUpTag, CLICK_EVENT_NAME, whichChild } from "../../helpers/dom"; +import { findUpClassName, cancelEvent, findUpTag, CLICK_EVENT_NAME, whichChild, getElementByPoint } from "../../helpers/dom"; import { getObjectKeysAndSort } from "../../helpers/object"; import { isTouchSupported } from "../../helpers/touchSupport"; -import { logger, LogLevels } from "../../lib/logger"; +import { logger } from "../../lib/logger"; import rootScope from "../../lib/rootScope"; import AppMediaViewer from "../appMediaViewer"; import BubbleGroups from "../bubbleGroups"; -import Button from "../button"; import PopupDatePicker from "../popupDatepicker"; import PopupForward from "../popupForward"; import PopupStickers from "../popupStickers"; import ProgressivePreloader from "../preloader"; import Scrollable from "../scrollable"; import StickyIntersector from "../stickyIntersector"; -import ChatContextMenu from "./contextMenu"; -import ChatSelection from "./selection"; import animationIntersector from "../animationIntersector"; import { months } from "../../helpers/date"; import RichTextProcessor from "../../lib/richtextprocessor"; @@ -50,8 +47,6 @@ let TEST_SCROLL = TEST_SCROLL_TIMES; export default class ChatBubbles { bubblesContainer: HTMLDivElement; chatInner: HTMLDivElement; - goDownBtn: HTMLButtonElement; - scrollable: Scrollable; scroll: HTMLElement; @@ -59,6 +54,7 @@ export default class ChatBubbles { private getHistoryBottomPromise: Promise; public peerID = 0; + //public messagesCount: number = -1; public unreadOut = new Set(); public needUpdate: {replyMid: number, mid: number}[] = []; // if need wrapSingleMessage @@ -104,6 +100,8 @@ export default class ChatBubbles { public listenerSetter: ListenerSetter; + public replyFollowHistory: number[] = []; + constructor(private chat: Chat, private appMessagesManager: AppMessagesManager, private appSidebarRight: AppSidebarRight, private appStickersManager: AppStickersManager, private appUsersManager: AppUsersManager, private appInlineBotsManager: AppInlineBotsManager, private appPhotosManager: AppPhotosManager, private appDocsManager: AppDocsManager, private appPeersManager: AppPeersManager, private appChatsManager: AppChatsManager) { this.chat.log.error('Bubbles construction'); @@ -115,9 +113,7 @@ export default class ChatBubbles { this.chatInner = document.createElement('div'); this.chatInner.classList.add('bubbles-inner'); - this.goDownBtn = Button('bubbles-go-down btn-corner z-depth-1 hide', {icon: 'down'}); - - this.bubblesContainer.append(this.chatInner, this.goDownBtn); + this.bubblesContainer.append(this.chatInner); this.setScroll(); @@ -209,7 +205,7 @@ export default class ChatBubbles { // set new mids to album items for mediaViewer if(message.grouped_id) { - const items = bubble.querySelectorAll('.album-item'); + const items = bubble.querySelectorAll('.grouped-item'); const groupIDs = getObjectKeysAndSort(appMessagesManager.groupedMessagesStorage[message.grouped_id]); (Array.from(items) as HTMLElement[]).forEach((item, idx) => { item.dataset.mid = '' + groupIDs[idx]; @@ -315,10 +311,25 @@ export default class ChatBubbles { const info = e.detail; const dialog = appMessagesManager.getDialogByPeerID(info.peerID)[0]; - if(dialog) { - if(dialog.peerID == this.peerID) { - this.updateUnreadByDialog(dialog); - } + if(dialog?.peerID == this.peerID) { + this.chat.input.setUnreadCount(); + this.updateUnreadByDialog(dialog); + } + }); + + this.listenerSetter.add(rootScope, 'dialogs_multiupdate', (e) => { + const dialogs = e.detail; + + if(dialogs[this.peerID]) { + this.chat.input.setUnreadCount(); + } + }); + + this.listenerSetter.add(rootScope, 'dialog_notify_settings', (e) => { + const peerID = e.detail; + + if(this.peerID == peerID) { + this.chat.input.setUnreadCount(); } }); @@ -362,7 +373,7 @@ export default class ChatBubbles { } //this.chatSelection.toggleByBubble(bubble); - this.chat.selection.toggleByBubble(findUpClassName(target, 'album-item') || bubble); + this.chat.selection.toggleByBubble(findUpClassName(target, 'grouped-item') || bubble); return; } @@ -494,6 +505,7 @@ export default class ChatBubbles { } catch(err) {} if(isReplyClick && bubble.classList.contains('is-reply')/* || bubble.classList.contains('forwarded') */) { + this.replyFollowHistory.push(+bubble.dataset.mid); let originalMessageID = +bubble.getAttribute('data-original-mid'); this.chat.setPeer(this.peerID, originalMessageID); } @@ -507,18 +519,6 @@ export default class ChatBubbles { //console.log('chatInner click', e); }, {capture: true, passive: false}); - - this.listenerSetter.add(this.goDownBtn, CLICK_EVENT_NAME, (e) => { - cancelEvent(e); - const dialog = appMessagesManager.getDialogByPeerID(this.peerID)[0]; - - if(dialog) { - this.chat.setPeer(this.peerID/* , dialog.top_message */); - } else { - this.log('will scroll down 3'); - this.scroll.scrollTop = this.scroll.scrollHeight; - } - }); this.stickyIntersector = new StickyIntersector(this.scrollable.container, (stuck, target) => { for(const timestamp in this.dateMessages) { @@ -574,7 +574,43 @@ export default class ChatBubbles { }); } - public getAlbumBubble(groupID: string) { + public onGoDownClick() { + if(this.replyFollowHistory.length) { + this.replyFollowHistory.forEachReverse((mid, idx) => { + const bubble = this.bubbles[mid]; + let bad = true; + if(bubble) { + const rect = bubble.getBoundingClientRect(); + bad = (this.appPhotosManager.windowH / 2) > rect.top; + } else { + const message = this.appMessagesManager.getMessage(mid); + if(!message.deleted) { + bad = false; + } + } + + if(bad) { + this.replyFollowHistory.splice(idx, 1); + } + }); + + this.replyFollowHistory.sort((a, b) => b - a); + + const mid = this.replyFollowHistory.pop(); + this.chat.setPeer(this.peerID, mid); + } else { + const dialog = this.appMessagesManager.getDialogByPeerID(this.peerID)[0]; + + if(dialog) { + this.chat.setPeer(this.peerID/* , dialog.top_message */); + } else { + this.log('will scroll down 3'); + this.scroll.scrollTop = this.scroll.scrollHeight; + } + } + } + + public getGroupedBubble(groupID: string) { const group = this.appMessagesManager.groupedMessagesStorage[groupID]; for(const mid in group) { if(this.bubbles[mid]) { @@ -588,18 +624,22 @@ export default class ChatBubbles { return null; } - public getBubbleAlbumItems(bubble: HTMLElement) { - return Array.from(bubble.querySelectorAll('.album-item')) as HTMLElement[]; + public getBubbleGroupedItems(bubble: HTMLElement) { + return Array.from(bubble.querySelectorAll('.grouped-item')) as HTMLElement[]; } public getMountedBubble(mid: number) { const message = this.appMessagesManager.getMessage(mid); - const bubble = this.bubbles[mid]; - if(!bubble && message.grouped_id) { - const a = this.getAlbumBubble(message.grouped_id); - if(a) return a; + if(message.grouped_id) { + const a = this.getGroupedBubble(message.grouped_id); + if(a) { + a.bubble = a.bubble.querySelector(`.document-container[data-mid="${mid}"]`) || a.bubble; + return a; + } } + + const bubble = this.bubbles[mid]; if(!bubble) return; return {bubble, message}; @@ -634,7 +674,7 @@ export default class ChatBubbles { let dialog = this.appMessagesManager.getDialogByPeerID(this.peerID)[0]; // if scroll down after search - if(!top && (!dialog || history.indexOf(dialog.top_message) === -1)) { + if(!top && (!dialog || history.indexOf(dialog.top_message) === -1)/* && this.chat.type == 'chat' */) { this.log('Will load more (down) history by maxID:', history[history.length - 1], history); /* false && */this.getHistory(history[history.length - 1], false, true, undefined, justLoad); } @@ -666,11 +706,13 @@ export default class ChatBubbles { this.scrolledDown = false; } - this.chat.topbar.pinnedMessage.setCorrectIndex(this.scrollable.lastScrollDirection); + if(this.chat.topbar.pinnedMessage) { + this.chat.topbar.pinnedMessage.setCorrectIndex(this.scrollable.lastScrollDirection); + } }; public setScroll() { - this.scrollable = new Scrollable(this.bubblesContainer/* .firstElementChild */ as HTMLElement, 'IM', 300); + this.scrollable = new Scrollable(this.bubblesContainer/* .firstElementChild */ as HTMLElement, 'IM', /* 10300 */300); /* const getScrollOffset = () => { //return Math.round(Math.max(300, appPhotosManager.windowH / 1.5)); @@ -684,8 +726,6 @@ export default class ChatBubbles { this.scrollable = new Scrollable(this.bubblesContainer, 'y', 'IM', this.chatInner, getScrollOffset()); */ this.scroll = this.scrollable.container; - this.bubblesContainer/* .firstElementChild */.append(this.goDownBtn); - this.scrollable.onAdditionalScroll = this.onScroll; this.scrollable.onScrolledTop = () => this.loadMoreHistory(true); this.scrollable.onScrolledBottom = () => this.loadMoreHistory(false); @@ -923,7 +963,6 @@ export default class ChatBubbles { //console.time('appImManager setPeer pre promise'); ////console.time('appImManager: pre render start'); if(peerID == 0) { - this.goDownBtn.classList.add('hide'); this.cleanup(true); this.peerID = 0; return null; @@ -932,14 +971,9 @@ export default class ChatBubbles { const samePeer = this.peerID == peerID; const dialog = this.appMessagesManager.getDialogByPeerID(peerID)[0] || null; - let topMessage = lastMsgID <= 0 ? lastMsgID : dialog?.top_message ?? 0; // убрать + 1 после создания базы референсов + let topMessage = lastMsgID <= 0 ? lastMsgID : dialog?.top_message ?? 0; const isTarget = lastMsgID !== undefined; - // @ts-ignore - /* if(topMessage && dialog && dialog.top_message == topMessage && dialog.refetchTopMessage) { - // @ts-ignore - dialog.refetchTopMessage = false; - topMessage += 1; - } */ + if(!isTarget && dialog) { if(dialog.unread_count && !samePeer) { lastMsgID = dialog.read_inbox_max_id; @@ -948,6 +982,8 @@ export default class ChatBubbles { //lastMsgID = topMessage; } } + + const isJump = lastMsgID != topMessage; if(samePeer) { const mounted = this.getMountedBubble(lastMsgID); @@ -955,20 +991,22 @@ export default class ChatBubbles { if(isTarget) { this.scrollable.scrollIntoView(mounted.bubble); this.highlightBubble(mounted.bubble); - } else if(dialog && lastMsgID == topMessage) { + this.chat.setListenerResult('setPeer', lastMsgID, false); + } else if(dialog && !isJump) { //this.log('will scroll down', this.scroll.scrollTop, this.scroll.scrollHeight); this.scroll.scrollTop = this.scroll.scrollHeight; + this.chat.setListenerResult('setPeer', lastMsgID, true); } return null; } } else { this.peerID = peerID; + this.replyFollowHistory.length = 0; } this.log('setPeer peerID:', this.peerID, dialog, lastMsgID, topMessage); - const isJump = lastMsgID != topMessage; // add last message, bc in getHistory will load < max_id const additionMsgID = isJump ? 0 : topMessage; @@ -978,7 +1016,21 @@ export default class ChatBubbles { //////appSidebarRight.toggleSidebar(true); - const maxBubbleID = samePeer && Math.max(...Object.keys(this.bubbles).map(mid => +mid)); + let maxBubbleID = 0; + if(samePeer) { + let el = getElementByPoint(this.chat.bubbles.scrollable.container, 'bottom'); + //this.chat.log('[PM]: setCorrectIndex: get last element perf:', performance.now() - perf, el); + if(el) { + el = findUpClassName(el, 'bubble'); + if(el) { // TODO: а что делать, если id будет -1, -2, -3? + maxBubbleID = +el.dataset.mid; + } + } + + if(maxBubbleID <= 0) { + maxBubbleID = Math.max(...Object.keys(this.bubbles).map(mid => +mid)); + } + } const oldChatInner = this.chatInner; this.cleanup(); @@ -999,6 +1051,10 @@ export default class ChatBubbles { this.scrollable.container.innerHTML = ''; //oldChatInner.remove(); + if(!samePeer) { + this.chat.finishPeerChange(isTarget, isJump, lastMsgID); + } + this.preloader.attach(this.bubblesContainer); } @@ -1009,6 +1065,10 @@ export default class ChatBubbles { ////this.log('setPeer removing preloader'); if(cached) { + if(!samePeer) { + this.chat.finishPeerChange(isTarget, isJump, lastMsgID); // * костыль + } + this.scrollable.container.innerHTML = ''; //oldChatInner.remove(); } else { @@ -1024,7 +1084,7 @@ export default class ChatBubbles { this.lazyLoadQueue.unlock(); //if(dialog && lastMsgID && lastMsgID != topMessage && (this.bubbles[lastMsgID] || this.firstUnreadBubble)) { - if(dialog && (isTarget || (lastMsgID != topMessage)) && (this.bubbles[lastMsgID] || this.firstUnreadBubble)) { + if(dialog && (isTarget || isJump)) { if(this.scrollable.scrollLocked) { clearTimeout(this.scrollable.scrollLocked); this.scrollable.scrollLocked = 0; @@ -1034,9 +1094,12 @@ export default class ChatBubbles { const forwardingUnread = dialog.read_inbox_max_id == lastMsgID && !isTarget; if(!fromUp && (samePeer || forwardingUnread)) { this.scrollable.scrollTop = this.scrollable.scrollHeight; + } else if(fromUp/* && (samePeer || forwardingUnread) */) { + this.scrollable.scrollTop = 0; } - let bubble: HTMLElement = forwardingUnread ? (this.firstUnreadBubble || this.bubbles[lastMsgID]) : this.bubbles[lastMsgID]; + const mountedByLastMsgID = this.getMountedBubble(lastMsgID); + let bubble: HTMLElement = (forwardingUnread && this.firstUnreadBubble) || mountedByLastMsgID?.bubble; if(!bubble?.parentElement) { bubble = this.findNextMountedBubbleByMsgID(lastMsgID); } @@ -1049,6 +1112,8 @@ export default class ChatBubbles { this.scrollable.scrollTop = this.scrollable.scrollHeight; } + this.chat.setListenerResult('setPeer', lastMsgID, !isJump); + // warning if(!lastMsgID || this.bubbles[topMessage] || lastMsgID == topMessage) { this.scrolledAllDown = true; @@ -1084,7 +1149,6 @@ export default class ChatBubbles { const isAnyGroup = this.appPeersManager.isAnyGroup(peerID); const isChannel = this.appPeersManager.isChannel(peerID); - const isBroadcast = this.appPeersManager.isBroadcast(peerID); const canWrite = this.appMessagesManager.canWriteToPeer(peerID); @@ -1093,11 +1157,6 @@ export default class ChatBubbles { this.chatInner.classList.toggle('is-chat', isAnyGroup || peerID == rootScope.myID); this.chatInner.classList.toggle('is-channel', isChannel); - this.goDownBtn.classList.toggle('is-broadcast', isBroadcast); - - window.requestAnimationFrame(() => { - this.goDownBtn.classList.remove('hide'); - }); } public renderMessagesQueue(message: any, bubble: HTMLDivElement, reverse: boolean) { @@ -1205,8 +1264,9 @@ export default class ChatBubbles { public renderMessage(message: any, reverse = false, multipleRender = false, bubble: HTMLDivElement = null, updatePosition = true) { this.log.debug('message to render:', message); //return; + const albumMustBeRenderedFull = this.chat.type == 'chat'; if(message.deleted) return; - else if(message.grouped_id) { // will render only last album's message + else if(message.grouped_id && albumMustBeRenderedFull) { // will render only last album's message const storage = this.appMessagesManager.groupedMessagesStorage[message.grouped_id]; const maxID = Math.max(...Object.keys(storage).map(i => +i)); if(message.mid < maxID) { @@ -1215,7 +1275,7 @@ export default class ChatBubbles { } const peerID = this.peerID; - const our = message.fromID == rootScope.myID; + const our = message.fromID == rootScope.myID; // * can't use 'message.pFlags.out' here because this check will be used to define side of message (left-right) const messageDiv = document.createElement('div'); messageDiv.classList.add('message'); @@ -1233,7 +1293,7 @@ export default class ChatBubbles { bubble.classList.add('bubble'); bubble.appendChild(bubbleContainer); - if(!our) { + if(!our && !message.pFlags.out) { //this.log('not our message', message, message.pFlags.unread); if(message.pFlags.unread) { this.unreadedObserver.observe(bubble); @@ -1300,7 +1360,9 @@ export default class ChatBubbles { let messageMedia = message.media; let messageMessage: string, totalEntities: any[]; - if(message.grouped_id) { + if(messageMedia?.document && !messageMedia.document.type) { + // * just filter this case + } else if(message.grouped_id && albumMustBeRenderedFull) { const t = this.appMessagesManager.getAlbumText(message.grouped_id); messageMessage = t.message; totalEntities = t.totalEntities; @@ -1436,6 +1498,7 @@ export default class ChatBubbles { } const isOut = our && (!message.fwd_from || this.peerID != rootScope.myID); + let nameContainer = bubbleContainer; // media if(messageMedia/* && messageMedia._ == 'messageMediaPhoto' */) { @@ -1457,7 +1520,7 @@ export default class ChatBubbles { case 'album': { this.log('will wrap pending album'); - bubble.classList.add('hide-name', 'photo', 'is-album'); + bubble.classList.add('hide-name', 'photo', 'is-album', 'is-grouped'); wrapAlbum({ groupID: '' + message.id, attachmentDiv, @@ -1548,8 +1611,8 @@ export default class ChatBubbles { const tailSupported = !isAndroid; const storage = this.appMessagesManager.groupedMessagesStorage[message.grouped_id]; - if(message.grouped_id && Object.keys(storage).length != 1) { - bubble.classList.add('is-album'); + if(message.grouped_id && Object.keys(storage).length != 1 && albumMustBeRenderedFull) { + bubble.classList.add('is-album', 'is-grouped'); wrapAlbum({ groupID: message.grouped_id, attachmentDiv, @@ -1713,8 +1776,8 @@ export default class ChatBubbles { bubble.classList.add('hide-name', doc.type == 'round' ? 'round' : 'video'); const storage = this.appMessagesManager.groupedMessagesStorage[message.grouped_id]; - if(message.grouped_id && Object.keys(storage).length != 1) { - bubble.classList.add('is-album'); + if(message.grouped_id && Object.keys(storage).length != 1 && albumMustBeRenderedFull) { + bubble.classList.add('is-album', 'is-grouped'); wrapAlbum({ groupID: message.grouped_id, @@ -1742,10 +1805,55 @@ export default class ChatBubbles { break; } else { - const docDiv = wrapDocument(doc, false, false, message.mid); - + //const storage = this.appMessagesManager.groupedMessagesStorage[message.grouped_id]; + //const isFullAlbum = storage && Object.keys(storage).length != 1; + const mids = albumMustBeRenderedFull ? this.appMessagesManager.getMidsByMid(message.mid) : [message.mid]; + mids.forEach((mid, idx) => { + const message = this.appMessagesManager.getMessage(mid); + const doc = message.media.document; + const div = wrapDocument(doc, false, false, mid); + + const container = document.createElement('div'); + container.classList.add('document-container'); + container.dataset.mid = '' + mid; + + const wrapper = document.createElement('div'); + wrapper.classList.add('document-wrapper'); + + if(message.message) { + const messageDiv = document.createElement('div'); + messageDiv.classList.add('document-message'); + + const richText = RichTextProcessor.wrapRichText(message.message, { + entities: message.totalEntities + }); + + messageDiv.innerHTML = richText; + wrapper.append(messageDiv); + } + + if(mids.length > 1) { + const selection = document.createElement('div'); + selection.classList.add('document-selection'); + container.append(selection); + + container.classList.add('grouped-item'); + + if(idx === 0) { + nameContainer = wrapper; + } + } + + wrapper.append(div); + container.append(wrapper); + messageDiv.append(container); + }); + + if(mids.length > 1) { + bubble.classList.add('is-multiple-documents', 'is-grouped'); + } + bubble.classList.remove('is-message-empty'); - messageDiv.append(docDiv); messageDiv.classList.add((doc.type != 'photo' ? doc.type || 'document' : 'document') + '-message'); processingWebPage = true; @@ -1818,6 +1926,8 @@ export default class ChatBubbles { } } */ } + + let savedFrom = ''; if((this.peerID < 0 && !our) || message.fwd_from || message.reply_to_mid) { // chat let title = this.appPeersManager.getPeerTitle(message.fwdFromID || message.fromID); @@ -1840,11 +1950,7 @@ export default class ChatBubbles { } if(message.savedFrom) { - let goto = document.createElement('div'); - goto.classList.add('bubble-beside-button', 'goto-original', 'tgico-next'); - bubbleContainer.append(goto); - bubble.dataset.savedFrom = message.savedFrom; - bubble.classList.add('with-beside-button'); + savedFrom = message.savedFrom; } if(!bubble.classList.contains('sticker')) { @@ -1861,7 +1967,7 @@ export default class ChatBubbles { nameDiv.innerHTML = 'Forwarded from ' + title; } - bubbleContainer.append(nameDiv); + nameContainer.append(nameDiv); } } else { if(message.reply_to_mid) { @@ -1895,7 +2001,7 @@ export default class ChatBubbles { nameDiv.innerHTML = title; nameDiv.style.color = this.appPeersManager.getPeerColorByID(message.fromID, false); nameDiv.dataset.peerID = message.fromID; - bubbleContainer.append(nameDiv); + nameContainer.append(nameDiv); } else /* if(!message.reply_to_mid) */ { bubble.classList.add('hide-name'); } @@ -1920,6 +2026,18 @@ export default class ChatBubbles { } else { bubble.classList.add('hide-name'); } + + if(this.chat.type == 'pinned') { + savedFrom = `${this.chat.peerID}_${message.mid}`; + } + + if(savedFrom) { + const goto = document.createElement('div'); + goto.classList.add('bubble-beside-button', 'goto-original', 'tgico-next'); + bubbleContainer.append(goto); + bubble.dataset.savedFrom = savedFrom; + bubble.classList.add('with-beside-button'); + } bubble.classList.add(isOut ? 'is-out' : 'is-in'); if(updatePosition) { @@ -1977,13 +2095,14 @@ export default class ChatBubbles { const method = (reverse ? history.shift : history.pop).bind(history); - //const padding = 99999; + //const padding = 10000; const realLength = this.scrollable.container.childElementCount; let previousScrollHeightMinusTop: number/* , previousScrollHeight: number */; if(realLength > 0 && (reverse || isSafari)) { // for safari need set when scrolling bottom too this.messagesQueueOnRender = () => { const {scrollTop, scrollHeight} = this.scrollable; + //previousScrollHeight = scrollHeight; //previousScrollHeight = scrollHeight + padding; previousScrollHeightMinusTop = reverse ? scrollHeight - scrollTop : scrollTop; @@ -2011,6 +2130,10 @@ export default class ChatBubbles { /* const scrollHeight = this.scrollable.scrollHeight; const addedHeight = scrollHeight - previousScrollHeight; + this.chatInner.style.paddingTop = (10000 - addedHeight) + 'px'; */ + /* const scrollHeight = this.scrollable.scrollHeight; + const addedHeight = scrollHeight - previousScrollHeight; + this.chatInner.style.paddingTop = (padding - addedHeight) + 'px'; //const newScrollTop = reverse ? scrollHeight - previousScrollHeightMinusTop : previousScrollHeightMinusTop; @@ -2054,6 +2177,26 @@ export default class ChatBubbles { }); }; + public requestHistory(maxID: number, loadCount: number, backLimit: number) { + //const middleware = this.getMiddleware(); + if(this.chat.type == 'chat') { + return this.appMessagesManager.getHistory(this.peerID, maxID, loadCount, backLimit); + } else if(this.chat.type == 'pinned') { + const promise = this.appMessagesManager.getSearch(this.peerID, '', {_: 'inputMessagesFilterPinned'}, maxID, loadCount, 0, backLimit); + + /* if(maxID) { + promise.then(result => { + if(!middleware()) return; + + this.messagesCount = result.count; + this.chat.topbar.setTitle(); + }); + } */ + + return promise; + } + } + /** * Load and render history * @param maxID max message id @@ -2099,7 +2242,7 @@ export default class ChatBubbles { } let additionMsgIDs: number[]; - if(additionMsgID) { + if(additionMsgID && !isBackLimit) { const historyStorage = this.appMessagesManager.historiesStorage[peerID]; if(historyStorage && historyStorage.history.length < loadCount) { additionMsgIDs = historyStorage.history.slice(); @@ -2118,7 +2261,7 @@ export default class ChatBubbles { /* const result = additionMsgID ? {history: [additionMsgID]} : appMessagesManager.getHistory(this.peerID, maxID, loadCount, backLimit); */ - let result: ReturnType | {history: number[]} = this.appMessagesManager.getHistory(this.peerID, maxID, loadCount, backLimit); + let result: ReturnType | {history: number[]} = this.requestHistory(maxID, loadCount, backLimit) as any; let resultPromise: Promise; //const isFirstMessageRender = !!additionMsgID && result instanceof Promise && !appMessagesManager.getMessage(additionMsgID).grouped_id; @@ -2251,10 +2394,19 @@ export default class ChatBubbles { // preload more //if(!isFirstMessageRender) { - setTimeout(() => { - this.loadMoreHistory(true, true); - this.loadMoreHistory(false, true); - }, 0); + if(this.chat.type == 'chat') { + const storage = this.appMessagesManager.historiesStorage[peerID]; + const isMaxIDInHistory = storage.history.indexOf(maxID) !== -1; + if(isMaxIDInHistory) { // * otherwise it is a search or jump + setTimeout(() => { + if(reverse) { + this.loadMoreHistory(true, true); + } else { + this.loadMoreHistory(false, true); + } + }, 0); + } + } //} }); diff --git a/src/components/chat/chat.ts b/src/components/chat/chat.ts index 13d81d0e..4dd3f5ad 100644 --- a/src/components/chat/chat.ts +++ b/src/components/chat/chat.ts @@ -10,6 +10,7 @@ import type { AppProfileManager } from "../../lib/appManagers/appProfileManager" import type { AppStickersManager } from "../../lib/appManagers/appStickersManager"; import type { AppUsersManager } from "../../lib/appManagers/appUsersManager"; import type { AppWebPagesManager } from "../../lib/appManagers/appWebPagesManager"; +import EventListenerBase from "../../helpers/eventListenerBase"; import { logger, LogLevels } from "../../lib/logger"; import rootScope from "../../lib/rootScope"; import appSidebarRight, { AppSidebarRight } from "../sidebarRight"; @@ -19,7 +20,11 @@ import ChatInput from "./input"; import ChatSelection from "./selection"; import ChatTopbar from "./topbar"; -export default class Chat { +export type ChatType = 'chat' | 'pinned' | 'replies' | 'discussion'; + +export default class Chat extends EventListenerBase<{ + setPeer: (mid: number, isTopMessage: boolean) => void +}> { public container: HTMLElement; public backgroundEl: HTMLElement; @@ -33,9 +38,13 @@ export default class Chat { public setPeerPromise: Promise; public peerChanged: boolean; - public log: ReturnType; + public log: ReturnType; + + public type: ChatType = 'chat'; constructor(public appImManager: AppImManager, private appChatsManager: AppChatsManager, private appDocsManager: AppDocsManager, private appInlineBotsManager: AppInlineBotsManager, private appMessagesManager: AppMessagesManager, private appPeersManager: AppPeersManager, private appPhotosManager: AppPhotosManager, private appProfileManager: AppProfileManager, private appStickersManager: AppStickersManager, private appUsersManager: AppUsersManager, private appWebPagesManager: AppWebPagesManager, private appSidebarRight: AppSidebarRight, private appPollsManager: AppPollsManager) { + super(); + this.container = document.createElement('div'); this.container.classList.add('chat'); @@ -52,12 +61,25 @@ export default class Chat { } private init() { - this.topbar = new ChatTopbar(this, appSidebarRight, this.appMessagesManager, this.appPeersManager, this.appChatsManager, this.appUsersManager, this.appProfileManager); + this.topbar = new ChatTopbar(this, appSidebarRight, this.appMessagesManager, this.appPeersManager, this.appChatsManager); this.bubbles = new ChatBubbles(this, this.appMessagesManager, this.appSidebarRight, this.appStickersManager, this.appUsersManager, this.appInlineBotsManager, this.appPhotosManager, this.appDocsManager, this.appPeersManager, this.appChatsManager); this.input = new ChatInput(this, this.appMessagesManager, this.appDocsManager, this.appChatsManager, this.appPeersManager, this.appWebPagesManager, this.appImManager); this.selection = new ChatSelection(this.bubbles, this.input, this.appMessagesManager); this.contextMenu = new ChatContextMenu(this.bubbles.bubblesContainer, this, this.appMessagesManager, this.appChatsManager, this.appPeersManager, this.appPollsManager); + if(this.type == 'chat') { + this.topbar.constructPeerHelpers(); + } + + this.topbar.construct(); + this.input.construct(); + + if(this.type == 'chat') { // * гений в деле, разный порядок из-за разной последовательности действий + this.input.constructPeerHelpers(); + } else if(this.type == 'pinned') { + this.input.constructPinnedHelpers(); + } + this.container.append(this.topbar.container, this.bubbles.bubblesContainer, this.input.chatInput); } @@ -126,24 +148,11 @@ export default class Chat { return; } - const {cached, promise} = result; + const {promise} = result; - // clear - if(!cached) { - if(!samePeer) { - this.finishPeerChange(); - } - } - //console.timeEnd('appImManager setPeer pre promise'); - this.setPeerPromise = promise.then(() => { - if(cached) { - if(!samePeer) { - this.finishPeerChange(); - } - } - }).finally(() => { + this.setPeerPromise = promise.finally(() => { if(this.peerID == peerID) { this.setPeerPromise = null; } @@ -155,18 +164,19 @@ export default class Chat { return this.setPeerPromise; } - public finishPeerChange() { + public finishPeerChange(isTarget: boolean, isJump: boolean, lastMsgID: number) { if(this.peerChanged) return; let peerID = this.peerID; this.peerChanged = true; this.topbar.setPeer(peerID); + this.topbar.finishPeerChange(isTarget, isJump, lastMsgID); this.bubbles.finishPeerChange(); this.input.finishPeerChange(); appSidebarRight.sharedMediaTab.fillProfileElements(); - rootScope.broadcast('peer_changed', this.peerID); + rootScope.broadcast('peer_changed', peerID); } } \ No newline at end of file diff --git a/src/components/chat/contextMenu.ts b/src/components/chat/contextMenu.ts index 223e2f4c..692ad4bb 100644 --- a/src/components/chat/contextMenu.ts +++ b/src/components/chat/contextMenu.ts @@ -4,7 +4,6 @@ import type { AppPeersManager } from "../../lib/appManagers/appPeersManager"; import type { AppPollsManager, Poll } from "../../lib/appManagers/appPollsManager"; import type Chat from "./chat"; import { isTouchSupported } from "../../helpers/touchSupport"; -import rootScope from "../../lib/rootScope"; import { attachClickEvent, cancelEvent, cancelSelection, findUpClassName } from "../../helpers/dom"; import ButtonMenu, { ButtonMenuItemOptions } from "../buttonMenu"; import { attachContextMenuListener, openBtnMenu, positionMenu } from "../misc"; @@ -18,7 +17,7 @@ export default class ChatContextMenu { private element: HTMLElement; private target: HTMLElement; - private isTargetAnAlbumItem: boolean; + private isTargetAGroupedItem: boolean; public peerID: number; public msgID: number; @@ -63,10 +62,10 @@ export default class ChatContextMenu { //this.msgID = msgID; this.target = e.target as HTMLElement; - const albumItem = findUpClassName(this.target, 'album-item'); - this.isTargetAnAlbumItem = !!albumItem; - if(albumItem) { - this.msgID = +albumItem.dataset.mid; + const groupedItem = findUpClassName(this.target, 'grouped-item'); + this.isTargetAGroupedItem = !!groupedItem; + if(groupedItem) { + this.msgID = +groupedItem.dataset.mid; } else { this.msgID = mid; } @@ -125,7 +124,7 @@ export default class ChatContextMenu { cancelSelection(); //cancelEvent(e as any); - const bubble = findUpClassName(e.target, 'album-item') || findUpClassName(e.target, 'bubble'); + const bubble = findUpClassName(e.target, 'grouped-item') || findUpClassName(e.target, 'bubble'); if(bubble) { chat.selection.toggleByBubble(bubble); } @@ -138,13 +137,13 @@ export default class ChatContextMenu { icon: 'reply', text: 'Reply', onClick: this.onReplyClick, - verify: () => (this.peerID > 0 || this.appChatsManager.hasRights(-this.peerID, 'send')) && this.msgID > 0/* , + verify: () => (this.peerID > 0 || this.appChatsManager.hasRights(-this.peerID, 'send')) && this.msgID > 0 && !!this.chat.input.messageInput/* , cancelEvent: true */ }, { icon: 'edit', text: 'Edit', onClick: this.onEditClick, - verify: () => this.appMessagesManager.canEditMessage(this.msgID, 'text') + verify: () => this.appMessagesManager.canEditMessage(this.msgID, 'text') && !!this.chat.input.messageInput }, { icon: 'copy', text: 'Copy', @@ -163,15 +162,16 @@ export default class ChatContextMenu { onClick: this.onPinClick, verify: () => { const message = this.appMessagesManager.getMessage(this.msgID); - // for new layer - // return this.msgID > 0 && message._ != 'messageService' && appImManager.pinnedMsgID != this.msgID && (this.peerID > 0 || appChatsManager.hasRights(-this.peerID, 'pin')); - return this.msgID > 0 && message._ != 'messageService' && /* appImManager.pinnedMsgID != this.msgID && */ (this.peerID == rootScope.myID || (this.peerID < 0 && this.appChatsManager.hasRights(-this.peerID, 'pin'))); + return this.msgID > 0 && message._ != 'messageService' && !message.pFlags.pinned && this.appPeersManager.canPinMessage(this.peerID); } }, { icon: 'unpin', text: 'Unpin', onClick: this.onUnpinClick, - verify: () => /* appImManager.pinnedMsgID == this.msgID && */ this.appPeersManager.canPinMessage(this.peerID) + verify: () => { + const message = this.appMessagesManager.getMessage(this.msgID); + return message.pFlags.pinned && this.appPeersManager.canPinMessage(this.peerID); + } }, { icon: 'revote', text: 'Revote', @@ -284,12 +284,12 @@ export default class ChatContextMenu { if(this.chat.selection.isSelecting) { this.chat.selection.selectionForwardBtn.click(); } else { - new PopupForward(this.isTargetAnAlbumItem ? [this.msgID] : this.appMessagesManager.getMidsByMid(this.msgID)); + new PopupForward(this.isTargetAGroupedItem ? [this.msgID] : this.appMessagesManager.getMidsByMid(this.msgID)); } }; private onSelectClick = () => { - this.chat.selection.toggleByBubble(findUpClassName(this.target, 'album-item') || findUpClassName(this.target, 'bubble')); + this.chat.selection.toggleByBubble(findUpClassName(this.target, 'grouped-item') || findUpClassName(this.target, 'bubble')); }; private onClearSelectionClick = () => { @@ -300,7 +300,7 @@ export default class ChatContextMenu { if(this.chat.selection.isSelecting) { this.chat.selection.selectionDeleteBtn.click(); } else { - new PopupDeleteMessages(this.isTargetAnAlbumItem ? [this.msgID] : this.appMessagesManager.getMidsByMid(this.msgID)); + new PopupDeleteMessages(this.isTargetAGroupedItem ? [this.msgID] : this.appMessagesManager.getMidsByMid(this.msgID)); } }; } \ No newline at end of file diff --git a/src/components/chat/input.ts b/src/components/chat/input.ts index b2a9ce27..01d717fa 100644 --- a/src/components/chat/input.ts +++ b/src/components/chat/input.ts @@ -22,12 +22,12 @@ import { toast } from "../toast"; import { wrapReply } from "../wrappers"; import InputField from '../inputField'; import { MessageEntity } from '../../layer'; -import MarkupTooltip from './markupTooltip'; import StickersHelper from './stickersHelper'; import ButtonIcon from '../buttonIcon'; import DivAndCaption from '../divAndCaption'; import ButtonMenuToggle from '../buttonMenuToggle'; import ListenerSetter from '../../helpers/listenerSetter'; +import Button from '../button'; const RECORD_MIN_TIME = 500; const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this group.'; @@ -94,11 +94,19 @@ export default class ChatInput { public stickersHelper: StickersHelper; public listenerSetter: ListenerSetter; + public pinnedControlBtn: HTMLButtonElement; + + public goDownBtn: HTMLButtonElement; + public goDownUnreadBadge: HTMLElement; + constructor(private chat: Chat, private appMessagesManager: AppMessagesManager, private appDocsManager: AppDocsManager, private appChatsManager: AppChatsManager, private appPeersManager: AppPeersManager, private appWebPagesManager: AppWebPagesManager, private appImManager: AppImManager) { this.listenerSetter = new ListenerSetter(); + } + public construct() { this.chatInput = document.createElement('div'); this.chatInput.classList.add('chat-input'); + this.chatInput.style.display = 'none'; this.inputContainer = document.createElement('div'); this.inputContainer.classList.add('chat-input-container'); @@ -106,6 +114,24 @@ export default class ChatInput { this.rowsWrapper = document.createElement('div'); this.rowsWrapper.classList.add('rows-wrapper'); + this.inputContainer.append(this.rowsWrapper); + this.chatInput.append(this.inputContainer); + + this.goDownBtn = Button('bubbles-go-down btn-corner btn-circle z-depth-1 hide', {icon: 'arrow-down'}); + this.goDownUnreadBadge = document.createElement('span'); + this.goDownUnreadBadge.classList.add('badge', 'badge-24', 'badge-green'); + this.goDownBtn.append(this.goDownUnreadBadge); + this.chatInput.append(this.goDownBtn); + + this.listenerSetter.add(this.goDownBtn, CLICK_EVENT_NAME, (e) => { + cancelEvent(e); + this.chat.bubbles.onGoDownClick(); + }); + + // * constructor end + } + + public constructPeerHelpers() { this.replyElements.container = document.createElement('div'); this.replyElements.container.classList.add('reply-wrapper'); @@ -137,7 +163,7 @@ export default class ChatInput { this.willAttachType = 'media'; this.fileInput.click(); }, - verify: (peerID: number) => peerID > 0 || appChatsManager.hasRights(peerID, 'send', 'send_media') + verify: (peerID: number) => peerID > 0 || this.appChatsManager.hasRights(peerID, 'send', 'send_media') }, { icon: 'document', text: 'Document', @@ -147,14 +173,14 @@ export default class ChatInput { this.willAttachType = 'document'; this.fileInput.click(); }, - verify: (peerID: number) => peerID > 0 || appChatsManager.hasRights(peerID, 'send', 'send_media') + verify: (peerID: number) => peerID > 0 || this.appChatsManager.hasRights(peerID, 'send', 'send_media') }, { icon: 'poll', text: 'Poll', onClick: () => { new PopupCreatePoll(this.chat.peerID).show(); }, - verify: (peerID: number) => peerID < 0 && appChatsManager.hasRights(peerID, 'send', 'send_polls') + verify: (peerID: number) => peerID < 0 && this.appChatsManager.hasRights(peerID, 'send', 'send_polls') }]; this.attachMenu = ButtonMenuToggle({noRipple: true, listenerSetter: this.listenerSetter}, 'top-left', this.attachMenuButtons); @@ -189,12 +215,8 @@ export default class ChatInput { this.btnSendContainer.append(this.recordRippleEl, this.btnSend); - this.inputContainer.append(this.rowsWrapper, this.btnCancelRecord, this.btnSendContainer); - this.chatInput.append(this.inputContainer); + this.inputContainer.append(this.btnCancelRecord, this.btnSendContainer); - // * constructor end - - const toggleClass = isTouchSupported ? 'flip-icon' : 'active'; emoticonsDropdown.attachButtonListener(this.btnToggleEmoticons); emoticonsDropdown.events.onOpen.push(this.onEmoticonsOpen); emoticonsDropdown.events.onClose.push(this.onEmoticonsClose); @@ -272,7 +294,7 @@ export default class ChatInput { let peerID = this.chat.peerID; // тут objectURL ставится уже с audio/wav - appMessagesManager.sendFile(peerID, dataBlob, { + this.appMessagesManager.sendFile(peerID, dataBlob, { isVoiceMessage: true, isMedia: true, duration, @@ -290,6 +312,34 @@ export default class ChatInput { this.listenerSetter.add(this.replyElements.container, CLICK_EVENT_NAME, this.onHelperClick); } + public constructPinnedHelpers() { + const container = document.createElement('div'); + container.classList.add('pinned-container'); + + this.pinnedControlBtn = Button('btn-primary btn-transparent pinned-container-button', {icon: 'unpin'}); + container.append(this.pinnedControlBtn); + + this.listenerSetter.add(this.pinnedControlBtn, 'click', () => { + const peerID = this.chat.peerID; + + let promise: Promise; + if(this.appPeersManager.canPinMessage(peerID)) { + promise = this.appMessagesManager.unpinAllMessages(peerID); + } else { + promise = this.appMessagesManager.hidePinnedMessages(peerID); + } + + promise.then(() => { + this.chat.appImManager.setPeer(0); // * close tab + }); + }); + + this.rowsWrapper.append(container); + + this.chatInput.classList.add('type-pinned'); + this.rowsWrapper.classList.add('is-centered'); + } + private onEmoticonsOpen = () => { const toggleClass = isTouchSupported ? 'flip-icon' : 'active'; this.btnToggleEmoticons.classList.toggle(toggleClass, true); @@ -300,6 +350,13 @@ export default class ChatInput { this.btnToggleEmoticons.classList.toggle(toggleClass, false); }; + public setUnreadCount() { + const dialog = this.appMessagesManager.getDialogByPeerID(this.chat.peerID)[0]; + const count = dialog?.unread_count; + this.goDownUnreadBadge.innerText = '' + (count || ''); + this.goDownUnreadBadge.classList.toggle('badge-gray', this.appMessagesManager.isPeerMuted(this.chat.peerID)); + } + public destroy() { this.chat.log.error('Input destroying'); @@ -312,33 +369,54 @@ export default class ChatInput { public cleanup() { if(!this.chat.peerID) { this.chatInput.style.display = 'none'; + this.goDownBtn.classList.add('hide'); } cancelSelection(); - this.clearInput(); - this.clearHelper(); + + if(this.messageInput) { + this.clearInput(); + this.clearHelper(); + } } public finishPeerChange() { const peerID = this.chat.peerID; - const visible = this.attachMenuButtons.filter(button => { - const good = button.verify(peerID); - button.element.classList.toggle('hide', !good); - return good; - }); - - const canWrite = this.appMessagesManager.canWriteToPeer(peerID); this.chatInput.style.display = ''; - this.chatInput.classList.toggle('is-hidden', !canWrite); - if(!canWrite) { - this.messageInput.removeAttribute('contenteditable'); - } else { - this.messageInput.setAttribute('contenteditable', 'true'); + + const isBroadcast = this.appPeersManager.isBroadcast(peerID); + this.goDownBtn.classList.toggle('is-broadcast', isBroadcast); + this.goDownBtn.classList.remove('hide'); + + if(this.goDownUnreadBadge) { + this.setUnreadCount(); + } + + if(this.messageInput) { + const canWrite = this.appMessagesManager.canWriteToPeer(peerID); + this.chatInput.classList.add('no-transition'); + this.chatInput.classList.toggle('is-hidden', !canWrite); + void this.chatInput.offsetLeft; // reflow + this.chatInput.classList.remove('no-transition'); + + const visible = this.attachMenuButtons.filter(button => { + const good = button.verify(peerID); + button.element.classList.toggle('hide', !good); + return good; + }); + + if(!canWrite) { + this.messageInput.removeAttribute('contenteditable'); + } else { + this.messageInput.setAttribute('contenteditable', 'true'); + } + + this.attachMenu.toggleAttribute('disabled', !visible.length); + this.updateSendBtn(); + } else if(this.pinnedControlBtn) { + this.pinnedControlBtn.append(this.appPeersManager.canPinMessage(this.chat.peerID) ? 'Unpin all messages' : 'Don\'t show pinned messages'); } - - this.attachMenu.toggleAttribute('disabled', !visible.length); - this.updateSendBtn(); } private attachMessageInputField() { diff --git a/src/components/chat/messageRender.ts b/src/components/chat/messageRender.ts index e14f3090..a550584f 100644 --- a/src/components/chat/messageRender.ts +++ b/src/components/chat/messageRender.ts @@ -1,5 +1,6 @@ import { getFullDate } from "../../helpers/date"; import { formatNumber } from "../../helpers/number"; +import appImManager from "../../lib/appManagers/appImManager"; import RichTextProcessor from "../../lib/richtextprocessor"; type Message = any; @@ -39,7 +40,7 @@ export namespace MessageRender { time = 'edited ' + time; } - if(message.pFlags.pinned) { + if(appImManager.chat.type != 'pinned' && message.pFlags.pinned) { bubble.classList.add('is-pinned'); time = '' + time; } diff --git a/src/components/chat/pinnedContainer.ts b/src/components/chat/pinnedContainer.ts index d102a321..362a3781 100644 --- a/src/components/chat/pinnedContainer.ts +++ b/src/components/chat/pinnedContainer.ts @@ -4,6 +4,7 @@ import mediaSizes from "../../helpers/mediaSizes"; import { cancelEvent } from "../../helpers/dom"; import DivAndCaption from "../divAndCaption"; import { ripple } from "../ripple"; +import ListenerSetter from "../../helpers/listenerSetter"; const classNames: string[] = []; const CLASSNAME_BASE = 'pinned-container'; @@ -13,7 +14,7 @@ export default class PinnedContainer { private close: HTMLElement; protected wrapper: HTMLElement; - constructor(protected topbar: ChatTopbar, protected chat: Chat, protected className: string, public divAndCaption: DivAndCaption<(title: string, subtitle: string, message?: any) => void>, onClose?: () => void | Promise) { + constructor(protected topbar: ChatTopbar, protected chat: Chat, public listenerSetter: ListenerSetter, protected className: string, public divAndCaption: DivAndCaption<(title: string, subtitle: string, message?: any) => void>, onClose?: () => void | Promise) { /* const prev = this.divAndCaption.fill; this.divAndCaption.fill = (mid, title, subtitle) => { this.divAndCaption.container.dataset.mid = '' + mid; @@ -39,7 +40,7 @@ export default class PinnedContainer { divAndCaption.container.append(this.close, this.wrapper); - this.topbar.listenerSetter.add(this.close, 'click', (e) => { + this.listenerSetter.add(this.close, 'click', (e) => { cancelEvent(e); ((onClose ? onClose() : null) || Promise.resolve(true)).then(needClose => { diff --git a/src/components/chat/pinnedMessage.ts b/src/components/chat/pinnedMessage.ts index c8b351fc..ca82af09 100644 --- a/src/components/chat/pinnedMessage.ts +++ b/src/components/chat/pinnedMessage.ts @@ -1,4 +1,3 @@ -import type { AppImManager } from "../../lib/appManagers/appImManager"; import type { AppMessagesManager } from "../../lib/appManagers/appMessagesManager"; import type { AppPeersManager } from "../../lib/appManagers/appPeersManager"; import type ChatTopbar from "./topbar"; @@ -8,8 +7,10 @@ import PinnedContainer from "./pinnedContainer"; import PinnedMessageBorder from "./pinnedMessageBorder"; import ReplyContainer, { wrapReplyDivAndCaption } from "./replyContainer"; import rootScope from "../../lib/rootScope"; -import { findUpClassName } from "../../helpers/dom"; +import { cancelEvent, findUpClassName, getElementByPoint, handleScrollSideEvent } from "../../helpers/dom"; import Chat from "./chat"; +import ListenerSetter from "../../helpers/listenerSetter"; +import ButtonIcon from "../buttonIcon"; class AnimatedSuper { static DURATION = 200; @@ -191,28 +192,54 @@ class AnimatedCounter { } export default class ChatPinnedMessage { + public static LOAD_COUNT = 50; + public static LOAD_OFFSET = 5; + public pinnedMessageContainer: PinnedContainer; public pinnedMessageBorder: PinnedMessageBorder; - public pinnedIndex = 0; + + public pinnedMaxMid = 0; + public pinnedMid = 0; + public pinnedIndex = -1; public wasPinnedIndex = 0; + public locked = false; public waitForScrollBottom = false; + public count = 0; + public mids: number[] = []; + public offsetIndex = 0; + + public loading = false; + public loadedBottom = false; + public loadedTop = false; + public animatedSubtitle: AnimatedSuper; public animatedMedia: AnimatedSuper; public animatedCounter: AnimatedCounter; + + public listenerSetter: ListenerSetter; + public scrollDownListenerSetter: ListenerSetter = null; + + public hidden = false; + + public getCurrentIndexPromise: Promise = null; + public btnOpen: HTMLButtonElement; constructor(private topbar: ChatTopbar, private chat: Chat, private appMessagesManager: AppMessagesManager, private appPeersManager: AppPeersManager) { - this.pinnedMessageContainer = new PinnedContainer(topbar, chat, 'message', new ReplyContainer('pinned-message'), () => { + this.listenerSetter = new ListenerSetter(); + + this.pinnedMessageContainer = new PinnedContainer(topbar, chat, this.listenerSetter, 'message', new ReplyContainer('pinned-message'), () => { if(appPeersManager.canPinMessage(this.topbar.peerID)) { - new PopupPinMessage(this.topbar.peerID, 0); + new PopupPinMessage(this.topbar.peerID, this.pinnedMid, true); return Promise.resolve(false); + } else { + return this.appMessagesManager.hidePinnedMessages(this.topbar.peerID).then(() => true); } }); this.pinnedMessageBorder = new PinnedMessageBorder(); this.pinnedMessageContainer.divAndCaption.border.replaceWith(this.pinnedMessageBorder.render(1, 0)); - this.topbar.btnJoin.parentElement.insertBefore(this.pinnedMessageContainer.divAndCaption.container, this.topbar.btnJoin); this.animatedSubtitle = new AnimatedSuper(); this.pinnedMessageContainer.divAndCaption.subtitle.append(this.animatedSubtitle.container); @@ -225,90 +252,261 @@ export default class ChatPinnedMessage { this.pinnedMessageContainer.divAndCaption.title.innerHTML = 'Pinned Message '; this.pinnedMessageContainer.divAndCaption.title.append(this.animatedCounter.container); - this.topbar.listenerSetter.add(rootScope, 'peer_pinned_messages', (e) => { + this.btnOpen = ButtonIcon('pinlist pinned-container-close pinned-message-pinlist', {noRipple: true}); + this.pinnedMessageContainer.divAndCaption.container.prepend(this.btnOpen); + + this.listenerSetter.add(this.btnOpen, 'click', (e) => { + cancelEvent(e); + this.topbar.openPinned(true); + }); + + this.listenerSetter.add(rootScope, 'peer_pinned_messages', (e) => { const peerID = e.detail; if(peerID == this.topbar.peerID) { - this.setPinnedMessage(); + //this.wasPinnedIndex = 0; + //setTimeout(() => { + if(this.hidden) { + this.pinnedMessageContainer.toggle(this.hidden = false); + } + + this.loadedTop = this.loadedBottom = false; + this.pinnedIndex = -1; + this.pinnedMid = 0; + this.count = 0; + this.mids = []; + this.offsetIndex = 0; + this.pinnedMaxMid = 0; + this.setCorrectIndex(0); + //}, 300); + } + }); + + this.listenerSetter.add(rootScope, 'peer_pinned_hidden', (e) => { + const {peerID, maxID} = e.detail; + + if(peerID == this.topbar.peerID) { + this.pinnedMessageContainer.toggle(this.hidden = true); } }); } - public setCorrectIndex(lastScrollDirection?: number) { - if(this.locked || this.chat.setPeerPromise) { - return; - }/* else if(this.waitForScrollBottom) { - if(lastScrollDirection === 1) { - this.waitForScrollBottom = false; - } else { - return; - } - } */ + public destroy() { + this.pinnedMessageContainer.divAndCaption.container.remove(); + this.listenerSetter.removeAll(); + this.unsetScrollDownListener(false); + } - ///const perf = performance.now(); - const rect = this.chat.bubbles.scrollable.container.getBoundingClientRect(); - const x = Math.ceil(rect.left + ((rect.right - rect.left) / 2) + 1); - const y = Math.floor(rect.top + rect.height - 1); - let el: HTMLElement = document.elementFromPoint(x, y) as any; - //this.appImManager.log('[PM]: setCorrectIndex: get last element perf:', performance.now() - perf, el, x, y); + public setCorrectIndex(lastScrollDirection?: number) { + if(this.locked || this.hidden/* || this.chat.setPeerPromise || this.chat.bubbles.messagesQueuePromise */) { + return; + } + + if((this.loadedBottom || this.loadedTop) && !this.count) { + return; + } + + //const perf = performance.now(); + let el = getElementByPoint(this.chat.bubbles.scrollable.container, 'bottom'); + //this.chat.log('[PM]: setCorrectIndex: get last element perf:', performance.now() - perf, el); if(!el) return; el = findUpClassName(el, 'bubble'); if(!el) return; - if(el && el.dataset.mid !== undefined) { - const mid = +el.dataset.mid; - this.appMessagesManager.getPinnedMessages(this.topbar.peerID).then(mids => { - let currentIndex = mids.findIndex(_mid => _mid <= mid); - if(currentIndex === -1) { - currentIndex = mids.length ? mids.length - 1 : 0; - } + const mid = el.dataset.mid; + if(el && mid !== undefined) { + this.chat.log('[PM]: setCorrectIndex will test mid:', mid); + this.testMid(+mid, lastScrollDirection); + } + } - //this.appImManager.log('pinned currentIndex', currentIndex); + public testMid(mid: number, lastScrollDirection?: number) { + //if(lastScrollDirection !== undefined) return; + if(this.hidden) return; + + this.chat.log('[PM]: testMid', mid); + + let currentIndex: number = this.mids.findIndex(_mid => _mid <= mid); + if(currentIndex !== -1 && !this.isNeededMore(currentIndex)) { + currentIndex += this.offsetIndex; + } else if(this.loadedTop && mid < this.mids[this.mids.length - 1]) { + //currentIndex = 0; + currentIndex = this.mids.length - 1 + this.offsetIndex; + } else { + if(!this.getCurrentIndexPromise) { + this.getCurrentIndexPromise = this.getCurrentIndex(mid, lastScrollDirection !== undefined); + } + + return; + } + + //const idx = Math.max(0, this.mids.indexOf(mid)); + + /* if(currentIndex == this.count) { + currentIndex = 0; + } */ + + this.chat.log('[PM]: testMid: pinned currentIndex', currentIndex, mid); + + const changed = this.pinnedIndex != currentIndex; + if(changed) { + if(this.waitForScrollBottom && lastScrollDirection !== undefined) { + if(this.pinnedIndex === 0 || this.pinnedIndex > currentIndex) { // если не скроллил вниз и пытается поставить нижний пиннед - выйти + return; + } + } + + this.pinnedIndex = currentIndex; + this.pinnedMid = this.mids.find(_mid => _mid <= mid) || this.mids[this.mids.length - 1]; + this.setPinnedMessage(); + } + } + + private isNeededMore(currentIndex: number) { + return (this.count > ChatPinnedMessage.LOAD_COUNT && + ( + (!this.loadedBottom && currentIndex <= ChatPinnedMessage.LOAD_OFFSET) || + (!this.loadedTop && (this.count - 1 - currentIndex) <= ChatPinnedMessage.LOAD_OFFSET) + ) + ); + } + + private async getCurrentIndex(mid: number, correctAfter = true) { + if(this.loading) return; + this.loading = true; + + try { + let gotRest = false; + const promises = [ + this.appMessagesManager.getSearch(this.topbar.peerID, '', {_: 'inputMessagesFilterPinned'}, mid, ChatPinnedMessage.LOAD_COUNT, 0, ChatPinnedMessage.LOAD_COUNT) + .then(r => { + gotRest = true; + return r; + }) + ]; - const changed = this.pinnedIndex != currentIndex; - if(changed) { - if(this.waitForScrollBottom) { - if(lastScrollDirection === 1) { // если проскроллил вниз - разблокировать - this.waitForScrollBottom = false; - } else if(this.pinnedIndex > currentIndex) { // если не скроллил вниз и пытается поставить нижний пиннед - выйти - return; - } - } + if(!this.pinnedMaxMid) { + const promise = this.appMessagesManager.getPinnedMessage(this.topbar.peerID).then(p => { + if(!p.maxID) return; + this.pinnedMaxMid = p.maxID; - this.pinnedIndex = currentIndex; - this.setPinnedMessage(); - } - }); + if(!gotRest && correctAfter) { + this.mids = [this.pinnedMaxMid]; + this.count = p.count; + this.pinnedIndex = 0; + this.pinnedMid = this.mids[0]; + this.setPinnedMessage(); + //this.pinnedMessageContainer.toggle(false); + } + }); + + promises.push(promise as any); + } + + const result = (await Promise.all(promises))[0]; + + let backLimited = result.history.findIndex(_mid => _mid <= mid); + if(backLimited === -1) { + backLimited = result.history.length; + }/* else { + backLimited -= 1; + } */ + + this.offsetIndex = result.offset_id_offset ? result.offset_id_offset - backLimited : 0; + this.mids = result.history.slice(); + this.count = result.count; + + if(!this.count) { + this.pinnedMessageContainer.toggle(true); + } + + this.loadedTop = (this.offsetIndex + this.mids.length) == this.count; + this.loadedBottom = !this.offsetIndex; + + this.chat.log('[PM]: getCurrentIndex result:', mid, result, backLimited, this.offsetIndex, this.loadedTop, this.loadedBottom); + } catch(err) { + this.chat.log.error('[PM]: getCurrentIndex error', err); + } + + this.loading = false; + + if(this.locked) { + this.testMid(mid); + } else if(correctAfter) { + this.setCorrectIndex(0); + } + + this.getCurrentIndexPromise = null; + //return result.offset_id_offset || 0; + } + + public setScrollDownListener() { + this.waitForScrollBottom = true; + + if(!this.scrollDownListenerSetter) { + this.scrollDownListenerSetter = new ListenerSetter(); + handleScrollSideEvent(this.chat.bubbles.scrollable.container, 'bottom', () => { + this.unsetScrollDownListener(); + }, this.scrollDownListenerSetter); + } + } + + public unsetScrollDownListener(refreshPosition = true) { + this.waitForScrollBottom = false; + + if(this.scrollDownListenerSetter) { + this.scrollDownListenerSetter.removeAll(); + this.scrollDownListenerSetter = null; + } + + if(refreshPosition) { + this.setCorrectIndex(0); + } + } + + public async handleFollowingPinnedMessage() { + this.locked = true; + + this.chat.log('[PM]: handleFollowingPinnedMessage'); + try { + this.setScrollDownListener(); + + const setPeerPromise = this.chat.setPeerPromise; + if(setPeerPromise instanceof Promise) { + await setPeerPromise; + } + + await this.chat.bubbles.scrollable.scrollLockedPromise; + + if(this.getCurrentIndexPromise) { + await this.getCurrentIndexPromise; + } + + this.chat.log('[PM]: handleFollowingPinnedMessage: unlock'); + this.locked = false; + + /* // подождём, пока скролл остановится + setTimeout(() => { + this.chat.log('[PM]: handleFollowingPinnedMessage: unlock'); + this.locked = false; + }, 50); */ + } catch(err) { + this.chat.log.error('[PM]: handleFollowingPinnedMessage error:', err); + + this.locked = false; + this.waitForScrollBottom = false; + this.setCorrectIndex(0); } } public async followPinnedMessage(mid: number) { const message = this.appMessagesManager.getMessage(mid); if(message && !message.deleted) { - this.locked = true; - - try { - const mids = await this.appMessagesManager.getPinnedMessages(message.peerID); - const index = mids.indexOf(mid); - - this.pinnedIndex = index >= (mids.length - 1) ? 0 : index + 1; - this.setPinnedMessage(); - - const setPeerPromise = this.chat.setPeer(message.peerID, mid); - if(setPeerPromise instanceof Promise) { - await setPeerPromise; - } - - await this.chat.bubbles.scrollable.scrollLockedPromise; - } catch(err) { - this.chat.log.error('[PM]: followPinnedMessage error:', err); - } - - // подождём, пока скролл остановится - setTimeout(() => { - this.locked = false; - this.waitForScrollBottom = true; - }, 50); + this.chat.setPeer(this.topbar.peerID, mid); + (this.chat.setPeerPromise || Promise.resolve()).then(() => { // * debounce fast clicker + this.handleFollowingPinnedMessage(); + this.testMid(this.pinnedIndex >= (this.count - 1) ? this.pinnedMaxMid : mid - 1); + }); } } @@ -320,24 +518,24 @@ export default class ChatPinnedMessage { public setPinnedMessage() { /////this.log('setting pinned message', message); //return; - const promise: Promise = this.chat.setPeerPromise || this.chat.bubbles.messagesQueuePromise || Promise.resolve(); + /* const promise: Promise = this.chat.setPeerPromise || this.chat.bubbles.messagesQueuePromise || Promise.resolve(); Promise.all([ - this.appMessagesManager.getPinnedMessages(this.topbar.peerID), promise - ]).then(([mids]) => { + ]).then(() => { */ //const mids = results[0]; - if(mids.length) { - const pinnedIndex = this.pinnedIndex >= mids.length ? mids.length - 1 : this.pinnedIndex; - const message = this.appMessagesManager.getMessage(mids[pinnedIndex]); + const count = this.count; + if(count) { + const pinnedIndex = this.pinnedIndex; + const message = this.appMessagesManager.getMessage(this.pinnedMid); - //this.animatedCounter.prepareNumber(mids.length); + //this.animatedCounter.prepareNumber(count); //setTimeout(() => { const isLast = pinnedIndex === 0; this.animatedCounter.container.classList.toggle('is-last', isLast); //SetTransition(this.animatedCounter.container, 'is-last', isLast, AnimatedSuper.DURATION); if(!isLast) { - this.animatedCounter.setCount(mids.length - pinnedIndex); + this.animatedCounter.setCount(count - pinnedIndex); } //}, 100); @@ -372,13 +570,15 @@ export default class ChatPinnedMessage { } //} - this.pinnedMessageBorder.render(mids.length, mids.length - pinnedIndex - 1); + this.pinnedMessageBorder.render(count, count - pinnedIndex - 1); this.wasPinnedIndex = pinnedIndex; this.pinnedMessageContainer.divAndCaption.container.dataset.mid = '' + message.mid; } else { this.pinnedMessageContainer.toggle(true); this.wasPinnedIndex = 0; } - }); + + this.pinnedMessageContainer.divAndCaption.container.classList.toggle('is-many', this.count > 1); + //}); } } \ No newline at end of file diff --git a/src/components/chat/selection.ts b/src/components/chat/selection.ts index 20dad727..8a45b58f 100644 --- a/src/components/chat/selection.ts +++ b/src/components/chat/selection.ts @@ -28,9 +28,9 @@ export default class ChatSelection { private listenerSetter: ListenerSetter; - constructor(private chatBubbles: ChatBubbles, private chatInput: ChatInput, private appMessagesManager: AppMessagesManager) { - const bubblesContainer = chatBubbles.bubblesContainer; - this.listenerSetter = chatBubbles.listenerSetter; + constructor(private bubbles: ChatBubbles, private input: ChatInput, private appMessagesManager: AppMessagesManager) { + const bubblesContainer = bubbles.bubblesContainer; + this.listenerSetter = bubbles.listenerSetter; if(isTouchSupported) { this.listenerSetter.add(bubblesContainer, 'touchend', (e) => { @@ -49,6 +49,7 @@ export default class ChatSelection { || ( !this.selectedMids.size && !(e.target as HTMLElement).classList.contains('bubble') + && !(e.target as HTMLElement).classList.contains('document-selection') && bubble ) ) { @@ -86,7 +87,7 @@ export default class ChatSelection { /* if(foundTargets.has(e.target as HTMLElement)) return; foundTargets.set(e.target as HTMLElement, true); */ - const bubble = findUpClassName(e.target, 'bubble'); + const bubble = findUpClassName(e.target, 'grouped-item') || findUpClassName(e.target, 'bubble'); if(!bubble) { //console.error('found no bubble', e); return; @@ -96,7 +97,7 @@ export default class ChatSelection { if(!mid) return; // * cancel selecting if selecting message text - if(e.target != bubble && selecting === undefined && !this.selectedMids.size) { + if(e.target != bubble && !(e.target as HTMLElement).classList.contains('document-selection') && selecting === undefined && !this.selectedMids.size) { this.listenerSetter.removeManual(bubblesContainer, 'mousemove', onMouseMove); this.listenerSetter.removeManual(document, 'mouseup', onMouseUp, documentListenerOptions); return; @@ -115,7 +116,7 @@ export default class ChatSelection { if(!this.selectedMids.size) { if(seen.size == 2) { [...seen].forEach(mid => { - const mounted = this.chatBubbles.getMountedBubble(mid); + const mounted = this.bubbles.getMountedBubble(mid); if(mounted) { this.toggleByBubble(mounted.bubble); } @@ -151,7 +152,7 @@ export default class ChatSelection { public toggleBubbleCheckbox(bubble: HTMLElement, show: boolean) { const hasCheckbox = !!this.getCheckboxInputFromBubble(bubble); - const isAlbum = bubble.classList.contains('is-album'); + const isGrouped = bubble.classList.contains('is-grouped'); if(show) { if(hasCheckbox) return; @@ -160,23 +161,44 @@ export default class ChatSelection { // * if it is a render of new message const mid = +bubble.dataset.mid; - if(this.selectedMids.has(mid) && (!isAlbum || this.isAlbumMidsSelected(mid))) { + if(this.selectedMids.has(mid) && (!isGrouped || this.isGroupedMidsSelected(mid))) { checkboxField.input.checked = true; bubble.classList.add('is-selected'); } - bubble.prepend(checkboxField.label); + if(bubble.classList.contains('document-container')) { + bubble.querySelector('.document, audio-element').append(checkboxField.label); + } else { + bubble.prepend(checkboxField.label); + } } else if(hasCheckbox) { - bubble.firstElementChild.remove(); + this.getCheckboxInputFromBubble(bubble).parentElement.remove(); } - if(isAlbum) { - this.chatBubbles.getBubbleAlbumItems(bubble).forEach(item => this.toggleBubbleCheckbox(item, show)); + if(isGrouped) { + this.bubbles.getBubbleGroupedItems(bubble).forEach(item => this.toggleBubbleCheckbox(item, show)); } } - public getCheckboxInputFromBubble(bubble: HTMLElement) { - return bubble.firstElementChild.tagName == 'LABEL' && bubble.firstElementChild.firstElementChild as HTMLInputElement; + public getCheckboxInputFromBubble(bubble: HTMLElement): HTMLInputElement { + /* let perf = performance.now(); + let checkbox = bubble.firstElementChild.tagName == 'LABEL' && bubble.firstElementChild.firstElementChild as HTMLInputElement; + console.log('getCheckboxInputFromBubble firstElementChild time:', performance.now() - perf); + + perf = performance.now(); + checkbox = bubble.querySelector('label input'); + console.log('getCheckboxInputFromBubble querySelector time:', performance.now() - perf); */ + /* let perf = performance.now(); + let contains = bubble.classList.contains('document-container'); + console.log('getCheckboxInputFromBubble classList time:', performance.now() - perf); + + perf = performance.now(); + contains = bubble.className.includes('document-container'); + console.log('getCheckboxInputFromBubble className time:', performance.now() - perf); */ + + return bubble.classList.contains('document-container') ? + bubble.querySelector('label input') : + bubble.firstElementChild.tagName == 'LABEL' && bubble.firstElementChild.firstElementChild as HTMLInputElement; } public updateForwardContainer(forceSelection = false) { @@ -213,7 +235,7 @@ export default class ChatSelection { if(wasSelecting == this.isSelecting) return; - const bubblesContainer = this.chatBubbles.bubblesContainer; + const bubblesContainer = this.bubbles.bubblesContainer; //bubblesContainer.classList.toggle('is-selecting', !!this.selectedMids.size); /* if(bubblesContainer.classList.contains('is-chat-input-hidden')) { @@ -234,7 +256,9 @@ export default class ChatSelection { blurActiveElement(); // * for mobile keyboards - SetTransition(bubblesContainer, 'is-selecting', !!this.selectedMids.size || forceSelection, 200, () => { + const forwards = !!this.selectedMids.size || forceSelection; + SetTransition(this.input.rowsWrapper, 'is-centering', forwards, 200); + SetTransition(bubblesContainer, 'is-selecting', forwards, 200, () => { if(!this.isSelecting) { this.selectionContainer.remove(); this.selectionContainer = this.selectionForwardBtn = this.selectionDeleteBtn = null; @@ -242,7 +266,7 @@ export default class ChatSelection { } window.requestAnimationFrame(() => { - this.chatBubbles.onScroll(); + this.bubbles.onScroll(); }); }); @@ -277,13 +301,13 @@ export default class ChatSelection { this.selectionContainer.append(btnCancel, this.selectionCountEl, this.selectionForwardBtn, this.selectionDeleteBtn); - this.chatInput.rowsWrapper.append(this.selectionContainer); + this.input.rowsWrapper.append(this.selectionContainer); } } if(toggleCheckboxes) { - for(const mid in this.chatBubbles.bubbles) { - const bubble = this.chatBubbles.bubbles[mid]; + for(const mid in this.bubbles.bubbles) { + const bubble = this.bubbles.bubbles[mid]; this.toggleBubbleCheckbox(bubble, this.isSelecting); } } @@ -295,9 +319,10 @@ export default class ChatSelection { public cancelSelection = () => { for(const mid of this.selectedMids) { - const mounted = this.chatBubbles.getMountedBubble(mid); + const mounted = this.bubbles.getMountedBubble(mid); if(mounted) { - this.toggleByBubble(mounted.message.grouped_id ? mounted.bubble.querySelector(`.album-item[data-mid="${mid}"]`) : mounted.bubble); + //this.toggleByBubble(mounted.message.grouped_id ? mounted.bubble.querySelector(`.grouped-item[data-mid="${mid}"]`) : mounted.bubble); + this.toggleByBubble(mounted.bubble); } /* const bubble = this.appImManager.bubbles[mid]; if(bubble) { @@ -325,12 +350,12 @@ export default class ChatSelection { SetTransition(bubble, 'is-selected', isSelected, 200); } - public isAlbumBubbleSelected(bubble: HTMLElement) { - const albumCheckboxInput = this.getCheckboxInputFromBubble(bubble); - return albumCheckboxInput?.checked; + public isGroupedBubbleSelected(bubble: HTMLElement) { + const groupedCheckboxInput = this.getCheckboxInputFromBubble(bubble); + return groupedCheckboxInput?.checked; } - public isAlbumMidsSelected(mid: number) { + public isGroupedMidsSelected(mid: number) { const mids = this.appMessagesManager.getMidsByMid(mid); const selectedMids = mids.filter(mid => this.selectedMids.has(mid)); return mids.length == selectedMids.length; @@ -339,14 +364,14 @@ export default class ChatSelection { public toggleByBubble = (bubble: HTMLElement) => { const mid = +bubble.dataset.mid; - const isAlbum = bubble.classList.contains('is-album'); - if(isAlbum) { - if(!this.isAlbumBubbleSelected(bubble)) { + const isGrouped = bubble.classList.contains('is-grouped'); + if(isGrouped) { + if(!this.isGroupedBubbleSelected(bubble)) { const mids = this.appMessagesManager.getMidsByMid(mid); mids.forEach(mid => this.selectedMids.delete(mid)); } - this.chatBubbles.getBubbleAlbumItems(bubble).forEach(this.toggleByBubble); + this.bubbles.getBubbleGroupedItems(bubble).forEach(this.toggleByBubble); return; } @@ -376,15 +401,15 @@ export default class ChatSelection { this.selectedMids.add(mid); } - const isAlbumItem = bubble.classList.contains('album-item'); - if(isAlbumItem) { - const albumContainer = findUpClassName(bubble, 'bubble'); - const isAlbumSelected = this.isAlbumBubbleSelected(albumContainer); - const isAlbumMidsSelected = this.isAlbumMidsSelected(mid); + const isGroupedItem = bubble.classList.contains('grouped-item'); + if(isGroupedItem) { + const groupContainer = findUpClassName(bubble, 'bubble'); + const isGroupedSelected = this.isGroupedBubbleSelected(groupContainer); + const isGroupedMidsSelected = this.isGroupedMidsSelected(mid); - const willChange = isAlbumMidsSelected || isAlbumSelected; + const willChange = isGroupedMidsSelected || isGroupedSelected; if(willChange) { - this.updateBubbleSelection(albumContainer, isAlbumMidsSelected); + this.updateBubbleSelection(groupContainer, isGroupedMidsSelected); } } diff --git a/src/components/chat/topbar.ts b/src/components/chat/topbar.ts index d0030979..3fbf866c 100644 --- a/src/components/chat/topbar.ts +++ b/src/components/chat/topbar.ts @@ -1,8 +1,6 @@ import type { AppChatsManager, Channel } from "../../lib/appManagers/appChatsManager"; import type { AppMessagesManager } from "../../lib/appManagers/appMessagesManager"; import type { AppPeersManager } from "../../lib/appManagers/appPeersManager"; -import type { AppProfileManager } from "../../lib/appManagers/appProfileManager"; -import type { AppUsersManager } from "../../lib/appManagers/appUsersManager"; import type { AppSidebarRight } from "../sidebarRight"; import type Chat from "./chat"; import { findUpClassName, cancelEvent, attachClickEvent } from "../../helpers/dom"; @@ -18,6 +16,7 @@ import ChatPinnedMessage from "./pinnedMessage"; import ChatSearch from "./search"; import { ButtonMenuItemOptions } from "../buttonMenu"; import ListenerSetter from "../../helpers/listenerSetter"; +import appStateManager from "../../lib/appManagers/appStateManager"; export default class ChatTopbar { container: HTMLDivElement; @@ -38,15 +37,20 @@ export default class ChatTopbar { private setUtilsRAF: number; public peerID: number; + public wasPeerID: number; private setPeerStatusInterval: number; public listenerSetter: ListenerSetter; - constructor(private chat: Chat, private appSidebarRight: AppSidebarRight, private appMessagesManager: AppMessagesManager, private appPeersManager: AppPeersManager, private appChatsManager: AppChatsManager, private appUsersManager: AppUsersManager, private appProfileManager: AppProfileManager) { + public menuButtons: (ButtonMenuItemOptions & {verify: () => boolean})[] = []; + + constructor(private chat: Chat, private appSidebarRight: AppSidebarRight, private appMessagesManager: AppMessagesManager, private appPeersManager: AppPeersManager, private appChatsManager: AppChatsManager) { + this.listenerSetter = new ListenerSetter(); + } + + public construct() { this.chat.log.error('Topbar construction'); - this.listenerSetter = new ListenerSetter(); - this.container = document.createElement('div'); this.container.classList.add('sidebar-header', 'topbar'); @@ -59,10 +63,6 @@ export default class ChatTopbar { const person = document.createElement('div'); person.classList.add('person'); - this.avatarElement = new AvatarElement(); - this.avatarElement.setAttribute('dialog', '1'); - this.avatarElement.setAttribute('clickable', ''); - const content = document.createElement('div'); content.classList.add('content'); @@ -77,13 +77,16 @@ export default class ChatTopbar { const bottom = document.createElement('div'); bottom.classList.add('bottom'); - this.subtitle = document.createElement('div'); - this.subtitle.classList.add('info'); - - bottom.append(this.subtitle); + if(this.subtitle) { + bottom.append(this.subtitle); + } content.append(top, bottom); - person.append(this.avatarElement, content); + if(this.avatarElement) { + person.append(this.avatarElement); + } + + person.append(content); this.chatInfo.append(person); // * chat utils section @@ -92,25 +95,77 @@ export default class ChatTopbar { this.chatAudio = new ChatAudio(this, this.chat, this.appMessagesManager, this.appPeersManager); + if(this.menuButtons.length) { + this.btnMore = ButtonMenuToggle({listenerSetter: this.listenerSetter}, 'bottom-left', this.menuButtons, () => { + this.menuButtons.forEach(button => { + button.element.classList.toggle('hide', !button.verify()); + }); + }); + } + + this.chatUtils.append(...[this.chatAudio ? this.chatAudio.divAndCaption.container : null, this.pinnedMessage ? this.pinnedMessage.pinnedMessageContainer.divAndCaption.container : null, this.btnJoin, this.btnPinned, this.btnMute, this.btnSearch, this.btnMore].filter(Boolean)); + + this.container.append(this.btnBack, this.chatInfo, this.chatUtils); + + // * construction end + + // * fix topbar overflow section + + this.listenerSetter.add(window, 'resize', this.onResize); + mediaSizes.addListener('changeScreen', this.onChangeScreen); + + this.listenerSetter.add(this.container, 'click', (e) => { + const pinned: HTMLElement = findUpClassName(e.target, 'pinned-container'); + if(pinned) { + cancelEvent(e); + + const mid = +pinned.dataset.mid; + if(pinned.classList.contains('pinned-message')) { + //if(!this.pinnedMessage.locked) { + this.pinnedMessage.followPinnedMessage(mid); + //} + } else { + const message = this.appMessagesManager.getMessage(mid); + + this.chat.setPeer(message.peerID, mid); + } + } else { + this.appSidebarRight.toggleSidebar(true); + } + }); + + this.listenerSetter.add(this.btnBack, 'click', (e) => { + cancelEvent(e); + this.chat.appImManager.setPeer(0); + }); + } + + public constructPeerHelpers() { + this.avatarElement = new AvatarElement(); + this.avatarElement.setAttribute('dialog', '1'); + this.avatarElement.setAttribute('clickable', ''); + + this.subtitle = document.createElement('div'); + this.subtitle.classList.add('info'); + + this.pinnedMessage = new ChatPinnedMessage(this, this.chat, this.appMessagesManager, this.appPeersManager); + this.btnJoin = Button('btn-primary chat-join hide'); this.btnJoin.append('SUBSCRIBE'); - this.btnPinned = ButtonIcon('pinlist'); - this.btnMute = ButtonIcon('mute'); - this.btnSearch = ButtonIcon('search'); - const menuButtons: (ButtonMenuItemOptions & {verify: () => boolean})[] = [{ + this.menuButtons = [{ icon: 'search', text: 'Search', onClick: () => { new ChatSearch(this, this.chat); }, verify: () => mediaSizes.isMobile - }, { + }, /* { icon: 'pinlist', text: 'Pinned Messages', - onClick: () => {}, + onClick: () => this.openPinned(false), verify: () => mediaSizes.isMobile - }, { + }, */ { icon: 'mute', text: 'Mute', onClick: () => { @@ -144,53 +199,20 @@ export default class ChatTopbar { onClick: () => {}, verify: () => true }]; - //menuButtons.forEach(b => b.options = {listenerSetter: this.listenerSetter}); - this.btnMore = ButtonMenuToggle({listenerSetter: this.listenerSetter}, 'bottom-left', menuButtons, () => { - menuButtons.forEach(button => { - button.element.classList.toggle('hide', !button.verify()); - }); - }); - this.chatUtils.append(this.chatAudio.divAndCaption.container, this.btnJoin, this.btnPinned, this.btnMute, this.btnSearch, this.btnMore); + this.btnPinned = ButtonIcon('pinlist'); + this.btnMute = ButtonIcon('mute'); + this.btnSearch = ButtonIcon('search'); - this.container.append(this.btnBack, this.chatInfo, this.chatUtils); - - // * construction end - - // * fix topbar overflow section - - this.listenerSetter.add(window, 'resize', this.onResize); - mediaSizes.addListener('changeScreen', this.onChangeScreen); - - this.pinnedMessage = new ChatPinnedMessage(this, this.chat, this.appMessagesManager, this.appPeersManager); - - this.listenerSetter.add(this.container, 'click', (e) => { - const pinned: HTMLElement = findUpClassName(e.target, 'pinned-container'); - if(pinned) { - cancelEvent(e); - - const mid = +pinned.dataset.mid; - if(pinned.classList.contains('pinned-message')) { - this.pinnedMessage.followPinnedMessage(mid); - } else { - const message = this.appMessagesManager.getMessage(mid); - - this.chat.setPeer(message.peerID, mid); - } - } else { - this.appSidebarRight.toggleSidebar(true); - } - }); - - this.listenerSetter.add(this.btnBack, 'click', (e) => { + this.listenerSetter.add(this.btnPinned, 'click', (e) => { cancelEvent(e); - this.chat.appImManager.setPeer(0); + this.openPinned(true); }); this.listenerSetter.add(this.btnSearch, 'click', (e) => { cancelEvent(e); if(this.peerID) { - appSidebarRight.searchTab.open(this.peerID); + this.appSidebarRight.searchTab.open(this.peerID); } }); @@ -199,12 +221,11 @@ export default class ChatTopbar { this.appMessagesManager.mutePeer(this.peerID); }); - //this.listenerSetter.add(this.btnJoin, 'mousedown', (e) => { attachClickEvent(this.btnJoin, (e) => { cancelEvent(e); this.btnJoin.setAttribute('disabled', 'true'); - appChatsManager.joinChannel(-this.peerID).finally(() => { + this.appChatsManager.joinChannel(-this.peerID).finally(() => { this.btnJoin.removeAttribute('disabled'); }); //}); @@ -213,7 +234,7 @@ export default class ChatTopbar { this.listenerSetter.add(rootScope, 'chat_update', (e) => { const peerID: number = e.detail; if(this.peerID == -peerID) { - const chat = appChatsManager.getChat(peerID) as Channel/* | Chat */; + const chat = this.appChatsManager.getChat(peerID) as Channel/* | Chat */; this.btnJoin.classList.toggle('hide', !(chat as Channel)?.pFlags?.left); this.setUtilsWidth(); @@ -244,7 +265,23 @@ export default class ChatTopbar { } }); + this.chat.addListener('setPeer', (mid, isTopMessage) => { + if(isTopMessage) { + this.pinnedMessage.unsetScrollDownListener(); + this.pinnedMessage.testMid(mid, 0); // * because slider will not let get bubble by document.elementFromPoint + } else if(!this.pinnedMessage.locked) { + this.pinnedMessage.handleFollowingPinnedMessage(); + this.pinnedMessage.testMid(mid); + } + }); + this.setPeerStatusInterval = window.setInterval(this.setPeerStatus, 60e3); + + return this; + } + + public openPinned(byCurrent: boolean) { + this.chat.appImManager.setInnerPeer(this.peerID, byCurrent ? +this.pinnedMessage.pinnedMessageContainer.divAndCaption.container.dataset.mid : 0, 'pinned'); } private onResize = () => { @@ -252,8 +289,8 @@ export default class ChatTopbar { }; private onChangeScreen = (from: ScreenSize, to: ScreenSize) => { - this.chatAudio.divAndCaption.container.classList.toggle('is-floating', to == ScreenSize.mobile); - this.pinnedMessage.onChangeScreen(from, to); + this.chatAudio && this.chatAudio.divAndCaption.container.classList.toggle('is-floating', to == ScreenSize.mobile); + this.pinnedMessage && this.pinnedMessage.onChangeScreen(from, to); this.setUtilsWidth(true); }; @@ -263,48 +300,86 @@ export default class ChatTopbar { this.listenerSetter.removeAll(); mediaSizes.removeListener('changeScreen', this.onChangeScreen); window.clearInterval(this.setPeerStatusInterval); + + if(this.pinnedMessage) { + this.pinnedMessage.destroy(); // * возможно это можно не делать + } delete this.chatAudio; delete this.pinnedMessage; } public setPeer(peerID: number) { + this.wasPeerID = this.peerID; this.peerID = peerID; - this.avatarElement.setAttribute('peer', '' + peerID); - this.avatarElement.update(); + this.container.style.display = peerID ? '' : 'none'; + } + + public finishPeerChange(isTarget: boolean, isJump: boolean, lastMsgID: number) { + const peerID = this.peerID; + + if(this.avatarElement) { + this.avatarElement.setAttribute('peer', '' + peerID); + this.avatarElement.update(); + } this.container.classList.remove('is-pinned-shown'); - this.container.style.display = peerID ? '' : 'none'; const isBroadcast = this.appPeersManager.isBroadcast(peerID); - this.btnMute.classList.toggle('hide', !isBroadcast); - this.btnJoin.classList.toggle('hide', !this.appChatsManager.getChat(-peerID)?.pFlags?.left); + this.btnMute && this.btnMute.classList.toggle('hide', !isBroadcast); + this.btnJoin && this.btnJoin.classList.toggle('hide', !this.appChatsManager.getChat(-peerID)?.pFlags?.left); this.setUtilsWidth(); + const middleware = this.chat.bubbles.getMiddleware(); + if(this.pinnedMessage) { // * replace with new one + if(this.wasPeerID) { // * change + const newPinnedMessage = new ChatPinnedMessage(this, this.chat, this.appMessagesManager, this.appPeersManager); + this.pinnedMessage.pinnedMessageContainer.divAndCaption.container.replaceWith(newPinnedMessage.pinnedMessageContainer.divAndCaption.container); + this.pinnedMessage.destroy(); + this.pinnedMessage = newPinnedMessage; + } + + appStateManager.getState().then((state) => { + if(!middleware()) return; + + this.pinnedMessage.hidden = !!state.hiddenPinnedMessages[peerID]; + + if(!isTarget) { + this.pinnedMessage.setCorrectIndex(0); + } + }); + } + window.requestAnimationFrame(() => { - this.pinnedMessage.pinnedIndex/* = this.pinnedMessage.wasPinnedIndex */ = 0; - //this.pinnedMessage.setCorrectIndex(); - this.pinnedMessage.setPinnedMessage(); - /* noTransition.forEach(el => { - el.classList.remove('no-transition-all'); - }); */ - /* if(needToChangeInputDisplay) { - this.chatInput.style.display = ''; - } */ - - let title = ''; - if(peerID == rootScope.myID) title = 'Saved Messages'; - else title = this.appPeersManager.getPeerTitle(peerID); - this.title.innerHTML = title; - + this.setTitle(); this.setPeerStatus(true); this.setMutedState(); }); } + public setTitle(count?: number) { + let title = ''; + if(this.chat.type == 'pinned') { + title = count === -1 ? 'Pinned Messages' : (count === 1 ? 'Pinned Message' : (count + ' Pinned Messages')); + + if(count === undefined) { + this.appMessagesManager.getSearchCounters(this.peerID, [{_: 'inputMessagesFilterPinned'}]).then(result => { + this.setTitle(result[0].count); + }); + } + } else { + if(this.peerID == rootScope.myID) title = 'Saved Messages'; + else title = this.appPeersManager.getPeerTitle(this.peerID); + } + + this.title.innerHTML = title; + } + public setMutedState() { + if(!this.btnMute) return; + const peerID = this.peerID; let muted = this.appMessagesManager.isPeerMuted(peerID); if(this.appPeersManager.isBroadcast(peerID)) { // not human @@ -350,6 +425,8 @@ export default class ChatTopbar { }; public setPeerStatus = (needClear = false) => { + if(!this.subtitle) return; + const peerID = this.peerID; if(needClear) { this.subtitle.innerHTML = ''; diff --git a/src/components/checkbox.ts b/src/components/checkbox.ts index bc97f79a..a26489b6 100644 --- a/src/components/checkbox.ts +++ b/src/components/checkbox.ts @@ -8,6 +8,7 @@ const CheckboxField = (text: string, name: string, round = false) => { const span = document.createElement('span'); span.classList.add('checkbox-caption'); + if(round) span.classList.add('tgico-check'); if(text) { span.innerText = text; } diff --git a/src/components/popupUnpinMessage.ts b/src/components/popupUnpinMessage.ts index 7454ae6a..f80f93cc 100644 --- a/src/components/popupUnpinMessage.ts +++ b/src/components/popupUnpinMessage.ts @@ -6,7 +6,11 @@ export default class PopupPinMessage { constructor(peerID: number, mid: number, unpin?: true) { let title: string, description: string, buttons: PopupButton[] = []; - const callback = () => appMessagesManager.updatePinnedMessage(peerID, mid, unpin); + const callback = () => { + setTimeout(() => { // * костыль, потому что document.elementFromPoint вернёт popup-peer пока он будет закрываться + appMessagesManager.updatePinnedMessage(peerID, mid, unpin); + }, 300); + }; if(unpin) { title = `Unpin Message?`; description = 'Would you like to unpin this message?'; diff --git a/src/components/scrollable.ts b/src/components/scrollable.ts index 02ad26dc..7f179e65 100644 --- a/src/components/scrollable.ts +++ b/src/components/scrollable.ts @@ -70,7 +70,7 @@ export class ScrollableBase { } protected setListeners() { - window.addEventListener('resize', this.onScroll); + window.addEventListener('resize', this.onScroll, {passive: true}); this.container.addEventListener('scroll', this.onScroll, {passive: true, capture: true}); } @@ -160,7 +160,7 @@ export default class Scrollable extends ScrollableBase { this.lastScrollDirection = this.lastScrollTop == scrollTop ? 0 : (this.lastScrollTop < scrollTop ? 1 : -1); // * 1 - bottom, -1 - top this.lastScrollTop = scrollTop; - if(this.onAdditionalScroll) { + if(this.onAdditionalScroll && this.lastScrollDirection !== 0) { this.onAdditionalScroll(); } diff --git a/src/components/sidebarLeft/tabs/includedChats.ts b/src/components/sidebarLeft/tabs/includedChats.ts index 78fb3e4b..f948e316 100644 --- a/src/components/sidebarLeft/tabs/includedChats.ts +++ b/src/components/sidebarLeft/tabs/includedChats.ts @@ -91,7 +91,7 @@ export default class AppIncludedChatsTab implements SliderTab { } checkbox(selected?: boolean) { - return `
`; + return `
`; } renderResults = async(peerIDs: number[]) => { diff --git a/src/components/sidebarRight/tabs/sharedMedia.ts b/src/components/sidebarRight/tabs/sharedMedia.ts index 7c5474a5..735126f5 100644 --- a/src/components/sidebarRight/tabs/sharedMedia.ts +++ b/src/components/sidebarRight/tabs/sharedMedia.ts @@ -15,7 +15,7 @@ import LazyLoadQueue from "../../lazyLoadQueue"; import { putPreloader, renderImageFromUrl } from "../../misc"; import Scrollable from "../../scrollable"; import { SliderTab } from "../../slider"; -import { wrapAudio, wrapDocument } from "../../wrappers"; +import { wrapDocument } from "../../wrappers"; const testScroll = false; @@ -101,6 +101,7 @@ export default class AppSharedMediaTab implements SliderTab { private log = logger('SM'/* , LogLevels.error */); setPeerStatusInterval: number; + cleaned: boolean; public init() { this.container = document.getElementById('shared-media-container'); @@ -804,6 +805,7 @@ export default class AppSharedMediaTab implements SliderTab { }); this.sharedMediaType = 'inputMessagesFilterPhotoVideo'; + this.cleaned = true; } public cleanupHTML() { @@ -861,6 +863,8 @@ export default class AppSharedMediaTab implements SliderTab { } public setPeer(peerID: number) { + if(this.peerID == peerID) return; + if(this.init) { this.init(); this.init = null; @@ -871,7 +875,10 @@ export default class AppSharedMediaTab implements SliderTab { } public fillProfileElements() { - let peerID = this.peerID = appImManager.chat.peerID; + if(!this.cleaned) return; + this.cleaned = false; + + const peerID = this.peerID; this.cleanupHTML(); @@ -906,7 +913,7 @@ export default class AppSharedMediaTab implements SliderTab { setText(user.rPhone, this.profileElements.phone); } - appProfileManager.getProfile(peerID, true).then(userFull => { + appProfileManager.getProfile(peerID).then(userFull => { if(this.peerID != peerID) { this.log.warn('peer changed'); return; diff --git a/src/components/wrappers.ts b/src/components/wrappers.ts index bca6596c..6ab15dd4 100644 --- a/src/components/wrappers.ts +++ b/src/components/wrappers.ts @@ -24,6 +24,7 @@ import { renderImageFromUrl } from './misc'; import PollElement from './poll'; import ProgressivePreloader from './preloader'; import './middleEllipsis'; +import { nextRandomInt } from '../helpers/random'; const MAX_VIDEO_AUTOPLAY_SIZE = 50 * 1024 * 1024; // 50 MB @@ -432,7 +433,7 @@ function wrapMediaWithTail(photo: MyPhoto | MyDocument, message: {mid: number, m svg.setAttributeNS(null, 'viewBox', '0 0 ' + width + ' ' + height); svg.setAttributeNS(null, 'preserveAspectRatio', 'none'); - const clipID = 'clip' + message.mid; + const clipID = 'clip' + message.mid + '_' + nextRandomInt(9999); svg.dataset.clipID = clipID; const defs = document.createElementNS("http://www.w3.org/2000/svg", 'defs'); @@ -803,7 +804,7 @@ export function prepareAlbum(options: { container.append(div); } - div.classList.add('album-item'); + div.classList.add('album-item', 'grouped-item'); div.style.width = (geometry.width / width * 100) + '%'; div.style.height = (geometry.height / height * 100) + '%'; diff --git a/src/helpers/dom.ts b/src/helpers/dom.ts index 6b78863f..20a8edea 100644 --- a/src/helpers/dom.ts +++ b/src/helpers/dom.ts @@ -543,3 +543,50 @@ export const isSelectionSingle = (input: Element = document.activeElement) => { return single; }; + +export const handleScrollSideEvent = (elem: HTMLElement, side: 'top' | 'bottom', callback: () => void, listenerSetter: ListenerSetter) => { + if(isTouchSupported) { + let lastY: number; + const options = {passive: true}; + listenerSetter.add(elem, 'touchstart', (e) => { + if(e.touches.length > 1) { + onTouchEnd(); + return; + } + + lastY = e.touches[0].clientY; + + listenerSetter.add(elem, 'touchmove', onTouchMove, options); + listenerSetter.add(elem, 'touchend', onTouchEnd, options); + }, options); + + const onTouchMove = (e: TouchEvent) => { + const clientY = e.touches[0].clientY; + + const isDown = clientY < lastY; + if(side == 'bottom' && isDown) callback(); + else if(side == 'top' && !isDown) callback(); + lastY = clientY; + //alert('isDown: ' + !!isDown); + }; + + const onTouchEnd = () => { + listenerSetter.removeManual(elem, 'touchmove', onTouchMove, options); + listenerSetter.removeManual(elem, 'touchend', onTouchEnd, options); + }; + } else { + listenerSetter.add(elem, 'wheel', (e) => { + const isDown = e.deltaY > 0; + //this.log('wheel', e, isDown); + if(side == 'bottom' && isDown) callback(); + else if(side == 'top' && !isDown) callback(); + }, {passive: true}); + } +}; + +export const getElementByPoint = (container: HTMLElement, verticalSide: 'top' | 'bottom'): HTMLElement => { + const rect = container.getBoundingClientRect(); + const x = Math.ceil(rect.left + ((rect.right - rect.left) / 2) + 1); + const y = verticalSide == 'bottom' ? Math.floor(rect.top + rect.height - 1) : Math.ceil(rect.top + 1); + return document.elementFromPoint(x, y) as any; +}; diff --git a/src/helpers/eventListenerBase.ts b/src/helpers/eventListenerBase.ts index 4e4b7448..15111c7f 100644 --- a/src/helpers/eventListenerBase.ts +++ b/src/helpers/eventListenerBase.ts @@ -30,7 +30,8 @@ export default class EventListenerBase) { + // * must be protected, but who cares + public setListenerResult(name: keyof Listeners, ...args: ArgumentTypes) { if(this.reuseResults) { this.listenerResults[name] = args; } diff --git a/src/index.hbs b/src/index.hbs index 38bc85d5..30c27e95 100644 --- a/src/index.hbs +++ b/src/index.hbs @@ -157,7 +157,7 @@
@@ -402,7 +402,7 @@

Phone

-