From 18debfb4a6cefb83b220d3fe61a605986c3f2a3c Mon Sep 17 00:00:00 2001 From: Eduard Kuzmenko Date: Tue, 22 Dec 2020 05:02:30 +0200 Subject: [PATCH] Update replies footer --- src/components/avatar.ts | 11 +- src/components/chat/bubbles.ts | 12 ++ src/components/chat/messageRender.ts | 130 +++++++++++------- src/components/chat/pinnedMessage.ts | 6 +- .../sidebarRight/tabs/sharedMedia.ts | 4 +- src/lib/appManagers/appDialogsManager.ts | 12 +- src/lib/appManagers/appMessagesManager.ts | 67 +++++++-- src/lib/rootScope.ts | 4 +- 8 files changed, 175 insertions(+), 71 deletions(-) diff --git a/src/components/avatar.ts b/src/components/avatar.ts index 49011f26..e8a902e3 100644 --- a/src/components/avatar.ts +++ b/src/components/avatar.ts @@ -1,7 +1,7 @@ import appMessagesManager from "../lib/appManagers/appMessagesManager"; import appProfileManager from "../lib/appManagers/appProfileManager"; import rootScope from "../lib/rootScope"; -import { cancelEvent } from "../helpers/dom"; +import { attachClickEvent, cancelEvent } from "../helpers/dom"; import AppMediaViewer, { AppMediaViewerAvatar } from "./appMediaViewer"; import { Photo } from "../layer"; @@ -33,7 +33,7 @@ export default class AvatarElement extends HTMLElement { if(this.getAttribute('clickable') === '') { this.setAttribute('clickable', 'set'); let loading = false; - this.addEventListener('click', async(e) => { + attachClickEvent(this, async(e) => { cancelEvent(e); if(loading) return; //console.log('avatar clicked'); @@ -120,13 +120,6 @@ export default class AvatarElement extends HTMLElement { public update() { appProfileManager.putPhoto(this, this.peerId, this.isDialog, this.peerTitle); } - - adoptedCallback() { - // вызывается, когда элемент перемещается в новый документ - // (происходит в document.adoptNode, используется очень редко) - } - - // у элемента могут быть ещё другие методы и свойства } customElements.define("avatar-element", AvatarElement); \ No newline at end of file diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index 49a1ee5d..e2af99e0 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -38,6 +38,7 @@ import ListenerSetter from "../../helpers/listenerSetter"; import PollElement from "../poll"; import AudioElement from "../audio"; import { MessageEntity, MessageReplies, MessageReplyHeader } from "../../layer"; +import { DEBUG, MOUNT_CLASS_TO } from "../../lib/mtproto/mtproto_config"; const IGNORE_ACTIONS = ['messageActionHistoryClear']; @@ -315,6 +316,17 @@ export default class ChatBubbles { this.listenerSetter.add(this.bubblesContainer, 'click', this.onBubblesClick/* , {capture: true, passive: false} */); + if(DEBUG) { + this.listenerSetter.add(this.bubblesContainer, 'dblclick', (e) => { + const bubble = findUpClassName(e.target, 'grouped-item') || findUpClassName(e.target, 'bubble'); + if(bubble) { + const mid = +bubble.dataset.mid; + this.log('debug message:', this.chat.getMessage(mid)); + this.chat.bubbles.highlightBubble(bubble); + } + }); + } + this.stickyIntersector = new StickyIntersector(this.scrollable.container, (stuck, target) => { for(const timestamp in this.dateMessages) { const dateMessage = this.dateMessages[timestamp]; diff --git a/src/components/chat/messageRender.ts b/src/components/chat/messageRender.ts index b3297aa6..3e090448 100644 --- a/src/components/chat/messageRender.ts +++ b/src/components/chat/messageRender.ts @@ -1,20 +1,19 @@ import { getFullDate } from "../../helpers/date"; import { formatNumber } from "../../helpers/number"; -import { MessageReplies } from "../../layer"; +import { Message } from "../../layer"; import appMessagesManager from "../../lib/appManagers/appMessagesManager"; import appPeersManager from "../../lib/appManagers/appPeersManager"; import RichTextProcessor from "../../lib/richtextprocessor"; +import rootScope from "../../lib/rootScope"; import { ripple } from "../ripple"; import Chat from "./chat"; -type Message = any; - export namespace MessageRender { /* export const setText = () => { }; */ - export const setTime = (chat: Chat, message: Message, bubble: HTMLElement, bubbleContainer: HTMLElement, messageDiv: HTMLElement) => { + export const setTime = (chat: Chat, message: any, bubble: HTMLElement, bubbleContainer: HTMLElement, messageDiv: HTMLElement) => { const date = new Date(message.date * 1000); let time = ('0' + date.getHours()).slice(-2) + ':' + ('0' + date.getMinutes()).slice(-2); @@ -69,50 +68,87 @@ export namespace MessageRender { message: any, messageDiv: HTMLElement }) => { - const replies = message.replies as MessageReplies; const isFooter = !bubble.classList.contains('sticker') && !bubble.classList.contains('emoji-big'); + const repliesFooter = new RepliesFooterElement(); + if(isFooter) { - const container = document.createElement('div'); - container.classList.add('replies-footer'); - - let leftHTML = '', lastStyle = ''; - if(replies?.recent_repliers) { - leftHTML += ''; - } else { - leftHTML = ''; - } - - let text: string; - if(replies?.replies) { - text = replies.replies + ' ' + (replies.replies > 1 ? 'Comments' : 'Comment'); - } else { - text = 'Leave a Comment'; - } - - if(replies) { - const historyStorage = appMessagesManager.getHistoryStorage(-replies.channel_id); - if(replies.read_max_id < replies.max_id && (!historyStorage.readMaxId || historyStorage.readMaxId < replies.max_id)) { - container.classList.add('is-unread'); - } - } - - container.innerHTML = `${leftHTML}${text}`; - - const rippleContainer = document.createElement('div'); - container.append(rippleContainer); - ripple(rippleContainer); - bubbleContainer.prepend(container); + repliesFooter.message = message; + repliesFooter.type = 'footer'; + bubbleContainer.prepend(repliesFooter); } }; -} \ No newline at end of file +} + +rootScope.on('replies_updated', (e) => { + const message = e.detail; + (Array.from(document.querySelectorAll(`replies-footer-element[data-post-key="${message.peerId}_${message.mid}"]`)) as RepliesFooterElement[]).forEach(element => { + element.message = message; + element.render(); + }); +}); + +class RepliesFooterElement extends HTMLElement { + public message: Message.message; + public type: 'footer' | 'beside'; + + private updated = false; + + constructor() { + super(); + + this.classList.add('replies-footer'); + } + + connectedCallback() { + this.render(); + this.dataset.postKey = this.message.peerId + '_' + this.message.mid; + } + + public render() { + const replies = this.message.replies; + + let leftHTML = '', lastStyle = ''; + if(replies.recent_repliers) { + leftHTML += ''; + } else { + leftHTML = ''; + } + + let text: string; + if(replies.replies) { + text = replies.replies + ' ' + (replies.replies > 1 ? 'Comments' : 'Comment'); + } else { + text = 'Leave a Comment'; + } + + const historyStorage = appMessagesManager.getHistoryStorage(-replies.channel_id); + if(replies.read_max_id < replies.max_id && (!historyStorage.readMaxId || historyStorage.readMaxId < replies.max_id)) { + this.classList.add('is-unread'); + } + + if(!this.updated) { + appMessagesManager.subscribeRepliesThread(this.message.peerId, this.message.mid); + appMessagesManager.updateMessage(this.message.peerId, this.message.mid, 'replies_updated'); + this.updated = true; + } + + this.innerHTML = `${leftHTML}${text}`; + + const rippleContainer = document.createElement('div'); + this.append(rippleContainer); + ripple(rippleContainer); + } +} + +customElements.define('replies-footer-element', RepliesFooterElement); \ No newline at end of file diff --git a/src/components/chat/pinnedMessage.ts b/src/components/chat/pinnedMessage.ts index 71178ea3..211a466c 100644 --- a/src/components/chat/pinnedMessage.ts +++ b/src/components/chat/pinnedMessage.ts @@ -7,7 +7,7 @@ import PinnedContainer from "./pinnedContainer"; import PinnedMessageBorder from "./pinnedMessageBorder"; import ReplyContainer, { wrapReplyDivAndCaption } from "./replyContainer"; import rootScope from "../../lib/rootScope"; -import { cancelEvent, findUpClassName, getElementByPoint, handleScrollSideEvent } from "../../helpers/dom"; +import { attachClickEvent, cancelEvent, findUpClassName, getElementByPoint, handleScrollSideEvent } from "../../helpers/dom"; import Chat from "./chat"; import ListenerSetter from "../../helpers/listenerSetter"; import ButtonIcon from "../buttonIcon"; @@ -271,10 +271,10 @@ export default class ChatPinnedMessage { 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) => { + attachClickEvent(this.btnOpen, (e) => { cancelEvent(e); this.topbar.openPinned(true); - }); + }, {listenerSetter: this.listenerSetter}); this.listenerSetter.add(rootScope, 'peer_pinned_messages', (e) => { const peerId = e.detail.peerId; diff --git a/src/components/sidebarRight/tabs/sharedMedia.ts b/src/components/sidebarRight/tabs/sharedMedia.ts index 915fa4aa..f5bec469 100644 --- a/src/components/sidebarRight/tabs/sharedMedia.ts +++ b/src/components/sidebarRight/tabs/sharedMedia.ts @@ -904,7 +904,9 @@ export default class AppSharedMediaTab implements SliderTab { setText(appPeersManager.getPeerUsername(peerId), this.profileElements.username); } - this.profileElements.notificationsCheckbox.checked = !appMessagesManager.isPeerMuted(peerId); + const muted = appMessagesManager.isPeerMuted(peerId); + this.profileElements.notificationsCheckbox.checked = !muted; + this.profileElements.notificationsStatus.innerText = muted ? 'Disabled' : 'Enabled'; } else { window.requestAnimationFrame(() => { this.profileElements.notificationsRow.style.display = 'none'; diff --git a/src/lib/appManagers/appDialogsManager.ts b/src/lib/appManagers/appDialogsManager.ts index b971d7a6..23c63672 100644 --- a/src/lib/appManagers/appDialogsManager.ts +++ b/src/lib/appManagers/appDialogsManager.ts @@ -19,7 +19,7 @@ import {MyDialogFilter as DialogFilter} from "../storages/filters"; import appPeersManager from './appPeersManager'; import appStateManager from "./appStateManager"; import appUsersManager, { User } from "./appUsersManager"; -import { App, MOUNT_CLASS_TO } from "../mtproto/mtproto_config"; +import { App, DEBUG, MOUNT_CLASS_TO } from "../mtproto/mtproto_config"; import Button from "../../components/button"; import SetTransition from "../../components/singleTransition"; import AppStorage from '../storage'; @@ -877,6 +877,16 @@ export class AppDialogsManager { } }, {capture: true}); + if(DEBUG) { + list.addEventListener('dblclick', (e) => { + const li = findUpTag(e.target, 'LI'); + if(li) { + const peerId = +li.getAttribute('data-peerId'); + this.log('debug dialog:', appMessagesManager.getDialogByPeerId(peerId)); + } + }); + } + if(withContext) { attachContextMenuListener(list, this.contextMenu.onContextMenu); } diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index 3e133bf2..f281fd1a 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -4,7 +4,7 @@ import { tsNow } from "../../helpers/date"; import { copy, defineNotNumerableProperties, getObjectKeysAndSort } from "../../helpers/object"; import { randomLong } from "../../helpers/random"; import { splitStringByLength, limitSymbols } from "../../helpers/string"; -import { Dialog as MTDialog, DialogPeer, DocumentAttribute, InputMedia, InputMessage, InputNotifyPeer, InputPeerNotifySettings, InputSingleMedia, Message, MessageAction, MessageEntity, MessageReplyHeader, MessagesDialogs, MessagesFilter, MessagesMessages, MessagesPeerDialogs, MethodDeclMap, NotifyPeer, PhotoSize, SendMessageAction, Update } from "../../layer"; +import { Dialog as MTDialog, DialogPeer, DocumentAttribute, InputMedia, InputMessage, InputNotifyPeer, InputPeerNotifySettings, InputSingleMedia, Message, MessageAction, MessageEntity, MessageReplies, MessageReplyHeader, MessagesDialogs, MessagesFilter, MessagesMessages, MessagesPeerDialogs, MethodDeclMap, NotifyPeer, PhotoSize, SendMessageAction, Update } from "../../layer"; import { InvokeApiOptions } from "../../types"; import { langPack } from "../langPack"; import { logger, LogLevels } from "../logger"; @@ -110,6 +110,10 @@ export class AppMessagesManager { } = {}; public pinnedMessages: {[peerId: string]: PinnedStorage} = {}; + public threadsToReplies: { + [peerId_threadId: string]: string; + } = {}; + public pendingByRandomId: { [randomId: string]: { peerId: number, @@ -2399,7 +2403,7 @@ export class AppMessagesManager { }); } - public markDialogUnread(peerId: number, read?: boolean) { + public markDialogUnread(peerId: number, read?: true) { const dialog = this.getDialogByPeerId(peerId)[0]; if(!dialog) return Promise.reject(); @@ -3022,6 +3026,15 @@ export class AppMessagesManager { }); } + public subscribeRepliesThread(peerId: number, mid: number) { + const repliesKey = peerId + '_' + mid; + for(const threadKey in this.threadsToReplies) { + if(this.threadsToReplies[threadKey] === repliesKey) return; + } + + this.getDiscussionMessage(peerId, mid); + } + public getDiscussionMessage(peerId: number, mid: number) { return apiManager.invokeApi('messages.getDiscussionMessage', { peer: appPeersManager.getInputPeerById(peerId), @@ -3037,6 +3050,8 @@ export class AppMessagesManager { result.read_inbox_max_id = historyStorage.readMaxId = this.generateMessageId(result.read_inbox_max_id) || 0; result.read_outbox_max_id = historyStorage.readOutboxMaxId = this.generateMessageId(result.read_outbox_max_id) || 0; + this.threadsToReplies[message.peerId + '_' + message.mid] = peerId + '_' + mid; + return result; }); } @@ -3315,11 +3330,10 @@ export class AppMessagesManager { // this.log.warn(dT(), 'message unread', message.mid, message.pFlags.unread) const pendingMessage = this.checkPendingMessage(message); - const historyStorage = this.getHistoryStorage(peerId); const history = message.mid > 0 ? historyStorage.history : historyStorage.pending; - if(history.indexOf(message.mid) != -1) { + if(history.indexOf(message.mid) !== -1) { return false; } const topMsgId = history[0]; @@ -3338,8 +3352,8 @@ export class AppMessagesManager { rootScope.broadcast('history_reply_markup', {peerId}); } - if(!message.pFlags.out && message.from_id) { - appUsersManager.forceUserOnline(appPeersManager.getPeerId(message.from_id), message.date); + if(message.fromId > 0 && !message.pFlags.out && message.from_id) { + appUsersManager.forceUserOnline(message.fromId, message.date); } if(!pendingMessage) { @@ -3352,6 +3366,8 @@ export class AppMessagesManager { this.newMessagesHandlePromise = window.setTimeout(this.handleNewMessages, 0); } } + + this.updateMessageRepliesIfNeeded(message); const dialog = foundDialog[0]; if(dialog) { @@ -3950,6 +3966,37 @@ export class AppMessagesManager { } } + private updateMessageRepliesIfNeeded(threadMessage: MyMessage) { + try { // * на всякий случай, скорее всего это не понадобится + if(threadMessage.peerId < 0 && threadMessage.reply_to) { + const threadId = threadMessage.reply_to.reply_to_top_id || threadMessage.reply_to.reply_to_msg_id; + const threadKey = threadMessage.peerId + '_' + threadId; + const repliesKey = this.threadsToReplies[threadKey]; + if(repliesKey) { + const [peerId, mid] = repliesKey.split('_').map(n => +n); + + this.updateMessage(peerId, mid, 'replies_updated'); + } + } + } catch(err) { + this.log.error('incrementMessageReplies err', err, threadMessage); + } + } + + public updateMessage(peerId: number, mid: number, broadcastEventName?: 'replies_updated'): Promise { + const promise: Promise = this.wrapSingleMessage(peerId, mid, true).then(() => { + const message = this.getMessageByPeer(peerId, mid); + + if(broadcastEventName) { + rootScope.broadcast(broadcastEventName, message); + } + + return message; + }); + + return promise; + } + private checkPendingMessage(message: any) { const randomId = this.pendingByMessageId[message.mid]; let pendingMessage: any; @@ -4474,7 +4521,7 @@ export class AppMessagesManager { Promise.all(promises).finally(() => { this.fetchSingleMessagesPromise = null; - if(this.needSingleMessages.length) this.fetchSingleMessages(); + if(Object.keys(this.needSingleMessages).length) this.fetchSingleMessages(); resolve(); }); }, 0); @@ -4482,10 +4529,10 @@ export class AppMessagesManager { } public wrapSingleMessage(peerId: number, msgId: number, overwrite = false): Promise { - if(this.getMessagesStorage(peerId)[msgId] && !overwrite) { + if(!this.getMessageByPeer(peerId, msgId).deleted && !overwrite) { rootScope.broadcast('messages_downloaded', {peerId, mids: [msgId]}); return Promise.resolve(); - } else if(!this.needSingleMessages[peerId] || this.needSingleMessages[peerId].indexOf(msgId) == -1) { + } else if(!this.needSingleMessages[peerId] || this.needSingleMessages[peerId].indexOf(msgId) === -1) { (this.needSingleMessages[peerId] ?? (this.needSingleMessages[peerId] = [])).push(msgId); return this.fetchSingleMessages(); } else if(this.fetchSingleMessagesPromise) { @@ -4531,6 +4578,8 @@ export class AppMessagesManager { } } + this.updateMessageRepliesIfNeeded(message); + if(!message.pFlags.out && message.pFlags.unread) { history.unread++; } diff --git a/src/lib/rootScope.ts b/src/lib/rootScope.ts index 1be9bdce..9ad3b808 100644 --- a/src/lib/rootScope.ts +++ b/src/lib/rootScope.ts @@ -1,4 +1,4 @@ -import type { StickerSet, Update } from "../layer"; +import type { Message, StickerSet, Update } from "../layer"; import type { MyDocument } from "./appManagers/appDocsManager"; import type { AppMessagesManager, Dialog, MessagesStorage } from "./appManagers/appMessagesManager"; import type { Poll, PollResults } from "./appManagers/appPollsManager"; @@ -46,6 +46,8 @@ type BroadcastEvents = { 'messages_downloaded': {peerId: number, mids: number[]}, 'messages_media_read': {peerId: number, mids: number[]}, + 'replies_updated': Message.message, + 'scheduled_new': {peerId: number, mid: number}, 'scheduled_delete': {peerId: number, mids: number[]},