From 368254af95bc69ade894487bde84631431130fa7 Mon Sep 17 00:00:00 2001 From: Eduard Kuzmenko Date: Thu, 8 Jul 2021 02:32:10 +0300 Subject: [PATCH] Bot description bubble --- src/components/chat/bubbles.ts | 314 +++++++++++-------- src/components/chat/chat.ts | 2 +- src/components/chat/contextMenu.ts | 2 +- src/components/chat/selection.ts | 2 +- src/lang.ts | 1 + src/layer.d.ts | 1 + src/lib/appManagers/appImManager.ts | 7 +- src/scripts/in/schema_additional_params.json | 3 +- src/scss/partials/_chatBubble.scss | 14 + 9 files changed, 211 insertions(+), 135 deletions(-) diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index 5444e0cd..a38db517 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -10,9 +10,9 @@ import type { AppStickersManager } from "../../lib/appManagers/appStickersManage import type { AppUsersManager } from "../../lib/appManagers/appUsersManager"; import type { AppInlineBotsManager } from "../../lib/appManagers/appInlineBotsManager"; import type { AppPhotosManager } from "../../lib/appManagers/appPhotosManager"; -import type { AppDocsManager, MyDocument } from "../../lib/appManagers/appDocsManager"; +import type { MyDocument } from "../../lib/appManagers/appDocsManager"; import type { AppPeersManager } from "../../lib/appManagers/appPeersManager"; -import type stateStorage from '../../lib/stateStorage'; +import type { AppProfileManager } from "../../lib/appManagers/appProfileManager"; import type Chat from "./chat"; import { CHAT_ANIMATION_GROUP } from "../../lib/appManagers/appImManager"; import { getObjectKeysAndSort } from "../../helpers/object"; @@ -38,7 +38,6 @@ import { ripple } from "../ripple"; import { wrapAlbum, wrapPhoto, wrapVideo, wrapDocument, wrapSticker, wrapPoll, wrapGroupedDocuments } from "../wrappers"; import { MessageRender } from "./messageRender"; import LazyLoadQueue from "../lazyLoadQueue"; -import { AppChatsManager } from "../../lib/appManagers/appChatsManager"; import ListenerSetter from "../../helpers/listenerSetter"; import PollElement from "../poll"; import AudioElement from "../audio"; @@ -147,13 +146,16 @@ export default class ChatBubbles { [_ in MessageEntity['_']]: boolean }> = {}; + private onAnimateLadder: () => void; + constructor(private chat: Chat, private appMessagesManager: AppMessagesManager, private appStickersManager: AppStickersManager, private appUsersManager: AppUsersManager, private appInlineBotsManager: AppInlineBotsManager, private appPhotosManager: AppPhotosManager, - private appPeersManager: AppPeersManager + private appPeersManager: AppPeersManager, + private appProfileManager: AppProfileManager ) { //this.chat.log.error('Bubbles construction'); @@ -429,7 +431,7 @@ export default class ChatBubbles { } const bubble = (e.target as HTMLElement).classList.contains('bubble') ? e.target as HTMLElement : null; - if(bubble) { + if(bubble && !bubble.classList.contains('bubble-first')) { const mid = +bubble.dataset.mid this.chat.input.initMessageReply(mid); } @@ -2825,8 +2827,12 @@ export default class ChatBubbles { } while(history.length) { - let message = this.chat.getMessage(method()); - this.renderMessage(message, reverse, true); + const message = this.chat.getMessage(method()); + if(message.id > 0) { + this.renderMessage(message, reverse, true); + } else { + this.processLocalMessageRender(message); + } } (this.messagesQueuePromise || Promise.resolve()) @@ -2918,6 +2924,147 @@ export default class ChatBubbles { } } + private animateAsLadder(additionMsgId: number, additionMsgIds: number[], isAdditionRender: boolean, backLimit: number, maxId: number) { + if(!Object.keys(this.bubbles).length) { + return; + } + + if(this.onAnimateLadder) { + this.onAnimateLadder(); + } + + let sortedMids = getObjectKeysAndSort(this.bubbles, 'desc'); + + if(isAdditionRender && additionMsgIds.length) { + sortedMids = sortedMids.filter(mid => !additionMsgIds.includes(mid)); + } + + let targetMid: number; + if(backLimit) { + targetMid = maxId || Math.max(...sortedMids); // * on discussion enter + } else { + if(additionMsgId) { + targetMid = additionMsgId; + } else { // * if maxId === 0 + targetMid = Math.max(...sortedMids); + } + } + + const topIds = sortedMids.slice(sortedMids.findIndex(mid => targetMid > mid)); + const middleIds = isAdditionRender ? [] : [targetMid]; + const bottomIds = isAdditionRender ? [] : sortedMids.slice(0, sortedMids.findIndex(mid => targetMid >= mid)).reverse(); + + if(DEBUG) { + this.log('getHistory: targeting mid:', targetMid, maxId, additionMsgId, + topIds.map(m => this.appMessagesManager.getServerMessageId(m)), + bottomIds.map(m => this.appMessagesManager.getServerMessageId(m))); + } + + const setBubbles: HTMLElement[] = []; + + this.chatInner.classList.add('zoom-fading'); + const delay = isAdditionRender ? 10 : 40; + const offsetIndex = isAdditionRender ? 0 : 1; + const animateAsLadder = (mids: number[], offsetIndex = 0) => { + const animationPromise = deferredPromise(); + let lastMsDelay = 0; + mids.forEach((mid, idx) => { + if(!this.bubbles[mid]) { + this.log.warn('animateAsLadder: no bubble by mid:', mid); + return; + } + + const contentWrapper = this.bubbles[mid].lastElementChild as HTMLElement; + + lastMsDelay = ((idx + offsetIndex) || 0.1) * delay; + //lastMsDelay = (idx + offsetIndex) * delay; + //lastMsDelay = (idx || 0.1) * 1000; + + contentWrapper.classList.add('zoom-fade'); + contentWrapper.style.transitionDelay = lastMsDelay + 'ms'; + + if(idx === (mids.length - 1)) { + const onTransitionEnd = (e: TransitionEvent) => { + if(e.target !== contentWrapper) { + return; + } + + animationPromise.resolve(); + contentWrapper.removeEventListener('transitionend', onTransitionEnd); + }; + + contentWrapper.addEventListener('transitionend', onTransitionEnd); + } + + //this.log('supa', bubble); + + setBubbles.push(contentWrapper); + }); + + if(!mids.length) { + animationPromise.resolve(); + } + + return {lastMsDelay, animationPromise}; + }; + + const topRes = animateAsLadder(topIds, offsetIndex); + const middleRes = animateAsLadder(middleIds); + const bottomRes = animateAsLadder(bottomIds, offsetIndex); + const promises = [topRes.animationPromise, middleRes.animationPromise, bottomRes.animationPromise]; + const delays: number[] = [topRes.lastMsDelay, middleRes.lastMsDelay, bottomRes.lastMsDelay]; + + fastRaf(() => { + setBubbles.forEach(contentWrapper => { + contentWrapper.classList.remove('zoom-fade'); + }); + }); + + let promise: Promise; + if(topIds.length || middleIds.length || bottomIds.length) { + promise = Promise.all(promises); + + dispatchHeavyAnimationEvent(promise, Math.max(...delays) + 200) // * 200 - transition time + .then(() => { + fastRaf(() => { + setBubbles.forEach(contentWrapper => { + contentWrapper.style.transitionDelay = ''; + }); + + this.chatInner.classList.remove('zoom-fading'); + }); + + // ! в хроме, каким-то образом из-за zoom-fade класса начинает прыгать скролл при подгрузке сообщений вверх, + // ! т.е. скролл не ставится, так же, как в сафари при translateZ на блок выше scrollable + if(!isSafari) { + this.needReflowScroll = true; + } + }); + } + + return promise; + } + + private processLocalMessageRender(message: any) { + const bubble = this.renderMessage(message, false, false, undefined, false); + bubble.classList.add('bubble-first', 'is-group-last', 'is-group-first'); + bubble.classList.remove('can-have-tail', 'is-in'); + + const messageDiv = bubble.querySelector('.message'); + const b = document.createElement('b'); + b.append(i18n('BotInfoTitle')); + messageDiv.prepend(b, '\n\n'); + + if(this.messagesQueueOnRenderAdditional) { + this.onAnimateLadder = () => { + this.chatInner.prepend(bubble); + this.onAnimateLadder = undefined; + }; + } else { + this.chatInner.prepend(bubble); + } + } + /** * Load and render history * @param maxId max message id @@ -3002,28 +3149,50 @@ export default class ChatBubbles { this.isFirstLoad = false; - const processResult = (historyResult: typeof result) => { - if(this.chat.type === 'discussion' && 'offsetIdOffset' in historyResult) { - //this.log('discussion got history', loadCount, backLimit, historyResult, isTopEnd); - - // * inject discussion start - if(historyResult.history.isEnd(SliceEnd.Top)) { + const processResult = async(historyResult: typeof result) => { + if('offsetIdOffset' in historyResult && historyResult.history.isEnd(SliceEnd.Top)) { + if(this.chat.type === 'discussion') { // * inject discussion start + //this.log('discussion got history', loadCount, backLimit, historyResult, isTopEnd); const serviceStartMessageId = this.appMessagesManager.threadsServiceMessagesIdsStorage[this.peerId + '_' + this.chat.threadId]; if(serviceStartMessageId) historyResult.history.push(serviceStartMessageId); historyResult.history.push(...this.chat.getMidsByMid(this.chat.threadId).reverse()); - this.scrollable.loadedAll.top = true; + } else if(this.appUsersManager.isBot(this.peerId)) { + this.log('inject bot description'); + + await this.appProfileManager.getProfile(this.peerId).then(userFull => { + if(!userFull.bot_info?.description) { + return; + } + + const offset = this.appMessagesManager.generateMessageId(0); + const message: Message.message = { + _: 'message', + date: 0, + id: -(this.peerId + offset), + message: userFull.bot_info.description, + peer_id: this.appPeersManager.getOutputPeer(this.peerId), + pFlags: { + bot_description: true + } + }; + + this.appMessagesManager.saveMessages([message]); + this.processLocalMessageRender(message); + }); } + + this.scrollable.loadedAll.top = true; } }; - const sup = (result: HistoryResult) => { + const sup = async(result: HistoryResult) => { /* if(maxId && result.history?.length) { if(this.bubbles[maxId]) { result.history.findAndSplice(mid => mid === maxId); } } */ - processResult(result); + await processResult(result); ////console.timeEnd('render history total'); @@ -3081,119 +3250,8 @@ export default class ChatBubbles { if(--times) return; this.messagesQueueOnRenderAdditional = undefined; - if(!Object.keys(this.bubbles).length) { - return; - } - - let sortedMids = getObjectKeysAndSort(this.bubbles, 'desc'); - - if(isAdditionRender && additionMsgIds.length) { - sortedMids = sortedMids.filter(mid => !additionMsgIds.includes(mid)); - } - - let targetMid: number; - if(backLimit) { - targetMid = maxId || Math.max(...sortedMids); // * on discussion enter - } else { - if(additionMsgId) { - targetMid = additionMsgId; - } else { // * if maxId === 0 - targetMid = Math.max(...sortedMids); - } - } - - const topIds = sortedMids.slice(sortedMids.findIndex(mid => targetMid > mid)); - const middleIds = isAdditionRender ? [] : [targetMid]; - const bottomIds = isAdditionRender ? [] : sortedMids.slice(0, sortedMids.findIndex(mid => targetMid >= mid)).reverse(); - if(DEBUG) { - this.log('getHistory: targeting mid:', targetMid, maxId, additionMsgId, - topIds.map(m => this.appMessagesManager.getServerMessageId(m)), - bottomIds.map(m => this.appMessagesManager.getServerMessageId(m))); - } - - const setBubbles: HTMLElement[] = []; - - this.chatInner.classList.add('zoom-fading'); - const delay = isAdditionRender ? 10 : 40; - const offsetIndex = isAdditionRender ? 0 : 1; - const animateAsLadder = (mids: number[], offsetIndex = 0) => { - const animationPromise = deferredPromise(); - let lastMsDelay = 0; - mids.forEach((mid, idx) => { - if(!this.bubbles[mid]) { - this.log.warn('animateAsLadder: no bubble by mid:', mid); - return; - } - - const contentWrapper = this.bubbles[mid].lastElementChild as HTMLElement; - - lastMsDelay = ((idx + offsetIndex) || 0.1) * delay; - //lastMsDelay = (idx + offsetIndex) * delay; - //lastMsDelay = (idx || 0.1) * 1000; - - contentWrapper.classList.add('zoom-fade'); - contentWrapper.style.transitionDelay = lastMsDelay + 'ms'; - - if(idx === (mids.length - 1)) { - const onTransitionEnd = (e: TransitionEvent) => { - if(e.target !== contentWrapper) { - return; - } - - animationPromise.resolve(); - contentWrapper.removeEventListener('transitionend', onTransitionEnd); - }; - - contentWrapper.addEventListener('transitionend', onTransitionEnd); - } - - //this.log('supa', bubble); - - setBubbles.push(contentWrapper); - }); - - if(!mids.length) { - animationPromise.resolve(); - } - - return {lastMsDelay, animationPromise}; - }; - - const topRes = animateAsLadder(topIds, offsetIndex); - const middleRes = animateAsLadder(middleIds); - const bottomRes = animateAsLadder(bottomIds, offsetIndex); - const promises = [topRes.animationPromise, middleRes.animationPromise, bottomRes.animationPromise]; - const delays: number[] = [topRes.lastMsDelay, middleRes.lastMsDelay, bottomRes.lastMsDelay]; - - fastRaf(() => { - setBubbles.forEach(contentWrapper => { - contentWrapper.classList.remove('zoom-fade'); - }); - }); - - let promise: Promise; - if(topIds.length || middleIds.length || bottomIds.length) { - promise = Promise.all(promises); - - dispatchHeavyAnimationEvent(promise, Math.max(...delays) + 200) // * 200 - transition time - .then(() => { - fastRaf(() => { - setBubbles.forEach(contentWrapper => { - contentWrapper.style.transitionDelay = ''; - }); - - this.chatInner.classList.remove('zoom-fading'); - }); - - // ! в хроме, каким-то образом из-за zoom-fade класса начинает прыгать скролл при подгрузке сообщений вверх, - // ! т.е. скролл не ставится, так же, как в сафари при translateZ на блок выше scrollable - if(!isSafari) { - this.needReflowScroll = true; - } - }); - } - + const promise = this.animateAsLadder(additionMsgId, additionMsgIds, isAdditionRender, backLimit, maxId); (promise || Promise.resolve()).then(() => { setTimeout(() => { // preload messages this.loadMoreHistory(reverse, true); diff --git a/src/components/chat/chat.ts b/src/components/chat/chat.ts index b2bc3662..9900b6c6 100644 --- a/src/components/chat/chat.ts +++ b/src/components/chat/chat.ts @@ -170,7 +170,7 @@ export default class Chat extends EventListenerBase<{ this.initPeerId = peerId; this.topbar = new ChatTopbar(this, appSidebarRight, this.appMessagesManager, this.appPeersManager, this.appChatsManager, this.appNotificationsManager); - this.bubbles = new ChatBubbles(this, this.appMessagesManager, this.appStickersManager, this.appUsersManager, this.appInlineBotsManager, this.appPhotosManager, this.appPeersManager); + this.bubbles = new ChatBubbles(this, this.appMessagesManager, this.appStickersManager, this.appUsersManager, this.appInlineBotsManager, this.appPhotosManager, this.appPeersManager, this.appProfileManager); this.input = new ChatInput(this, this.appMessagesManager, this.appDocsManager, this.appChatsManager, this.appPeersManager, this.appWebPagesManager, this.appImManager, this.appDraftsManager, this.serverTimeManager, this.appNotificationsManager, this.appEmojiManager); this.selection = new ChatSelection(this, this.bubbles, this.input, this.appMessagesManager); this.contextMenu = new ChatContextMenu(this.bubbles.bubblesContainer, this, this.appMessagesManager, this.appPeersManager, this.appPollsManager, this.appDocsManager); diff --git a/src/components/chat/contextMenu.ts b/src/components/chat/contextMenu.ts index eb63a5a0..5d04406c 100644 --- a/src/components/chat/contextMenu.ts +++ b/src/components/chat/contextMenu.ts @@ -61,7 +61,7 @@ export default class ChatContextMenu { } catch(e) {} // ! context menu click by date bubble (there is no pointer-events) - if(!bubble) return; + if(!bubble || bubble.classList.contains('bubble-first')) return; if(e instanceof MouseEvent || e.hasOwnProperty('preventDefault')) (e as any).preventDefault(); if(this.element.classList.contains('active')) { diff --git a/src/components/chat/selection.ts b/src/components/chat/selection.ts index b856a861..5d0a7fb9 100644 --- a/src/components/chat/selection.ts +++ b/src/components/chat/selection.ts @@ -523,6 +523,6 @@ export default class ChatSelection { } public canSelectBubble(bubble: HTMLElement) { - return !bubble.classList.contains('service') && !bubble.classList.contains('is-sending'); + return !bubble.classList.contains('service') && !bubble.classList.contains('is-sending') && !bubble.classList.contains('bubble-first'); } } diff --git a/src/lang.ts b/src/lang.ts index fc90b727..40dcb7dd 100644 --- a/src/lang.ts +++ b/src/lang.ts @@ -493,6 +493,7 @@ const lang = { "PrivacyDeleteCloudDrafts": "Delete All Cloud Drafts", "AreYouSureClearDraftsTitle": "Delete cloud drafts", "AreYouSureClearDrafts": "Are you sure you want to delete all cloud drafts?", + "BotInfoTitle": "What can this bot do?", // * macos "AccountSettings.Filters": "Chat Folders", diff --git a/src/layer.d.ts b/src/layer.d.ts index 6378bcc3..5c54394f 100644 --- a/src/layer.d.ts +++ b/src/layer.d.ts @@ -833,6 +833,7 @@ export namespace Message { pinned?: true, unread?: true, is_outgoing?: true, + bot_description?: true, }>, id: number, from_id?: Peer, diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index 7e9e0d1a..334658ee 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -438,14 +438,15 @@ export class AppImManager { //const bubble = chat.bubbles.getBubbleByPoint('top'); //if(bubble) { //const top = bubble.getBoundingClientRect().top; - const top = chat.bubbles.scrollable.scrollTop; + const chatBubbles = chat.bubbles; + const top = chatBubbles.scrollable.scrollTop; const key = chat.peerId + (chat.threadId ? '_' + chat.threadId : ''); const chatPositions = stateStorage.getFromCache('chatPositions'); - if(!(chat.bubbles.scrollable.getDistanceToEnd() <= 16 && chat.bubbles.scrollable.loadedAll.bottom) && Object.keys(chat.bubbles.bubbles).length) { + if(!(chatBubbles.scrollable.getDistanceToEnd() <= 16 && chatBubbles.scrollable.loadedAll.bottom) && Object.keys(chatBubbles.bubbles).length) { const position = { - mids: getObjectKeysAndSort(chat.bubbles.bubbles, 'desc'), + mids: getObjectKeysAndSort(chatBubbles.bubbles, 'desc'), top }; diff --git a/src/scripts/in/schema_additional_params.json b/src/scripts/in/schema_additional_params.json index 90422378..53c86644 100644 --- a/src/scripts/in/schema_additional_params.json +++ b/src/scripts/in/schema_additional_params.json @@ -51,7 +51,8 @@ {"name": "unread", "type": "true"}, {"name": "is_outgoing", "type": "true"}, {"name": "rReply", "type": "string"}, - {"name": "viaBotId", "type": "number"} + {"name": "viaBotId", "type": "number"}, + {"name": "bot_description", "type": "true"} ] }, { "predicate": "messageService", diff --git a/src/scss/partials/_chatBubble.scss b/src/scss/partials/_chatBubble.scss index 2113f1d5..c5343f1b 100644 --- a/src/scss/partials/_chatBubble.scss +++ b/src/scss/partials/_chatBubble.scss @@ -721,6 +721,20 @@ $bubble-margin: .25rem; } } + &-first { + order: -1; + margin-top: .5rem; + justify-content: center; + + .time { + display: none !important; + } + + &:before, &:after { + display: none; + } + } + .web { padding-top: 1px; margin: 4px 0 -5px 1px;