diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index 1192b176..44141a60 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -383,7 +383,7 @@ export default class ChatBubbles { let details = e; if(!this.scrolledAllDown) { - this.chat.setPeer(this.peerId, 0); + this.chat.setMessageId(0); } else { this.renderNewMessagesByIds([details.messageId], true); } @@ -729,11 +729,11 @@ export default class ChatBubbles { this.chat.appImManager.setInnerPeer(replyToPeerId, replyToMid, this.chat.type, this.chat.threadId); /* if(this.chat.type === 'discussion') { - this.chat.appImManager.setPeer(this.peerId, originalMessageId); + this.chat.appImManager.setMessageId(, originalMessageId); } else { this.chat.appImManager.setInnerPeer(this.peerId, originalMessageId); } */ - //this.chat.setPeer(this.peerId, originalMessageId); + //this.chat.setMessageId(, originalMessageId); } } else if(target.tagName == 'IMG' && target.parentElement.tagName == "AVATAR-ELEMENT") { let peerId = +target.parentElement.getAttribute('peer'); @@ -769,9 +769,9 @@ export default class ChatBubbles { this.replyFollowHistory.sort((a, b) => b - a); const mid = this.replyFollowHistory.pop(); - this.chat.setPeer(this.peerId, mid); + this.chat.setMessageId(mid); } else { - this.chat.setPeer(this.peerId/* , dialog.top_message */); + this.chat.setMessageId(/* , dialog.top_message */); // const dialog = this.appMessagesManager.getDialogByPeerId(this.peerId)[0]; // if(dialog) { @@ -1076,7 +1076,7 @@ export default class ChatBubbles { } else { str = months[date.getMonth()] + ' ' + date.getDate(); - if(date.getFullYear() != today.getFullYear()) { + if(date.getFullYear() !== today.getFullYear()) { str += ', ' + date.getFullYear(); } } @@ -1156,9 +1156,7 @@ export default class ChatBubbles { this.bubbleGroups.cleanup(); this.unreadOut.clear(); this.needUpdate.length = 0; - //this.lazyLoadQueue.clear(); - - //this.chatInputC.replyElements.cancelBtn.click(); + this.lazyLoadQueue.clear(); // clear messages if(bubblesToo) { @@ -1194,13 +1192,13 @@ export default class ChatBubbles { //console.time('appImManager setPeer'); //console.time('appImManager setPeer pre promise'); ////console.time('appImManager: pre render start'); - if(peerId == 0) { + if(!peerId) { this.cleanup(true); this.peerId = 0; return null; } - const samePeer = this.peerId == peerId; + const samePeer = this.peerId === peerId; const historyStorage = this.appMessagesManager.getHistoryStorage(peerId, this.chat.threadId); let topMessage = this.chat.type === 'pinned' ? this.appMessagesManager.pinnedMessages[peerId].maxId : historyStorage.maxId ?? 0; @@ -1248,7 +1246,9 @@ export default class ChatBubbles { this.replyFollowHistory.length = 0; } - this.log('setPeer peerId:', this.peerId, historyStorage, lastMsgId, topMessage); + if(DEBUG) { + this.log('setPeer peerId:', this.peerId, historyStorage, lastMsgId, topMessage); + } // add last message, bc in getHistory will load < max_id const additionMsgId = isJump || this.chat.type === 'scheduled' ? 0 : topMessage; @@ -1261,7 +1261,7 @@ export default class ChatBubbles { let maxBubbleId = 0; if(samePeer) { - let el = getElementByPoint(this.chat.bubbles.scrollable.container, 'bottom'); + let el = getElementByPoint(this.chat.bubbles.scrollable.container, 'bottom'); // ! this may not work if being called when chat is hidden //this.chat.log('[PM]: setCorrectIndex: get last element perf:', performance.now() - perf, el); if(el) { el = findUpClassName(el, 'bubble'); @@ -1281,10 +1281,6 @@ export default class ChatBubbles { this.chatInner.className = oldChatInner.className; this.chatInner.classList.add('disable-hover', 'is-scrolling'); - if(!samePeer) { - this.lazyLoadQueue.clear(); - } - this.lazyLoadQueue.lock(); const {promise, cached} = this.getHistory(lastMsgId, true, isJump, additionMsgId); @@ -1321,8 +1317,6 @@ export default class ChatBubbles { this.scrollable.container.append(this.chatInner); animationIntersector.unlockGroup(CHAT_ANIMATION_GROUP); animationIntersector.checkAnimations(false, CHAT_ANIMATION_GROUP/* , true */); - //this.scrollable.attachSentinels(); - //this.scrollable.container.insertBefore(this.chatInner, this.scrollable.container.lastElementChild); this.lazyLoadQueue.unlock(); @@ -1457,7 +1451,6 @@ export default class ChatBubbles { this.messagesQueuePromise = new Promise((resolve, reject) => { setTimeout(() => { - const chatInner = this.chatInner; const queue = this.messagesQueue.slice(); this.messagesQueue.length = 0; @@ -1468,10 +1461,10 @@ export default class ChatBubbles { // promises.push(getHeavyAnimationPromise()); //this.log('promises to call', promises, queue); + const middleware = this.getMiddleware(); Promise.all(promises).then(() => { - if(this.chatInner != chatInner) { - //this.log.warn('chatInner changed!', this.chatInner, chatInner); - return reject('chatInner changed!'); + if(!middleware()) { + return Promise.reject('setMessagesQueuePromise: peer changed!'); } if(this.messagesQueueOnRender) { @@ -1490,7 +1483,7 @@ export default class ChatBubbles { if(this.messagesQueue.length) { this.setMessagesQueuePromise(); } - }, reject); + }).catch(reject); }, 0); }); } @@ -2333,6 +2326,7 @@ export default class ChatBubbles { //const realLength = this.scrollable.container.childElementCount; let previousScrollHeightMinusTop: number/* , previousScrollHeight: number */; //if(realLength > 0/* && (reverse || isSafari) */) { // for safari need set when scrolling bottom too + //if(!this.scrollable.isHeavyScrolling) { this.messagesQueueOnRender = () => { const {scrollTop, scrollHeight} = this.scrollable; @@ -2351,6 +2345,7 @@ export default class ChatBubbles { this.messagesQueueOnRender = undefined; }; //} + //} while(history.length) { let message = this.chat.getMessage(method()); @@ -2412,7 +2407,7 @@ export default class ChatBubbles { return; } - this.chat.setPeer(this.peerId, (history.messages[0] as MyMessage).mid); + this.chat.setMessageId((history.messages[0] as MyMessage).mid); //console.log('got history date:', history); }); }; @@ -2540,17 +2535,17 @@ export default class ChatBubbles { const promise = result.then((result) => { //this.log('getHistory not cached result by maxId:', maxId, reverse, isBackLimit, result, peerId, justLoad); + if(reverse ? this.getHistoryTopPromise !== promise : this.getHistoryBottomPromise !== promise) { + this.log.warn('getHistory: peer changed'); + ////console.timeEnd('render history total'); + return Promise.reject(); + } + if(justLoad) { this.scrollable.onScroll(); // нужно делать из-за ранней прогрузки return true; } //console.timeEnd('appImManager call getHistory'); - - if(this.peerId != peerId || (this.getHistoryTopPromise != promise && this.getHistoryBottomPromise != promise)) { - this.log.warn('peer changed'); - ////console.timeEnd('render history total'); - return Promise.reject(); - } processResult(result); @@ -2561,7 +2556,7 @@ export default class ChatBubbles { }); }, (err) => { this.log.error('getHistory error:', err); - return false; + throw err; }); return promise; @@ -2626,7 +2621,7 @@ export default class ChatBubbles { } (reverse ? this.getHistoryTopPromise = waitPromise : this.getHistoryBottomPromise = waitPromise); - waitPromise.finally(() => { + waitPromise.then(() => { (reverse ? this.getHistoryTopPromise = undefined : this.getHistoryBottomPromise = undefined); }); diff --git a/src/components/chat/chat.ts b/src/components/chat/chat.ts index 58d7b706..c2b77b8f 100644 --- a/src/components/chat/chat.ts +++ b/src/components/chat/chat.ts @@ -11,6 +11,8 @@ import type { AppStickersManager } from "../../lib/appManagers/appStickersManage import type { AppUsersManager } from "../../lib/appManagers/appUsersManager"; import type { AppWebPagesManager } from "../../lib/appManagers/appWebPagesManager"; import type { ApiManagerProxy } from "../../lib/mtproto/mtprotoworker"; +import type { AppDraftsManager } from "../../lib/appManagers/appDraftsManager"; +import type { ServerTimeManager } from "../../lib/mtproto/serverTimeManager"; import EventListenerBase from "../../helpers/eventListenerBase"; import { logger, LogLevels } from "../../lib/logger"; import rootScope from "../../lib/rootScope"; @@ -36,6 +38,7 @@ export default class Chat extends EventListenerBase<{ public selection: ChatSelection; public contextMenu: ChatContextMenu; + public initPeerId = 0; public peerId = 0; public threadId: number; public setPeerPromise: Promise; @@ -45,7 +48,7 @@ export default class Chat extends EventListenerBase<{ public type: ChatType = 'chat'; - constructor(public appImManager: AppImManager, public appChatsManager: AppChatsManager, public appDocsManager: AppDocsManager, public appInlineBotsManager: AppInlineBotsManager, public appMessagesManager: AppMessagesManager, public appPeersManager: AppPeersManager, public appPhotosManager: AppPhotosManager, public appProfileManager: AppProfileManager, public appStickersManager: AppStickersManager, public appUsersManager: AppUsersManager, public appWebPagesManager: AppWebPagesManager, public appPollsManager: AppPollsManager, public apiManager: ApiManagerProxy) { + constructor(public appImManager: AppImManager, public appChatsManager: AppChatsManager, public appDocsManager: AppDocsManager, public appInlineBotsManager: AppInlineBotsManager, public appMessagesManager: AppMessagesManager, public appPeersManager: AppPeersManager, public appPhotosManager: AppPhotosManager, public appProfileManager: AppProfileManager, public appStickersManager: AppStickersManager, public appUsersManager: AppUsersManager, public appWebPagesManager: AppWebPagesManager, public appPollsManager: AppPollsManager, public apiManager: ApiManagerProxy, public appDraftsManager: AppDraftsManager, public serverTimeManager: ServerTimeManager) { super(); this.container = document.createElement('div'); @@ -72,10 +75,12 @@ export default class Chat extends EventListenerBase<{ } } - public init() { + public init(peerId: number) { + this.initPeerId = peerId; + this.topbar = new ChatTopbar(this, appSidebarRight, this.appMessagesManager, this.appPeersManager, this.appChatsManager); this.bubbles = new ChatBubbles(this, this.appMessagesManager, 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.input = new ChatInput(this, this.appMessagesManager, this.appDocsManager, this.appChatsManager, this.appPeersManager, this.appWebPagesManager, this.appImManager, this.appDraftsManager, this.serverTimeManager); this.selection = new ChatSelection(this, this.bubbles, this.input, this.appMessagesManager); this.contextMenu = new ChatContextMenu(this.bubbles.bubblesContainer, this, this.appMessagesManager, this.appChatsManager, this.appPeersManager, this.appPollsManager); @@ -131,22 +136,25 @@ export default class Chat extends EventListenerBase<{ public cleanup() { this.input.cleanup(); this.selection.cleanup(); - - this.peerChanged = false; } public setPeer(peerId: number, lastMsgId?: number) { if(this.init) { - this.init(); + this.init(peerId); this.init = null; } + const samePeer = this.peerId === peerId; + if(!samePeer) { + rootScope.broadcast('peer_changing', this); + this.peerId = peerId; + } + //console.time('appImManager setPeer'); //console.time('appImManager setPeer pre promise'); ////console.time('appImManager: pre render start'); - if(peerId == 0) { + if(!peerId) { appSidebarRight.toggleSidebar(false); - this.peerId = peerId; this.cleanup(); this.topbar.setPeer(peerId); this.bubbles.setPeer(peerId); @@ -155,21 +163,17 @@ export default class Chat extends EventListenerBase<{ return; } - const samePeer = this.peerId == peerId; - // set new if(!samePeer) { - if(appSidebarRight.historyTabIds[appSidebarRight.historyTabIds.length - 1] == AppSidebarRight.SLIDERITEMSIDS.search) { + if(appSidebarRight.historyTabIds[appSidebarRight.historyTabIds.length - 1] === AppSidebarRight.SLIDERITEMSIDS.search) { appSidebarRight.closeTab(AppSidebarRight.SLIDERITEMSIDS.search); } - this.peerId = peerId; appSidebarRight.sharedMediaTab.setPeer(peerId, this.threadId); - this.cleanup(); - } else { - this.peerChanged = true; } + this.peerChanged = samePeer; + const result = this.bubbles.setPeer(peerId, lastMsgId); if(!result) { return; @@ -179,8 +183,8 @@ export default class Chat extends EventListenerBase<{ //console.timeEnd('appImManager setPeer pre promise'); - this.setPeerPromise = promise.finally(() => { - if(this.peerId == peerId) { + const setPeerPromise = this.setPeerPromise = promise.finally(() => { + if(this.setPeerPromise === setPeerPromise) { this.setPeerPromise = null; } }); @@ -194,12 +198,18 @@ export default class Chat extends EventListenerBase<{ return result; } + public setMessageId(messageId?: number) { + return this.setPeer(this.peerId, messageId); + } + public finishPeerChange(isTarget: boolean, isJump: boolean, lastMsgId: number) { if(this.peerChanged) return; let peerId = this.peerId; this.peerChanged = true; + this.cleanup(); + this.topbar.setPeer(peerId); this.topbar.finishPeerChange(isTarget, isJump, lastMsgId); this.bubbles.finishPeerChange(); diff --git a/src/components/chat/input.ts b/src/components/chat/input.ts index 96465ccc..b6a32398 100644 --- a/src/components/chat/input.ts +++ b/src/components/chat/input.ts @@ -4,6 +4,8 @@ import type { AppMessagesManager } from "../../lib/appManagers/appMessagesManage import type { AppPeersManager } from '../../lib/appManagers/appPeersManager'; import type { AppWebPagesManager } from "../../lib/appManagers/appWebPagesManager"; import type { AppImManager } from '../../lib/appManagers/appImManager'; +import type { AppDraftsManager, MyDraftMessage } from '../../lib/appManagers/appDraftsManager'; +import type { ServerTimeManager } from '../../lib/mtproto/serverTimeManager'; import type Chat from './chat'; import Recorder from '../../../public/recorder.min'; import { isTouchSupported } from "../../helpers/touchSupport"; @@ -21,7 +23,7 @@ import Scrollable from "../scrollable"; import { toast } from "../toast"; import { wrapReply } from "../wrappers"; import InputField from '../inputField'; -import { MessageEntity } from '../../layer'; +import { MessageEntity, DraftMessage } from '../../layer'; import StickersHelper from './stickersHelper'; import ButtonIcon from '../buttonIcon'; import DivAndCaption from '../divAndCaption'; @@ -32,7 +34,8 @@ import PopupSchedule from '../popups/schedule'; import SendMenu from './sendContextMenu'; import rootScope from '../../lib/rootScope'; import PopupPinMessage from '../popups/unpinMessage'; -import { isApple } from '../../helpers/userAgent'; +import { debounce } from '../../helpers/schedulers'; +import { tsNow } from '../../helpers/date'; const RECORD_MIN_TIME = 500; const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this group.'; @@ -109,9 +112,11 @@ export default class ChatInput { public goDownBtn: HTMLButtonElement; public goDownUnreadBadge: HTMLElement; - btnScheduled: HTMLButtonElement; + public btnScheduled: HTMLButtonElement; - constructor(private chat: Chat, private appMessagesManager: AppMessagesManager, private appDocsManager: AppDocsManager, private appChatsManager: AppChatsManager, private appPeersManager: AppPeersManager, private appWebPagesManager: AppWebPagesManager, private appImManager: AppImManager) { + public saveDraftDebounced: () => void; + + constructor(private chat: Chat, private appMessagesManager: AppMessagesManager, private appDocsManager: AppDocsManager, private appChatsManager: AppChatsManager, private appPeersManager: AppPeersManager, private appWebPagesManager: AppWebPagesManager, private appImManager: AppImManager, private appDraftsManager: AppDraftsManager, private serverTimeManager: ServerTimeManager) { this.listenerSetter = new ListenerSetter(); } @@ -311,6 +316,18 @@ export default class ChatInput { } }); + this.listenerSetter.add(rootScope, 'draft_updated', (e) => { + const {peerId, threadId, draft} = e; + if(this.chat.threadId !== threadId || this.chat.peerId !== peerId) return; + this.setDraft(draft); + }); + + this.listenerSetter.add(rootScope, 'peer_changing', (chat) => { + if(this.chat === chat) { + this.saveDraft(); + } + }); + try { this.recorder = new Recorder({ //encoderBitRate: 32, @@ -388,7 +405,8 @@ export default class ChatInput { waveform: result.waveform, objectURL: result.url, replyToMsgId: this.replyToMsgId, - threadId: this.chat.threadId + threadId: this.chat.threadId, + clearDraft: true }); this.onMessageSent(false, true); @@ -398,6 +416,8 @@ export default class ChatInput { attachClickEvent(this.replyElements.cancelBtn, this.onHelperCancel, {listenerSetter: this.listenerSetter}); attachClickEvent(this.replyElements.container, this.onHelperClick, {listenerSetter: this.listenerSetter}); + + this.saveDraftDebounced = debounce(() => this.saveDraft(), 2500, false, true); } public constructPinnedHelpers() { @@ -470,6 +490,29 @@ export default class ChatInput { this.goDownUnreadBadge.classList.toggle('badge-gray', this.appMessagesManager.isPeerMuted(this.chat.peerId)); } + public saveDraft() { + if(!this.chat.peerId) return; + + const entities: MessageEntity[] = []; + const str = getRichValue(this.messageInputField.input, entities); + + let draft: DraftMessage.draftMessage; + if(str.length) { + draft = { + _: 'draftMessage', + date: tsNow(true) + this.serverTimeManager.serverTimeOffset, + message: str, + entities: entities.length ? entities : undefined, + pFlags: { + no_webpage: this.noWebPage + }, + reply_to_msg_id: this.replyToMsgId + }; + } + + this.appDraftsManager.syncDraft(this.chat.peerId, this.chat.threadId, draft); + } + public destroy() { //this.chat.log.error('Input destroying'); @@ -493,6 +536,20 @@ export default class ChatInput { } } + public setDraft(draft?: MyDraftMessage, fromUpdate = true) { + if(!isInputEmpty(this.messageInput)) return; + + if(!draft) { + draft = this.appDraftsManager.getDraft(this.chat.peerId, this.chat.threadId); + + if(!draft) { + return; + } + } + + this.setInputValue(draft.rMessage, fromUpdate, fromUpdate); + } + public finishPeerChange() { const peerId = this.chat.peerId; @@ -541,6 +598,7 @@ export default class ChatInput { this.messageInput.removeAttribute('contenteditable'); } else { this.messageInput.setAttribute('contenteditable', 'true'); + this.setDraft(undefined, false); } this.attachMenu.toggleAttribute('disabled', !visible.length); @@ -801,7 +859,7 @@ export default class ChatInput { alert('not single'); } */ - //console.log('messageInput input', this.messageInput.innerText, this.serializeNodes(Array.from(this.messageInput.childNodes))); + //console.log('messageInput input', this.messageInput.innerText); //const value = this.messageInput.innerText; const markdownEntities: MessageEntity[] = []; const richValue = getRichValue(this.messageInputField.input, markdownEntities); @@ -896,6 +954,8 @@ export default class ChatInput { } } + this.saveDraftDebounced(); + this.updateSendBtn(); }; @@ -1039,9 +1099,9 @@ export default class ChatInput { } }); } else if(this.helperType == 'reply') { - this.chat.setPeer(this.chat.peerId, this.replyToMsgId); + this.chat.setMessageId(this.replyToMsgId); } else if(this.helperType == 'edit') { - this.chat.setPeer(this.chat.peerId, this.editMsgId); + this.chat.setMessageId(this.editMsgId); } }; @@ -1125,7 +1185,8 @@ export default class ChatInput { noWebPage: this.noWebPage, webPage: this.willSendWebPage, scheduleDate: this.scheduleDate, - silent: this.sendSilent + silent: this.sendSilent, + clearDraft: true }); } @@ -1269,6 +1330,16 @@ export default class ChatInput { this.chat.container.classList.remove('is-helper-active'); } + public setInputValue(value: string, clear = true, focus = true) { + clear && this.clearInput(); + this.messageInput.innerHTML = value || ''; + this.onMessageInput(); + window.requestAnimationFrame(() => { + focus && placeCaretAtEnd(this.messageInput); + this.inputScroll.scrollTop = this.inputScroll.scrollHeight; + }); + } + public setTopInfo(type: ChatInputHelperType, callerFunc: () => void, title = '', subtitle = '', input?: string, message?: any) { if(type != 'webpage') { this.clearHelper(type); @@ -1288,13 +1359,7 @@ export default class ChatInput { } */ if(input !== undefined) { - this.clearInput(); - this.messageInput.innerHTML = input || ''; - this.onMessageInput(); - window.requestAnimationFrame(() => { - placeCaretAtEnd(this.messageInput); - this.inputScroll.scrollTop = this.inputScroll.scrollHeight; - }); + this.setInputValue(input); } setTimeout(() => { diff --git a/src/components/chat/pinnedMessage.ts b/src/components/chat/pinnedMessage.ts index 6f350e07..2ed42f51 100644 --- a/src/components/chat/pinnedMessage.ts +++ b/src/components/chat/pinnedMessage.ts @@ -542,7 +542,7 @@ export default class ChatPinnedMessage { public async followPinnedMessage(mid: number) { const message = this.chat.getMessage(mid); if(message && !message.deleted) { - this.chat.setPeer(this.topbar.peerId, mid); + this.chat.setMessageId(mid); (this.chat.setPeerPromise || Promise.resolve()).then(() => { // * debounce fast clicker this.handleFollowingPinnedMessage(); this.testMid(this.pinnedIndex >= (this.count - 1) ? this.pinnedMaxMid : mid - 1); diff --git a/src/components/chat/topbar.ts b/src/components/chat/topbar.ts index edca464c..1535ef9e 100644 --- a/src/components/chat/topbar.ts +++ b/src/components/chat/topbar.ts @@ -208,7 +208,7 @@ export default class ChatTopbar { public constructPeerHelpers() { this.avatarElement = new AvatarElement(); this.avatarElement.setAttribute('dialog', '1'); - this.avatarElement.setAttribute('clickable', ''); + //this.avatarElement.setAttribute('clickable', ''); this.avatarElement.classList.add('avatar-40', 'person-avatar'); this.subtitle = document.createElement('div'); diff --git a/src/components/popups/newMedia.ts b/src/components/popups/newMedia.ts index 93ebe63f..f671604f 100644 --- a/src/components/popups/newMedia.ts +++ b/src/components/popups/newMedia.ts @@ -168,7 +168,8 @@ export default class PopupNewMedia extends PopupElement { threadId: this.chat.threadId, isMedia: willAttach.isMedia, silent, - scheduleDate + scheduleDate, + clearDraft: true as true }, w)); caption = undefined; @@ -181,7 +182,8 @@ export default class PopupNewMedia extends PopupElement { replyToMsgId: input.replyToMsgId, threadId: this.chat.threadId, silent, - scheduleDate + scheduleDate, + clearDraft: true }); caption = ''; //input.replyToMsgId = undefined; @@ -196,7 +198,8 @@ export default class PopupNewMedia extends PopupElement { replyToMsgId: input.replyToMsgId, threadId: this.chat.threadId, silent, - scheduleDate + scheduleDate, + clearDraft: true as true }, params)); caption = ''; diff --git a/src/components/scrollable.ts b/src/components/scrollable.ts index 96ef6865..fa9c118f 100644 --- a/src/components/scrollable.ts +++ b/src/components/scrollable.ts @@ -1,7 +1,6 @@ -import { CancellablePromise } from "../helpers/cancellablePromise"; import { isTouchSupported } from "../helpers/touchSupport"; import { logger, LogLevels } from "../lib/logger"; -import fastSmoothScroll from "../helpers/fastSmoothScroll"; +import fastSmoothScroll, { FocusDirection } from "../helpers/fastSmoothScroll"; import useHeavyAnimationCheck from "../hooks/useHeavyAnimationCheck"; /* var el = $0; @@ -52,7 +51,8 @@ export class ScrollableBase { public onScrollMeasure: number = 0; protected onScroll: () => void; - public isHeavyAnimationInProgress: boolean; + public isHeavyAnimationInProgress = false; + public isHeavyScrolling = false; constructor(public el: HTMLElement, logPrefix = '', public container: HTMLElement = document.createElement('div')) { this.container.classList.add('scrollable'); @@ -87,7 +87,21 @@ export class ScrollableBase { this.container.append(element); } - public scrollIntoViewNew = fastSmoothScroll.bind(this, this.container); + public scrollIntoViewNew( + element: HTMLElement, + position: ScrollLogicalPosition, + margin?: number, + maxDistance?: number, + forceDirection?: FocusDirection, + forceDuration?: number, + axis?: 'x' | 'y' + ) { + this.isHeavyScrolling = true; + return fastSmoothScroll(this.container, element, position, margin, maxDistance, forceDirection, forceDuration, axis) + .finally(() => { + this.isHeavyScrolling = false; + }); + } } export type SliceSides = 'top' | 'bottom'; diff --git a/src/helpers/fastSmoothScroll.ts b/src/helpers/fastSmoothScroll.ts index 1a3474ea..48880948 100644 --- a/src/helpers/fastSmoothScroll.ts +++ b/src/helpers/fastSmoothScroll.ts @@ -104,6 +104,8 @@ function scrollWithJs( const elementRect = element.getBoundingClientRect(); const containerRect = container.getBoundingClientRect(); + //const transformable = container.firstElementChild as HTMLElement; + const elementPosition = elementRect[rectStartKey] - containerRect[rectStartKey]; const elementSize = element[scrollSizeKey]; // margin is exclusive in DOMRect @@ -153,6 +155,13 @@ function scrollWithJs( // console.log('scrollWithJs: will scroll path:', path, element); + /* let existsTransform = 0; + const currentTransform = transformable.style.transform; + if(currentTransform) { + existsTransform = parseInt(currentTransform.match(/\((.+?), (.+?), .+\)/)[2]); + //path += existsTransform; + } */ + if(path < 0) { const remainingPath = -scrollPosition; path = Math.max(path, remainingPath); @@ -167,6 +176,46 @@ function scrollWithJs( ); const startAt = Date.now(); + /* transformable.classList.add('no-transition'); + + const tickTransform = () => { + const t = duration ? Math.min((Date.now() - startAt) / duration, 1) : 1; + const currentPath = path * transition(t); + + transformable.style.transform = `translate3d(0, ${-currentPath}px, 0)`; + container.dataset.translate = '' + -currentPath; + + const willContinue = t < 1; + if(!willContinue) { + fastRaf(() => { + delete container.dataset.transform; + container.dataset.transform = ''; + transformable.style.transform = ''; + void transformable.offsetLeft; // reflow + transformable.classList.remove('no-transition'); + void transformable.offsetLeft; // reflow + container[scrollPositionKey] = Math.round(target); + }); + } + + return willContinue; + }; + + return animateSingle(tickTransform, container); */ + + /* return new Promise((resolve) => { + fastRaf(() => { + transformable.style.transform = ''; + transformable.style.transition = ''; + + setTimeout(resolve, duration); + }); + }); + + const transformableHeight = transformable.scrollHeight; + //transformable.style.minHeight = `${transformableHeight}px`; + */ + const tick = () => { const t = duration ? Math.min((Date.now() - startAt) / duration, 1) : 1; @@ -182,6 +231,24 @@ function scrollWithJs( return Promise.resolve(); } + /* return new Promise((resolve) => { + setTimeout(resolve, duration); + }).then(() => { + transformable.classList.add('no-transition'); + void transformable.offsetLeft; // reflow + transformable.style.transform = ''; + transformable.style.transition = ''; + void transformable.offsetLeft; // reflow + transformable.classList.remove('no-transition'); + void transformable.offsetLeft; // reflow + fastRaf(() => { + + container[scrollPositionKey] = Math.round(target); + //transformable.style.minHeight = ``; + }); + + }); */ + return animateSingle(tick, container); } diff --git a/src/layer.d.ts b/src/layer.d.ts index c0a9c0cf..c25e4722 100644 --- a/src/layer.d.ts +++ b/src/layer.d.ts @@ -831,7 +831,8 @@ export namespace Message { deleted?: boolean, peerId?: number, fromId?: number, - canBeEdited?: boolean + canBeEdited?: boolean, + rReply?: string }; export type messageService = { @@ -856,7 +857,8 @@ export namespace Message { deleted?: boolean, peerId?: number, fromId?: number, - canBeEdited?: boolean + canBeEdited?: boolean, + rReply?: string }; } @@ -4777,7 +4779,9 @@ export namespace DraftMessage { reply_to_msg_id?: number, message: string, entities?: Array, - date: number + date: number, + rReply?: string, + rMessage?: string }; } diff --git a/src/lib/appManagers/apiUpdatesManager.ts b/src/lib/appManagers/apiUpdatesManager.ts index 12667128..aa6c3eae 100644 --- a/src/lib/appManagers/apiUpdatesManager.ts +++ b/src/lib/appManagers/apiUpdatesManager.ts @@ -137,12 +137,15 @@ export class ApiUpdatesManager { } } - processUpdateMessage = (updateMessage: any) => { + processUpdateMessage = (updateMessage: any, options: Partial<{ + ignoreSyncLoading: boolean + }> = {}) => { // return forceGetDifference() - var processOpts = { + const processOpts = { date: updateMessage.date, seq: updateMessage.seq, - seqStart: updateMessage.seq_start + seqStart: updateMessage.seq_start, + ignoreSyncLoading: options.ignoreSyncLoading }; switch(updateMessage._) { @@ -389,7 +392,12 @@ export class ApiUpdatesManager { return this.channelStates[channelId]; } - public processUpdate(update: any, options: any = {}) { + public processUpdate(update: any, options: Partial<{ + date: number, + seq: number, + seqStart: number, + ignoreSyncLoading: boolean + }> = {}) { let channelId = 0; switch(update._) { case 'updateNewChannelMessage': @@ -411,7 +419,7 @@ export class ApiUpdatesManager { // this.log.log('process', channelId, curState.pts, update) - if(curState.syncLoading) { + if(curState.syncLoading && !options.ignoreSyncLoading) { return false; } @@ -557,10 +565,10 @@ export class ApiUpdatesManager { this.updatesState.seq = stateResult.seq; this.updatesState.pts = stateResult.pts; this.updatesState.date = stateResult.date; - setTimeout(() => { + //setTimeout(() => { this.updatesState.syncLoading = false; //rootScope.broadcast('state_synchronized'); - }, 1000); + //}, 1000); // ! for testing // updatesState.seq = 1 diff --git a/src/lib/appManagers/appDialogsManager.ts b/src/lib/appManagers/appDialogsManager.ts index d1dd85e4..7c79ac33 100644 --- a/src/lib/appManagers/appDialogsManager.ts +++ b/src/lib/appManagers/appDialogsManager.ts @@ -24,6 +24,7 @@ import Button from "../../components/button"; import SetTransition from "../../components/singleTransition"; import AppStorage from '../storage'; import apiUpdatesManager from "./apiUpdatesManager"; +import appDraftsManager, { MyDraftMessage } from "./appDraftsManager"; type DialogDom = { avatarEl: AvatarElement, @@ -335,6 +336,13 @@ export class AppDialogsManager { } }); + rootScope.on('dialog_draft', (e) => { + const dialog = appMessagesManager.getDialogByPeerId(e.peerId)[0]; + if(dialog) { + this.updateDialog(dialog); + } + }); + rootScope.on('peer_changed', (e) => { const peerId = e; @@ -458,6 +466,11 @@ export class AppDialogsManager { } }); + if(state.dialogs?.length) { + appDraftsManager.getAllDrafts(); + appDraftsManager.addMissedDialogs(); + } + return this.loadDialogs(); }).then(() => { appMessagesManager.getConversationsAll('', 0).finally(() => { @@ -903,7 +916,7 @@ export class AppDialogsManager { }); } - public setLastMessage(dialog: any, lastMessage?: any, dom?: DialogDom, highlightWord?: string) { + public setLastMessage(dialog: Dialog, lastMessage?: any, dom?: DialogDom, highlightWord?: string) { ///////console.log('setlastMessage:', lastMessage); if(!dom) { dom = this.getDialogDom(dialog.peerId); @@ -914,7 +927,12 @@ export class AppDialogsManager { } } + let draftMessage: MyDraftMessage; if(!lastMessage) { + if(dialog.draft && dialog.draft._ === 'draftMessage') { + draftMessage = dialog.draft; + } + lastMessage = appMessagesManager.getMessageByPeer(dialog.peerId, dialog.top_message); } @@ -959,6 +977,8 @@ export class AppDialogsManager { }); dom.lastMessageSpan.innerHTML = lastMessageText + messageWrapped; + } else if(draftMessage) { + dom.lastMessageSpan.innerHTML = draftMessage.rReply; } else if(!lastMessage.deleted) { dom.lastMessageSpan.innerHTML = lastMessage.rReply; } else { @@ -966,7 +986,12 @@ export class AppDialogsManager { } /* if(lastMessage.from_id == auth.id) { // You: */ - if(peer._ != 'peerUser' && peerId != lastMessage.fromId && !lastMessage.action) { + if(draftMessage) { + const bold = document.createElement('b'); + bold.classList.add('danger'); + bold.innerHTML = 'Draft: '; + dom.lastMessageSpan.prepend(bold); + } else if(peer._ !== 'peerUser' && peerId !== lastMessage.fromId && !lastMessage.action) { const sender = appPeersManager.getPeer(lastMessage.fromId); if(sender && sender.id) { const senderBold = document.createElement('b'); @@ -987,8 +1012,9 @@ export class AppDialogsManager { } } - if(!lastMessage.deleted) { - dom.lastTimeSpan.innerHTML = formatDateAccordingToToday(new Date(lastMessage.date * 1000)); + if(!lastMessage.deleted || draftMessage/* && lastMessage._ !== 'draftMessage' */) { + const date = draftMessage ? Math.max(draftMessage.date, lastMessage.date || 0) : lastMessage.date; + dom.lastTimeSpan.innerHTML = formatDateAccordingToToday(new Date(date * 1000)); } else dom.lastTimeSpan.innerHTML = ''; if(this.doms[peerId] == dom) { @@ -1024,7 +1050,9 @@ export class AppDialogsManager { dom.listEl.classList.toggle('is-muted', isMuted); } - const lastMessage = appMessagesManager.getMessageByPeer(dialog.peerId, dialog.top_message); + const lastMessage = dialog.draft && dialog.draft._ === 'draftMessage' ? + dialog.draft : + appMessagesManager.getMessageByPeer(dialog.peerId, dialog.top_message); if(lastMessage._ != 'messageEmpty' && !lastMessage.deleted && lastMessage.fromId == rootScope.myId && lastMessage.peerId != rootScope.myId/* && dialog.read_outbox_max_id */) { // maybe comment, 06.20.2020 diff --git a/src/lib/appManagers/appDraftsManager.ts b/src/lib/appManagers/appDraftsManager.ts new file mode 100644 index 00000000..29d5cc5d --- /dev/null +++ b/src/lib/appManagers/appDraftsManager.ts @@ -0,0 +1,187 @@ +import { MOUNT_CLASS_TO } from "../mtproto/mtproto_config"; +import rootScope from "../rootScope"; +import appPeersManager from "./appPeersManager"; +import appMessagesManager from "./appMessagesManager"; +import apiUpdatesManager from "./apiUpdatesManager"; +import RichTextProcessor from "../richtextprocessor"; +import serverTimeManager from "../mtproto/serverTimeManager"; +import { MessageEntity, DraftMessage, MessagesSaveDraft } from "../../layer"; +import apiManager from "../mtproto/mtprotoworker"; +import { tsNow } from "../../helpers/date"; +import { deepEqual } from "../../helpers/object"; +import appStateManager from "./appStateManager"; + +export type MyDraftMessage = DraftMessage.draftMessage; + +export class AppDraftsManager { + private drafts: {[peerIdAndThreadId: string]: MyDraftMessage} = {}; + private getAllDraftPromise: Promise = null; + + constructor() { + appStateManager.getState().then(state => { + this.drafts = state.drafts; + }); + + appStateManager.addListener('save', async() => { + appStateManager.pushToState('drafts', this.drafts); + }); + + rootScope.on('apiUpdate', (update) => { + if(update._ !== 'updateDraftMessage') { + return + } + + const peerID = appPeersManager.getPeerId(update.peer); + this.saveDraft(peerID, (update as any).threadId, update.draft, {notify: true}); + }); + } + + private getKey(peerId: number, threadId?: number) { + return '' + peerId + (threadId ? '_' + threadId : ''); + } + + public getDraft(peerId: number, threadId?: number) { + return this.drafts[this.getKey(peerId, threadId)]; + } + + public addMissedDialogs() { + return this.getAllDrafts().then(() => { + for(const key in this.drafts) { + if(key.indexOf('_') !== -1) { // exclude threads + continue; + } + + const peerId = +key; + const dialog = appMessagesManager.getDialogByPeerId(peerId)[0]; + if(!dialog) { + appMessagesManager.reloadConversation(peerId); + /* const dialog = appMessagesManager.generateDialog(peerId); + dialog.draft = this.drafts[key]; + appMessagesManager.saveConversation(dialog); + appMessagesManager.newDialogsToHandle[peerId] = dialog; + appMessagesManager.scheduleHandleNewDialogs(); */ + } + } + }); + } + + public getAllDrafts() { + return this.getAllDraftPromise || (this.getAllDraftPromise = new Promise((resolve) => { + apiManager.invokeApi('messages.getAllDrafts').then((updates) => { + apiUpdatesManager.processUpdateMessage(updates, {ignoreSyncLoading: true}); + resolve(); + }); + })); + } + + public saveDraft(peerId: number, threadId: number, apiDraft: DraftMessage, options: Partial<{ + notify: boolean + }> = {}) { + const draft = this.processApiDraft(apiDraft); + + const key = this.getKey(peerId, threadId); + if(draft) { + this.drafts[key] = draft; + } else { + delete this.drafts[key]; + } + + if(options.notify) { + // console.warn(dT(), 'save draft', peerId, apiDraft, options) + rootScope.broadcast('draft_updated', { + peerId, + threadId, + draft + }); + } + + return draft; + } + + public draftsAreEqual(draft1: DraftMessage, draft2: DraftMessage) { + return deepEqual(draft1, draft2); + } + + public isEmptyDraft(draft: DraftMessage) { + if(!draft || draft._ === 'draftMessageEmpty') { + return true; + } + + if(draft.reply_to_msg_id > 0) { + return false; + } + + if(!draft.message.length) { + return true; + } + + return false; + } + + public processApiDraft(draft: DraftMessage): MyDraftMessage { + if(!draft || draft._ !== 'draftMessage') { + return undefined; + } + + const myEntities = RichTextProcessor.parseEntities(draft.message); + const apiEntities = draft.entities || []; + const totalEntities = RichTextProcessor.mergeEntities(apiEntities, myEntities); // ! only in this order, otherwise bold and emoji formatting won't work + + draft.rMessage = RichTextProcessor.wrapDraftText(draft.message, {entities: totalEntities}); + draft.rReply = appMessagesManager.getRichReplyText(draft); + if(draft.reply_to_msg_id) { + draft.reply_to_msg_id = appMessagesManager.generateMessageId(draft.reply_to_msg_id); + } + + return draft; + } + + public syncDraft(peerId: number, threadId: number, localDraft?: MyDraftMessage, saveOnServer = true) { + // console.warn(dT(), 'sync draft', peerID) + const serverDraft = this.getDraft(peerId, threadId); + if(this.draftsAreEqual(serverDraft, localDraft)) { + // console.warn(dT(), 'equal drafts', localDraft, serverDraft) + return; + } + + // console.warn(dT(), 'changed draft', localDraft, serverDraft) + let params: MessagesSaveDraft = { + peer: appPeersManager.getInputPeerById(peerId), + message: '' + }; + + let draftObj: DraftMessage; + if(this.isEmptyDraft(localDraft)) { + draftObj = {_: 'draftMessageEmpty'}; + } else { + draftObj = {_: 'draftMessage'} as any as DraftMessage.draftMessage; + let message = localDraft.message; + let entities: MessageEntity[] = []; + //message = RichTextProcessor.parseEmojis(message); + //message = RichTextProcessor.parseMarkdown(message, entities, true); + + if(localDraft.reply_to_msg_id) { + params.reply_to_msg_id = draftObj.reply_to_msg_id = localDraft.reply_to_msg_id; + } + + if(entities.length) { + params.entities = draftObj.entities = entities; + } + + params.message = draftObj.message = message; + } + + draftObj.date = tsNow(true) + serverTimeManager.serverTimeOffset; + this.saveDraft(peerId, threadId, draftObj, {notify: true}); + + if(saveOnServer && !threadId) { + apiManager.invokeApi('messages.saveDraft', params).then(() => { + + }); + } + } +} + +const appDraftsManager = new AppDraftsManager(); +MOUNT_CLASS_TO && (MOUNT_CLASS_TO.appDraftsManager = appDraftsManager); +export default appDraftsManager; \ No newline at end of file diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index 93dd6f79..a25259b1 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -30,6 +30,8 @@ import ChatDragAndDrop from '../../components/chat/dragAndDrop'; import { debounce } from '../../helpers/schedulers'; import lottieLoader from '../lottieLoader'; import useHeavyAnimationCheck from '../../hooks/useHeavyAnimationCheck'; +import appDraftsManager from './appDraftsManager'; +import serverTimeManager from '../mtproto/serverTimeManager'; //console.log('appImManager included33!'); @@ -450,7 +452,7 @@ export class AppImManager { } private createNewChat() { - const chat = new Chat(this, appChatsManager, appDocsManager, appInlineBotsManager, appMessagesManager, appPeersManager, appPhotosManager, appProfileManager, appStickersManager, appUsersManager, appWebPagesManager, appPollsManager, apiManager); + const chat = new Chat(this, appChatsManager, appDocsManager, appInlineBotsManager, appMessagesManager, appPeersManager, appPhotosManager, appProfileManager, appStickersManager, appUsersManager, appWebPagesManager, appPollsManager, apiManager, appDraftsManager, serverTimeManager); this.chats.push(chat); } @@ -458,6 +460,10 @@ export class AppImManager { private spliceChats(fromIndex: number, justReturn = true) { if(fromIndex >= this.chats.length) return; + if(this.chats.length > 1 && justReturn) { + rootScope.broadcast('peer_changing', this.chat); + } + const spliced = this.chats.splice(fromIndex, this.chats.length - fromIndex); // * fix middle chat z-index on animation @@ -561,7 +567,7 @@ export class AppImManager { public setInnerPeer(peerId: number, lastMsgId?: number, type: ChatType = 'chat', threadId?: number) { // * prevent opening already opened peer - const existingIndex = this.chats.findIndex(chat => chat.peerId == peerId && chat.type == type); + const existingIndex = this.chats.findIndex(chat => chat.peerId === peerId && chat.type === type); if(existingIndex !== -1) { this.spliceChats(existingIndex + 1); return this.setPeer(peerId, lastMsgId); diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index c0824e79..3ec12cbd 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -24,7 +24,7 @@ import DialogsStorage from "../storages/dialogs"; import FiltersStorage from "../storages/filters"; //import { telegramMeWebService } from "../mtproto/mtproto"; import apiUpdatesManager from "./apiUpdatesManager"; -import appChatsManager from "./appChatsManager"; +import appChatsManager, { Channel } from "./appChatsManager"; import appDocsManager, { MyDocument } from "./appDocsManager"; import appDownloadManager from "./appDownloadManager"; import appPeersManager from "./appPeersManager"; @@ -33,6 +33,7 @@ import appPollsManager from "./appPollsManager"; import appStateManager from "./appStateManager"; import appUsersManager from "./appUsersManager"; import appWebPagesManager from "./appWebPagesManager"; +import appDraftsManager from "./appDraftsManager"; //console.trace('include'); // TODO: если удалить сообщение в непрогруженном диалоге, то при обновлении, из-за стейта, последнего сообщения в чатлисте не будет @@ -201,41 +202,26 @@ export class AppMessagesManager { }); }); - /* rootScope.$on('draft_updated', (e) => { - let eventData = e;; - var peerId = eventData.peerId; - var draft = eventData.draft; + rootScope.on('draft_updated', (e) => { + const {peerId, threadId, draft} = e; - var dialog = this.getDialogByPeerID(peerId)[0]; - if(dialog) { - var topDate; - if(draft && draft.date) { - topDate = draft.date; - } else { - var channelId = appPeersManager.isChannel(peerId) ? -peerId : 0 - var topDate = this.getMessage(dialog.top_message).date; - - if(channelId) { - var channel = appChatsManager.getChat(channelId); - if(!topDate || channel.date && channel.date > topDate) { - topDate = channel.date; - } - } - } - - if(!dialog.pFlags.pinned) { - dialog.index = this.dialogsStorage.generateDialogIndex(topDate); - } + if(threadId) return; + const dialog = this.getDialogByPeerId(peerId)[0]; + if(dialog && !threadId) { + dialog.draft = draft; + this.dialogsStorage.generateIndexForDialog(dialog); this.dialogsStorage.pushDialog(dialog); - rootScope.$broadcast('dialog_draft', { + rootScope.broadcast('dialog_draft', { peerId, draft, index: dialog.index }); + } else { + this.reloadConversation(peerId); } - }); */ + }); function timedChunk(items: any[], process: (...args: any[]) => any, context: any, callback: (...args: any[]) => void) { if(!items.length) return callback(items); @@ -278,7 +264,7 @@ export class AppMessagesManager { } dialog.top_message = message.mid; - this.setDialogIndexByMessage(dialog, message); + this.dialogsStorage.generateIndexForDialog(dialog, false, message); break; } else if(message.pFlags && message.pFlags.unread) { @@ -602,7 +588,11 @@ export class AppMessagesManager { this.pendingAfterMsgs[peerId] = sentRequestOptions; } - this.beforeMessageSending(message, {isScheduled: !!options.scheduleDate || undefined, threadId: options.threadId}); + this.beforeMessageSending(message, { + isScheduled: !!options.scheduleDate || undefined, + threadId: options.threadId, + clearDraft: options.clearDraft + }); } public sendFile(peerId: number, file: File | Blob | MyDocument, options: Partial<{ @@ -623,6 +613,7 @@ export class AppMessagesManager { duration: number, background: true, silent: true, + clearDraft: true, scheduleDate: number, waveform: Uint8Array @@ -957,7 +948,12 @@ export class AppMessagesManager { return sentDeferred; }; - this.beforeMessageSending(message, {isGroupedItem: options.isGroupedItem, isScheduled: !!options.scheduleDate || undefined, threadId: options.threadId}); + this.beforeMessageSending(message, { + isGroupedItem: options.isGroupedItem, + isScheduled: !!options.scheduleDate || undefined, + threadId: options.threadId, + clearDraft: options.clearDraft + }); if(!options.isGroupedItem) { sentDeferred.then(inputMedia => { @@ -972,7 +968,8 @@ export class AppMessagesManager { reply_to_msg_id: replyToMsgId, schedule_date: options.scheduleDate, silent: options.silent, - entities + entities, + clear_draft: options.clearDraft }).then((updates) => { apiUpdatesManager.processUpdateMessage(updates); }, (error) => { @@ -1009,6 +1006,7 @@ export class AppMessagesManager { thumbURL: string }>[], silent: true, + clearDraft: true, scheduleDate: number }> = {}) { //this.checkSendOptions(options); @@ -1065,6 +1063,10 @@ export class AppMessagesManager { this.setDialogTopMessage(message); } + + if(options.clearDraft) { + appDraftsManager.syncDraft(peerId, options.threadId); + } // * test pending //return; @@ -1090,7 +1092,8 @@ export class AppMessagesManager { multi_media: multiMedia, reply_to_msg_id: replyToMsgId, schedule_date: options.scheduleDate, - silent: options.silent + silent: options.silent, + clear_draft: options.clearDraft }).then((updates) => { apiUpdatesManager.processUpdateMessage(updates); }, (error) => { @@ -1332,7 +1335,11 @@ export class AppMessagesManager { this.pendingAfterMsgs[peerId] = sentRequestOptions; } - this.beforeMessageSending(message, {isScheduled: !!options.scheduleDate || undefined, threadId: options.threadId}); + this.beforeMessageSending(message, { + isScheduled: !!options.scheduleDate || undefined, + threadId: options.threadId, + clearDraft: options.clearDraft + }); } /* private checkSendOptions(options: Partial<{ @@ -1346,7 +1353,12 @@ export class AppMessagesManager { } } */ - private beforeMessageSending(message: any, options: Partial<{isGroupedItem: true, isScheduled: true, threadId: number}> = {}) { + private beforeMessageSending(message: any, options: Partial<{ + isGroupedItem: true, + isScheduled: true, + threadId: number, + clearDraft: true + }> = {}) { const messageId = message.id; const peerId = this.getMessagePeer(message); const storage = options.isScheduled ? this.getScheduledMessagesStorage(peerId) : this.getMessagesStorage(peerId); @@ -1375,6 +1387,10 @@ export class AppMessagesManager { this.setDialogTopMessage(message); } } + + if(!options.isGroupedItem && options.clearDraft && !options.threadId) { + appDraftsManager.syncDraft(peerId, options.threadId); + } this.pendingByRandomId[message.random_id] = {peerId, tempId: messageId, storage}; @@ -1427,12 +1443,6 @@ export class AppMessagesManager { return pFlags; } - private setDialogIndexByMessage(dialog: MTDialog.dialog, message: MyMessage) { - if(!dialog.pFlags.pinned || !dialog.index) { - dialog.index = this.dialogsStorage.generateDialogIndex(message.date); - } - } - public setDialogTopMessage(message: MyMessage, dialog: MTDialog.dialog = this.getDialogByPeerId(message.peerId)[0]) { if(dialog) { dialog.top_message = message.mid; @@ -1440,7 +1450,7 @@ export class AppMessagesManager { const historyStorage = this.getHistoryStorage(message.peerId); historyStorage.maxId = message.mid; - this.setDialogIndexByMessage(dialog, message); + this.dialogsStorage.generateIndexForDialog(dialog, false, message); this.newDialogsToHandle[message.peerId] = dialog; this.scheduleHandleNewDialogs(); @@ -1587,28 +1597,16 @@ export class AppMessagesManager { let offsetDate = 0; let offsetPeerId = 0; let offsetIndex = 0; - let flags = 0; if(this.dialogsStorage.dialogsOffsetDate[folderId]) { offsetDate = this.dialogsStorage.dialogsOffsetDate[folderId] + serverTimeManager.serverTimeOffset; offsetIndex = this.dialogsStorage.dialogsOffsetDate[folderId] * 0x10000; - //flags |= 1; // means pinned already loaded } - /* if(this.dialogsStorage.dialogsOffsetDate[0]) { - flags |= 1; // means pinned already loaded - } */ - - //if(folderId > 0) { - //flags |= 1; - flags |= 2; - //} - // ! ВНИМАНИЕ: ОЧЕНЬ СЛОЖНАЯ ЛОГИКА: // ! если делать запрос сначала по папке 0, потом по папке 1, по индексу 0 в массиве будет один и тот же диалог, с dialog.pFlags.pinned, ЛОЛ??? // ! т.е., с запросом folder_id: 1, и exclude_pinned: 0, в результате будут ещё и закреплённые с папки 0 return apiManager.invokeApi('messages.getDialogs', { - flags, folder_id: folderId, offset_date: offsetDate, offset_id: offsetId, @@ -1621,7 +1619,9 @@ export class AppMessagesManager { }).then((dialogsResult) => { if(dialogsResult._ == 'messages.dialogsNotModified') return null; - //this.log.error('messages.getDialogs result:', dialogsResult.dialogs, {...dialogsResult.dialogs[0]}); + if(DEBUG) { + this.log('messages.getDialogs result:', dialogsResult.dialogs, {...dialogsResult.dialogs[0]}); + } /* if(!offsetDate) { telegramMeWebService.setAuthorized(true); @@ -2659,7 +2659,7 @@ export class AppMessagesManager { appChatsManager.saveApiChats(dialogsResult.chats); this.saveMessages(dialogsResult.messages); - //this.log('applyConversation', dialogsResult); + this.log('applyConversation', dialogsResult); const updatedDialogs: {[peerId: number]: Dialog} = {}; (dialogsResult.dialogs as Dialog[]).forEach((dialog) => { @@ -2679,7 +2679,7 @@ export class AppMessagesManager { this.log.error('applyConversation lun', dialog, d); } */ - if(topMessage) { + if(topMessage || (dialog.draft && dialog.draft._ === 'draftMessage')) { //const wasDialogBefore = this.getDialogByPeerID(peerId)[0]; // here need to just replace, not FULL replace dialog! WARNING @@ -2720,6 +2720,24 @@ export class AppMessagesManager { } } + public generateDialog(peerId: number) { + const dialog: Dialog = { + _: 'dialog', + pFlags: {}, + peer: appPeersManager.getOutputPeer(peerId), + top_message: 0, + read_inbox_max_id: 0, + read_outbox_max_id: 0, + unread_count: 0, + unread_mentions_count: 0, + notify_settings: { + _: 'peerNotifySettings', + }, + }; + + return dialog; + } + public saveConversation(dialog: Dialog, folderId = 0) { const peerId = appPeersManager.getPeerId(dialog.peer); if(!peerId) { @@ -2743,7 +2761,7 @@ export class AppMessagesManager { message = { _: 'message', id: mid, - mid: mid, + mid, from_id: appPeersManager.getOutputPeer(appUsersManager.getSelf().id), peer_id: appPeersManager.getOutputPeer(peerId), deleted: true, @@ -2783,6 +2801,7 @@ export class AppMessagesManager { } */ } + dialog.draft = appDraftsManager.saveDraft(peerId, 0, dialog.draft); dialog.peerId = peerId; // Because we saved message without dialog present @@ -4717,9 +4736,9 @@ export class AppMessagesManager { } public setTyping(peerId: number, _action: any): Promise { - if(!rootScope.myId || !peerId || !this.canWriteToPeer(peerId)) return Promise.resolve(false); + if(!rootScope.myId || !peerId || !this.canWriteToPeer(peerId) || peerId === rootScope.myId) return Promise.resolve(false); - const action: SendMessageAction = typeof(_action) == 'string' ? {_: _action} : _action; + const action: SendMessageAction = typeof(_action) === 'string' ? {_: _action} : _action; return apiManager.invokeApi('messages.setTyping', { peer: appPeersManager.getInputPeerById(peerId), action diff --git a/src/lib/appManagers/appStateManager.ts b/src/lib/appManagers/appStateManager.ts index e1cad97f..06940f9d 100644 --- a/src/lib/appManagers/appStateManager.ts +++ b/src/lib/appManagers/appStateManager.ts @@ -11,6 +11,7 @@ import type { AuthState } from '../../types'; import type FiltersStorage from '../storages/filters'; import type DialogsStorage from '../storages/dialogs'; import { copy, setDeepProperty, isObject, validateInitObject } from '../../helpers/object'; +import { AppDraftsManager } from './appDraftsManager'; const REFRESH_EVERY = 24 * 60 * 60 * 1000; // 1 day const STATE_VERSION = App.version; @@ -55,7 +56,8 @@ export type State = Partial<{ suggest: boolean, loop: boolean } - } + }, + drafts: AppDraftsManager['drafts'] }>; const STATE_INIT: State = { @@ -96,7 +98,8 @@ const STATE_INIT: State = { suggest: true, loop: true } - } + }, + drafts: {} }; const ALL_KEYS = Object.keys(STATE_INIT) as any as Array; diff --git a/src/lib/langPack.ts b/src/lib/langPack.ts index 87ba739e..f46cd680 100644 --- a/src/lib/langPack.ts +++ b/src/lib/langPack.ts @@ -9,7 +9,7 @@ export const langPack: {[actionType: string]: string} = { "messageActionChatAddUsers": "invited {} users", "messageActionChatLeave": "left the group", "messageActionChatDeleteUser": "removed user {}", - "messageActionChatJoinedByLink": "joined the group", + "messageActionChatJoinedByLink": "joined the group via invite link", "messageActionPinMessage": "pinned message", "messageActionContactSignUp": "joined Telegram", "messageActionChannelCreate": "Channel created", diff --git a/src/lib/mtproto/mtproto.worker.ts b/src/lib/mtproto/mtproto.worker.ts index 15ed79a2..da42b587 100644 --- a/src/lib/mtproto/mtproto.worker.ts +++ b/src/lib/mtproto/mtproto.worker.ts @@ -56,8 +56,8 @@ export const isWebpSupported = () => { return webpSupported; }; -networkerFactory.setUpdatesProcessor((obj, bool) => { - respond({update: {obj, bool}}); +networkerFactory.setUpdatesProcessor((obj) => { + respond({update: obj}); }); networkerFactory.onConnectionStatusChange = (status) => { diff --git a/src/lib/mtproto/mtprotoworker.ts b/src/lib/mtproto/mtprotoworker.ts index 56cecfe9..4758ed3b 100644 --- a/src/lib/mtproto/mtprotoworker.ts +++ b/src/lib/mtproto/mtprotoworker.ts @@ -46,7 +46,7 @@ export class ApiManagerProxy extends CryptoWorkerMethods { } = {} as any; private pending: Array = []; - public updatesProcessor: (obj: any, bool: boolean) => void = null; + public updatesProcessor: (obj: any) => void = null; private log = logger('API-PROXY'); @@ -134,7 +134,7 @@ export class ApiManagerProxy extends CryptoWorkerMethods { if(task.update) { if(this.updatesProcessor) { - this.updatesProcessor(task.update.obj, task.update.bool); + this.updatesProcessor(task.update); } } else if(task.progress) { rootScope.broadcast('download_progress', task.progress); @@ -218,7 +218,7 @@ export class ApiManagerProxy extends CryptoWorkerMethods { } } - public setUpdatesProcessor(callback: (obj: any, bool: boolean) => void) { + public setUpdatesProcessor(callback: (obj: any) => void) { this.updatesProcessor = callback; } diff --git a/src/lib/mtproto/networker.ts b/src/lib/mtproto/networker.ts index 2dd050c7..3dd9de19 100644 --- a/src/lib/mtproto/networker.ts +++ b/src/lib/mtproto/networker.ts @@ -1262,7 +1262,7 @@ export default class MTPNetworker { AppStorage.get('dc').then((baseDcId: number) => { if(baseDcId == this.dcId && !this.isFileNetworker && NetworkerFactory.updatesProcessor) { - NetworkerFactory.updatesProcessor(message, true); + NetworkerFactory.updatesProcessor(message); } }); break; @@ -1360,7 +1360,7 @@ export default class MTPNetworker { this.log.debug('Update', message); if(NetworkerFactory.updatesProcessor !== null) { - NetworkerFactory.updatesProcessor(message, true); + NetworkerFactory.updatesProcessor(message); } break; } diff --git a/src/lib/mtproto/networkerFactory.ts b/src/lib/mtproto/networkerFactory.ts index 79551149..a76a4c1c 100644 --- a/src/lib/mtproto/networkerFactory.ts +++ b/src/lib/mtproto/networkerFactory.ts @@ -3,10 +3,10 @@ import { ConnectionStatusChange, InvokeApiOptions } from "../../types"; import MTTransport from "./transports/transport"; export class NetworkerFactory { - public updatesProcessor: (obj: any, bool: boolean) => void = null; + public updatesProcessor: (obj: any) => void = null; public onConnectionStatusChange: (info: ConnectionStatusChange) => void = null; - public setUpdatesProcessor(callback: (obj: any, bool: boolean) => void) { + public setUpdatesProcessor(callback: (obj: any) => void) { this.updatesProcessor = callback; } diff --git a/src/lib/rootScope.ts b/src/lib/rootScope.ts index 8b3d9491..1af79907 100644 --- a/src/lib/rootScope.ts +++ b/src/lib/rootScope.ts @@ -5,14 +5,17 @@ import type { Poll, PollResults } from "./appManagers/appPollsManager"; import type { MyDialogFilter } from "./storages/filters"; import type { ConnectionStatusChange } from "../types"; import type { UserTyping } from "./appManagers/appChatsManager"; +import type Chat from "../components/chat/chat"; import { DEBUG, MOUNT_CLASS_TO, UserAuth } from "./mtproto/mtproto_config"; import { State } from "./appManagers/appStateManager"; import EventListenerBase from "../helpers/eventListenerBase"; +import { MyDraftMessage } from "./appManagers/appDraftsManager"; type BroadcastEvents = { 'user_update': number, 'user_auth': UserAuth, 'peer_changed': number, + 'peer_changing': Chat, 'peer_pinned_messages': {peerId: number, mids?: number[], pinned?: boolean, unpinAll?: true}, 'peer_pinned_hidden': {peerId: number, maxId: number}, 'peer_typings': {peerId: number, typings: UserTyping[]}, @@ -21,7 +24,7 @@ type BroadcastEvents = { 'filter_update': MyDialogFilter, 'filter_order': number[], - 'dialog_draft': {peerId: number, draft: any, index: number}, + 'dialog_draft': {peerId: number, draft: MyDraftMessage | undefined, index: number}, 'dialog_unread': {peerId: number}, 'dialog_flush': {peerId: number}, 'dialog_drop': {peerId: number, dialog?: Dialog}, @@ -76,7 +79,7 @@ type BroadcastEvents = { 'download_progress': any, 'connection_status_change': ConnectionStatusChange, 'settings_updated': {key: string, value: any}, - //'draft_updated': any, + 'draft_updated': {peerId: number, threadId: number, draft: MyDraftMessage | undefined}, 'event-heavy-animation-start': void, 'event-heavy-animation-end': void diff --git a/src/lib/storages/dialogs.ts b/src/lib/storages/dialogs.ts index 93822f00..7259b0f7 100644 --- a/src/lib/storages/dialogs.ts +++ b/src/lib/storages/dialogs.ts @@ -1,7 +1,7 @@ import { tsNow } from "../../helpers/date"; import type { Message } from "../../layer"; import type { AppChatsManager } from "../appManagers/appChatsManager"; -import type { AppMessagesManager, Dialog } from "../appManagers/appMessagesManager"; +import type { AppMessagesManager, Dialog, MyMessage } from "../appManagers/appMessagesManager"; import type { AppPeersManager } from "../appManagers/appPeersManager"; import type { ServerTimeManager } from "../mtproto/serverTimeManager"; @@ -89,27 +89,32 @@ export default class DialogsStorage { return (date * 0x10000) + ((++this.dialogsNum) & 0xFFFF); } - public generateIndexForDialog(dialog: Dialog, justReturn = false) { + public generateIndexForDialog(dialog: Dialog, justReturn = false, message?: MyMessage) { const channelId = this.appPeersManager.isChannel(dialog.peerId) ? -dialog.peerId : 0; - const mid = dialog.top_message; - const message = this.appMessagesManager.getMessageByPeer(dialog.peerId, mid); + + let topDate = 0; + if(dialog.pFlags.pinned && !justReturn) { + topDate = this.generateDialogPinnedDate(dialog); + } else { + if(!message) { + message = this.appMessagesManager.getMessageByPeer(dialog.peerId, dialog.top_message); + } - let topDate = (message as Message.message).date || Date.now() / 1000; - if(channelId) { - const channel = this.appChatsManager.getChat(channelId); - if(!topDate || channel.date && channel.date > topDate) { - topDate = channel.date; + topDate = (message as Message.message).date || topDate; + if(channelId) { + const channel = this.appChatsManager.getChat(channelId); + if(!topDate || (channel.date && channel.date > topDate)) { + topDate = channel.date; + } + } + + if(dialog.draft && dialog.draft._ === 'draftMessage' && dialog.draft.date > topDate) { + topDate = dialog.draft.date; } } - const savedDraft: any = {};// DraftsManager.saveDraft(peerId, dialog.draft); // warning - if(savedDraft && savedDraft.date > topDate) { - topDate = savedDraft.date; - } - - if(dialog.pFlags.pinned && !justReturn) { - topDate = this.generateDialogPinnedDate(dialog); - //this.log('topDate', peerId, topDate); + if(!topDate) { + topDate = Date.now() / 1000; } const index = this.generateDialogIndex(topDate); diff --git a/src/scripts/generate_mtproto_types.js b/src/scripts/generate_mtproto_types.js index 6196043e..012a8f8b 100644 --- a/src/scripts/generate_mtproto_types.js +++ b/src/scripts/generate_mtproto_types.js @@ -255,4 +255,6 @@ for(const method in methodsMap) { out += `}\n\n`; const path = process.argv[2]; -require('fs').writeFileSync((path || __dirname + '/out/') + 'layer.d.ts', out); \ No newline at end of file +const writePathTo = (path || __dirname + '/out/') + 'layer.d.ts'; +console.log('Writing layer to:', writePathTo); +require('fs').writeFileSync(writePathTo, out); \ No newline at end of file diff --git a/src/scripts/in/schema_additional_params.json b/src/scripts/in/schema_additional_params.json index 690a026b..d7c7979d 100644 --- a/src/scripts/in/schema_additional_params.json +++ b/src/scripts/in/schema_additional_params.json @@ -70,7 +70,8 @@ {"name": "fromId", "type": "number"}, {"name": "grouped_id", "type": "string"}, {"name": "canBeEdited", "type": "boolean"}, - {"name": "unread", "type": "true"} + {"name": "unread", "type": "true"}, + {"name": "rReply", "type": "string"} ] }, { "predicate": "messageService", @@ -80,7 +81,8 @@ {"name": "peerId", "type": "number"}, {"name": "fromId", "type": "number"}, {"name": "canBeEdited", "type": "boolean"}, - {"name": "unread", "type": "true"} + {"name": "unread", "type": "true"}, + {"name": "rReply", "type": "string"} ] }, { "predicate": "messageEmpty", @@ -228,4 +230,10 @@ "params": [ {"name": "phone_number", "type": "string"} ] +}, { + "predicate": "draftMessage", + "params": [ + {"name": "rReply", "type": "string"}, + {"name": "rMessage", "type": "string"} + ] }] \ No newline at end of file