/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once #include "data/data_types.h" #include "data/data_peer.h" #include "dialogs/dialogs_entry.h" #include "ui/effects/send_action_animations.h" #include "base/observer.h" #include "base/timer.h" #include "base/variant.h" #include "base/flat_set.h" #include "base/flags.h" class History; class HistoryBlock; class HistoryItem; class HistoryMessage; class HistoryService; class HistoryMedia; class AuthSession; namespace Data { struct Draft; class Session; class Folder; } // namespace Data namespace Dialogs { class Row; class IndexedList; } // namespace Dialogs namespace HistoryView { class Element; } // namespace HistoryView namespace AdminLog { class LocalIdManager; } // namespace AdminLog enum class NewMessageType { Unread, Last, Existing, }; enum class UnreadMentionType { New, // when new message is added to history Existing, // when some messages slice was received }; class History final : public Dialogs::Entry { public: using Element = HistoryView::Element; History(not_null owner, PeerId peerId); History(const History &) = delete; History &operator=(const History &) = delete; ~History(); ChannelId channelId() const; bool isChannel() const; bool isMegagroup() const; not_null migrateToOrMe() const; History *migrateFrom() const; MsgRange rangeForDifferenceRequest() const; HistoryService *insertJoinedMessage(bool unread); void checkJoinedMessage(bool createUnread = false); void removeJoinedMessage(); bool isEmpty() const; bool isDisplayedEmpty() const; Element *findFirstNonEmpty() const; Element *findLastNonEmpty() const; bool hasOrphanMediaGroupPart() const; bool removeOrphanMediaGroupPart(); QVector collectMessagesFromUserToDelete( not_null user) const; enum class ClearType { Unload, DeleteChat, ClearHistory, }; void clear(ClearType type); void clearUpTill(MsgId availableMinId); void applyGroupAdminChanges( const base::flat_map &changes); HistoryItem *addNewMessage(const MTPMessage &msg, NewMessageType type); HistoryItem *addToHistory(const MTPMessage &msg); not_null addNewService( MsgId msgId, TimeId date, const QString &text, MTPDmessage::Flags flags = 0, bool newMsg = true); not_null addNewForwarded( MsgId id, MTPDmessage::Flags flags, TimeId date, UserId from, const QString &postAuthor, not_null original); not_null addNewDocument( MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, TimeId date, UserId from, const QString &postAuthor, not_null document, const TextWithEntities &caption, const MTPReplyMarkup &markup); not_null addNewPhoto( MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, TimeId date, UserId from, const QString &postAuthor, not_null photo, const TextWithEntities &caption, const MTPReplyMarkup &markup); not_null addNewGame( MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, TimeId date, UserId from, const QString &postAuthor, not_null game, const MTPReplyMarkup &markup); // Used only internally and for channel admin log. HistoryItem *createItem( const MTPMessage &message, bool detachExistingItem); std::vector> createItems( const QVector &data); void addOlderSlice(const QVector &slice); void addNewerSlice(const QVector &slice); void newItemAdded(not_null item); MsgId readInbox(); void applyInboxReadUpdate( FolderId folderId, MsgId upTo, int stillUnread, int32 channelPts = 0); void inboxRead(MsgId upTo, std::optional stillUnread = {}); void inboxRead(not_null wasRead); void outboxRead(MsgId upTo); void outboxRead(not_null wasRead); bool isServerSideUnread(not_null item) const; MsgId loadAroundId() const; int unreadCount() const; bool unreadCountKnown() const; void setUnreadCount(int newUnreadCount); void setUnreadMark(bool unread); bool unreadMark() const; int unreadCountForBadge() const; // unreadCount || unreadMark ? 1 : 0. bool mute() const; bool changeMute(bool newMute); void addUnreadBar(); void destroyUnreadBar(); bool hasNotFreezedUnreadBar() const; Element *unreadBar() const; void calculateFirstUnreadMessage(); void unsetFirstUnreadMessage(); Element *firstUnreadMessage() const; void clearNotifications(); bool loadedAtBottom() const; // last message is in the list void setNotLoadedAtBottom(); bool loadedAtTop() const; // nothing was added after loading history back bool isReadyFor(MsgId msgId); // has messages for showing history at msgId void getReadyFor(MsgId msgId); HistoryItem *lastMessage() const; bool lastMessageKnown() const; void unknownMessageDeleted(MsgId messageId); void applyDialogTopMessage(MsgId topMessageId); void applyDialog(Data::Folder *requestFolder, const MTPDdialog &data); void applyPinnedUpdate(const MTPDupdateDialogPinned &data); void applyDialogFields( Data::Folder *folder, int unreadCount, MsgId maxInboxRead, MsgId maxOutboxRead); void dialogEntryApplied(); MsgId minMsgId() const; MsgId maxMsgId() const; MsgId msgIdForRead() const; HistoryItem *lastSentMessage() const; void resizeToWidth(int newWidth); int height() const; void itemRemoved(not_null item); void itemVanished(not_null item); HistoryItem *currentNotification(); bool hasNotification() const; void skipNotification(); void popNotification(HistoryItem *item); bool hasPendingResizedItems() const; void setHasPendingResizedItems(); bool mySendActionUpdated(SendAction::Type type, bool doing); bool paintSendAction( Painter &p, int x, int y, int availableWidth, int outerWidth, style::color color, crl::time now); // Interface for Histories bool updateSendActionNeedsAnimating( crl::time now, bool force = false); bool updateSendActionNeedsAnimating( not_null user, const MTPSendMessageAction &action); void clearLastKeyboard(); int getUnreadMentionsLoadedCount() const { return _unreadMentions.size(); } MsgId getMinLoadedUnreadMention() const { return _unreadMentions.empty() ? 0 : _unreadMentions.front(); } MsgId getMaxLoadedUnreadMention() const { return _unreadMentions.empty() ? 0 : _unreadMentions.back(); } int getUnreadMentionsCount(int notLoadedValue = -1) const { return _unreadMentionsCount ? *_unreadMentionsCount : notLoadedValue; } bool hasUnreadMentions() const { return (getUnreadMentionsCount() > 0); } void setUnreadMentionsCount(int count); bool addToUnreadMentions(MsgId msgId, UnreadMentionType type); void eraseFromUnreadMentions(MsgId msgId); void addUnreadMentionsSlice(const MTPmessages_Messages &result); Data::Draft *localDraft() const { return _localDraft.get(); } Data::Draft *cloudDraft() const { return _cloudDraft.get(); } Data::Draft *editDraft() const { return _editDraft.get(); } void setLocalDraft(std::unique_ptr &&draft); void takeLocalDraft(History *from); void setCloudDraft(std::unique_ptr &&draft); Data::Draft *createCloudDraft(const Data::Draft *fromDraft); bool skipCloudDraft(const QString &text, MsgId replyTo, TimeId date) const; void setSentDraftText(const QString &text); void clearSentDraftText(const QString &text); void setEditDraft(std::unique_ptr &&draft); void clearLocalDraft(); void clearCloudDraft(); void applyCloudDraft(); void clearEditDraft(); void draftSavedToCloud(); Data::Draft *draft() { return _editDraft ? editDraft() : localDraft(); } const MessageIdsList &forwardDraft() const { return _forwardDraft; } HistoryItemsList validateForwardDraft(); void setForwardDraft(MessageIdsList &&items); History *migrateSibling() const; bool useProxyPromotion() const; int fixedOnTopIndex() const override; void updateChatListExistence() override; bool shouldBeInChatList() const override; bool toImportant() const override; int chatListUnreadCount() const override; bool chatListUnreadMark() const override; bool chatListMutedBadge() const override; Dialogs::UnreadState chatListUnreadState() const override; HistoryItem *chatListMessage() const override; bool chatListMessageKnown() const override; void requestChatListMessage() override; const QString &chatListName() const override; const base::flat_set &chatListNameWords() const override; const base::flat_set &chatListFirstLetters() const override; void loadUserpic() override; void paintUserpic( Painter &p, int x, int y, int size) const override; void setFakeChatListMessageFrom(const MTPmessages_Messages &data); void checkChatListMessageRemoved(not_null item); void forgetScrollState() { scrollTopItem = nullptr; } // find the correct scrollTopItem and scrollTopOffset using given top // of the displayed window relative to the history start coordinate void countScrollState(int top); std::shared_ptr adminLogIdManager(); bool folderKnown() const override; Data::Folder *folder() const override; void setFolder( not_null folder, HistoryItem *folderDialogItem = nullptr); void clearFolder(); // Still public data. std::deque> blocks; not_null peer; // we save the last showAtMsgId to restore the state when switching // between different conversation histories MsgId showAtMsgId = ShowAtUnreadMsgId; // we save a pointer of the history item at the top of the displayed window // together with an offset from the window top to the top of this message // resulting scrollTop = top(scrollTopItem) + scrollTopOffset Element *scrollTopItem = nullptr; int scrollTopOffset = 0; bool lastKeyboardInited = false; bool lastKeyboardUsed = false; MsgId lastKeyboardId = 0; MsgId lastKeyboardHiddenId = 0; PeerId lastKeyboardFrom = 0; mtpRequestId sendRequestId = 0; Text cloudDraftTextCache; private: friend class HistoryBlock; enum class Flag { f_has_pending_resized_items = (1 << 0), }; using Flags = base::flags; friend inline constexpr auto is_flag_type(Flag) { return true; }; // when this item is destroyed scrollTopItem just points to the next one // and scrollTopOffset remains the same // if we are at the bottom of the window scrollTopItem == nullptr and // scrollTopOffset is undefined void getNextScrollTopItem(HistoryBlock *block, int32 i); // helper method for countScrollState(int top) void countScrollTopItem(int top); HistoryItem *addNewToLastBlock(const MTPMessage &msg, NewMessageType type); // this method just removes a block from the blocks list // when the last item from this block was detached and // calls the required previousItemChanged() void removeBlock(not_null block); void clearSharedMedia(); not_null addNewItem( not_null item, bool unread); not_null addNewInTheMiddle( not_null item, int blockIndex, int itemIndex); // All this methods add a new item to the first or last block // depending on if we are in isBuildingFronBlock() state. // The last block is created on the go if it is needed. // Adds the item to the back or front block, depending on // isBuildingFrontBlock(), creating the block if necessary. void addItemToBlock(not_null item); // Usually all new items are added to the last block. // Only when we scroll up and add a new slice to the // front we want to create a new front block. void startBuildingFrontBlock(int expectedItemsCount = 1); void finishBuildingFrontBlock(); bool isBuildingFrontBlock() const { return _buildingFrontBlock != nullptr; } void checkForLoadedAtTop(not_null added); void mainViewRemoved( not_null block, not_null view); void removeNotification(not_null item); TimeId adjustedChatListTimeId() const override; void changedChatListPinHook() override; void setInboxReadTill(MsgId upTo); void setOutboxReadTill(MsgId upTo); void readClientSideMessages(); void applyMessageChanges( not_null item, const MTPMessage &original); void applyServiceChanges( not_null item, const MTPDmessageService &data); // After adding a new history slice check lastMessage / loadedAtBottom. void checkLastMessage(); void setLastMessage(HistoryItem *item); void refreshChatListMessage(); void setChatListMessage(HistoryItem *item); std::optional computeChatListMessageFromLast() const; void setChatListMessageFromLast(); void setChatListMessageUnknown(); void setFakeChatListMessage(); // Add all items to the unread mentions if we were not loaded at bottom and now are. void checkAddAllToUnreadMentions(); void addToSharedMedia(const std::vector> &items); void addEdgesToSharedMedia(); void addItemsToLists(const std::vector> &items); void clearSendAction(not_null from); bool clearUnreadOnClientSide() const; bool skipUnreadUpdate() const; HistoryItem *lastAvailableMessage() const; void getNextFirstUnreadMessage(); bool nonEmptyCountMoreThan(int count) const; std::optional countUnread(MsgId upTo) const; // Creates if necessary a new block for adding item. // Depending on isBuildingFrontBlock() gets front or back block. HistoryBlock *prepareBlockForAddingItem(); void viewReplaced(not_null was, Element *now); void createLocalDraftFromCloud(); void setFolderPointer(Data::Folder *folder); Flags _flags = 0; bool _mute = false; int _width = 0; int _height = 0; Element *_unreadBarView = nullptr; Element *_firstUnreadView = nullptr; HistoryService *_joinedMessage = nullptr; bool _loadedAtTop = false; bool _loadedAtBottom = true; std::optional _folder; std::optional _inboxReadBefore; std::optional _outboxReadBefore; std::optional _unreadCount; std::optional _unreadMentionsCount; base::flat_set _unreadMentions; std::optional _lastMessage; // This almost always is equal to _lastMessage. The only difference is // for a group that migrated to a supergroup. Then _lastMessage can // be a migrate message, but _chatListMessage should be the one before. std::optional _chatListMessage; bool _unreadMark = false; // A pointer to the block that is currently being built. // We hold this pointer so we can destroy it while building // and then create a new one if it is necessary. struct BuildingBlock { int expectedItemsCount = 0; // optimization for block->items.reserve() call HistoryBlock *block = nullptr; }; std::unique_ptr _buildingFrontBlock; std::unique_ptr _localDraft, _cloudDraft; std::unique_ptr _editDraft; std::optional _lastSentDraftText; TimeId _lastSentDraftTime = 0; MessageIdsList _forwardDraft; base::flat_map, crl::time> _typing; base::flat_map, SendAction> _sendActions; QString _sendActionString; Text _sendActionText; Ui::SendActionAnimation _sendActionAnimation; base::flat_map _mySendActions; std::deque> _notifications; std::weak_ptr _adminLogIdManager; }; class HistoryBlock { public: using Element = HistoryView::Element; HistoryBlock(not_null history); HistoryBlock(const HistoryBlock &) = delete; HistoryBlock &operator=(const HistoryBlock &) = delete; ~HistoryBlock(); std::vector> messages; void remove(not_null view); void refreshView(not_null view); int resizeGetHeight(int newWidth, bool resizeAllItems); int y() const { return _y; } void setY(int y) { _y = y; } int height() const { return _height; } not_null history() const { return _history; } HistoryBlock *previousBlock() const { Expects(_indexInHistory >= 0); return (_indexInHistory > 0) ? _history->blocks[_indexInHistory - 1].get() : nullptr; } HistoryBlock *nextBlock() const { Expects(_indexInHistory >= 0); return (_indexInHistory + 1 < _history->blocks.size()) ? _history->blocks[_indexInHistory + 1].get() : nullptr; } void setIndexInHistory(int index) { _indexInHistory = index; } int indexInHistory() const { Expects(_indexInHistory >= 0); Expects(_indexInHistory < _history->blocks.size()); Expects(_history->blocks[_indexInHistory].get() == this); return _indexInHistory; } protected: const not_null _history; int _y = 0; int _height = 0; int _indexInHistory = -1; };