/* 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 "data/data_drafts.h" #include "dialogs/dialogs_entry.h" #include "dialogs/ui/dialogs_message_view.h" #include "history/view/history_view_send_action.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; struct HistoryMessageMarkupData; namespace Main { class Session; } // namespace Main namespace Data { struct Draft; class Session; class Folder; class ChatFilter; enum class ForwardOptions { PreserveInfo, NoSenderNames, NoNamesAndCaptions, }; struct ForwardDraft { MessageIdsList ids; ForwardOptions options = ForwardOptions::PreserveInfo; }; struct ResolvedForwardDraft { HistoryItemsList items; ForwardOptions options = ForwardOptions::PreserveInfo; }; } // namespace Data namespace Dialogs { class Row; class IndexedList; } // namespace Dialogs namespace HistoryView { class Element; } // namespace HistoryView 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; void checkLocalMessages(); void removeJoinedMessage(); bool isEmpty() const; bool isDisplayedEmpty() const; Element *findFirstNonEmpty() const; Element *findFirstDisplayed() const; Element *findLastNonEmpty() const; Element *findLastDisplayed() 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_set &changes); template not_null makeMessage(Args &&...args) { return static_cast( insertItem( std::make_unique( this, std::forward(args)...)).get()); } template not_null makeServiceMessage(Args &&...args) { return static_cast( insertItem( std::make_unique( this, std::forward(args)...)).get()); } void destroyMessage(not_null item); void unpinAllMessages(); HistoryItem *addNewMessage( MsgId id, const MTPMessage &msg, MessageFlags localFlags, NewMessageType type); not_null addNewLocalMessage( MsgId id, MessageFlags flags, UserId viaBotId, MsgId replyTo, TimeId date, PeerId from, const QString &postAuthor, const TextWithEntities &text, const MTPMessageMedia &media, HistoryMessageMarkupData &&markup, uint64 groupedId = 0); not_null addNewLocalMessage( MsgId id, MessageFlags flags, TimeId date, PeerId from, const QString &postAuthor, not_null forwardOriginal); not_null addNewLocalMessage( MsgId id, MessageFlags flags, UserId viaBotId, MsgId replyTo, TimeId date, PeerId from, const QString &postAuthor, not_null document, const TextWithEntities &caption, HistoryMessageMarkupData &&markup); not_null addNewLocalMessage( MsgId id, MessageFlags flags, UserId viaBotId, MsgId replyTo, TimeId date, PeerId from, const QString &postAuthor, not_null photo, const TextWithEntities &caption, HistoryMessageMarkupData &&markup); not_null addNewLocalMessage( MsgId id, MessageFlags flags, UserId viaBotId, MsgId replyTo, TimeId date, PeerId from, const QString &postAuthor, not_null game, HistoryMessageMarkupData &&markup); // Used only internally and for channel admin log. HistoryItem *createItem( MsgId id, const MTPMessage &message, MessageFlags localFlags, bool detachExistingItem); std::vector> createItems( const QVector &data); void addOlderSlice(const QVector &slice); void addNewerSlice(const QVector &slice); void newItemAdded(not_null item); void registerLocalMessage(not_null item); void unregisterLocalMessage(not_null item); [[nodiscard]] auto localMessages() -> const base::flat_set> &; [[nodiscard]] HistoryItem *latestSendingMessage() const; [[nodiscard]] bool readInboxTillNeedsRequest(MsgId tillId); 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); [[nodiscard]] bool isServerSideUnread( not_null item) const; [[nodiscard]] MsgId loadAroundId() const; [[nodiscard]] MsgId inboxReadTillId() const; [[nodiscard]] MsgId outboxReadTillId() const; [[nodiscard]] bool trackUnreadMessages() const; [[nodiscard]] int unreadCount() const; [[nodiscard]] bool unreadCountKnown() const; // Some old unread count is known, but we read history till some place. [[nodiscard]] bool unreadCountRefreshNeeded(MsgId readTillId) const; void setUnreadCount(int newUnreadCount); void setUnreadMark(bool unread); [[nodiscard]] bool unreadMark() const; void setFakeUnreadWhileOpened(bool enabled); [[nodiscard]] bool fakeUnreadWhileOpened() const; [[nodiscard]] int unreadCountForBadge() const; // unreadCount || unreadMark ? 1 : 0. [[nodiscard]] bool mute() const; bool changeMute(bool newMute); void addUnreadBar(); void destroyUnreadBar(); [[nodiscard]] Element *unreadBar() const; void calculateFirstUnreadMessage(); void unsetFirstUnreadMessage(); [[nodiscard]] Element *firstUnreadMessage() const; void clearNotifications(); void clearIncomingNotifications(); [[nodiscard]] bool loadedAtBottom() const; // last message is in the list void setNotLoadedAtBottom(); [[nodiscard]] bool loadedAtTop() const; // nothing was added after loading history back [[nodiscard]] bool isReadyFor(MsgId msgId); // has messages for showing history at msgId void getReadyFor(MsgId msgId); [[nodiscard]] HistoryItem *lastMessage() const; [[nodiscard]] HistoryItem *lastServerMessage() const; [[nodiscard]] bool lastMessageKnown() const; [[nodiscard]] bool lastServerMessageKnown() 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(); void cacheTopPromotion( bool promoted, const QString &type, const QString &message); [[nodiscard]] QStringView topPromotionType() const; [[nodiscard]] QString topPromotionMessage() const; [[nodiscard]] bool topPromotionAboutShown() const; void markTopPromotionAboutShown(); [[nodiscard]] bool canHaveSponsoredMessages() const; MsgId minMsgId() const; MsgId maxMsgId() const; MsgId msgIdForRead() const; HistoryItem *lastEditableMessage() const; void resizeToWidth(int newWidth); void forceFullResize(); 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(); [[nodiscard]] auto sendActionPainter() -> not_null { return &_sendActionPainter; } 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 *draft(Data::DraftKey key) const; void setDraft(Data::DraftKey key, std::unique_ptr &&draft); void clearDraft(Data::DraftKey key); [[nodiscard]] const Data::HistoryDrafts &draftsMap() const; void setDraftsMap(Data::HistoryDrafts &&map); Data::Draft *localDraft() const { return draft(Data::DraftKey::Local()); } Data::Draft *localEditDraft() const { return draft(Data::DraftKey::LocalEdit()); } Data::Draft *cloudDraft() const { return draft(Data::DraftKey::Cloud()); } void setLocalDraft(std::unique_ptr &&draft) { setDraft(Data::DraftKey::Local(), std::move(draft)); } void setLocalEditDraft(std::unique_ptr &&draft) { setDraft(Data::DraftKey::LocalEdit(), std::move(draft)); } void setCloudDraft(std::unique_ptr &&draft) { setDraft(Data::DraftKey::Cloud(), std::move(draft)); } void clearLocalDraft() { clearDraft(Data::DraftKey::Local()); } void clearCloudDraft() { clearDraft(Data::DraftKey::Cloud()); } void clearLocalEditDraft() { clearDraft(Data::DraftKey::LocalEdit()); } void clearDrafts(); Data::Draft *createCloudDraft(const Data::Draft *fromDraft); bool skipCloudDraftUpdate(TimeId date) const; void startSavingCloudDraft(); void finishSavingCloudDraft(TimeId savedAt); void takeLocalDraft(not_null from); void applyCloudDraft(); void draftSavedToCloud(); [[nodiscard]] const Data::ForwardDraft &forwardDraft() const { return _forwardDraft; } [[nodiscard]] Data::ResolvedForwardDraft resolveForwardDraft( const Data::ForwardDraft &draft) const; [[nodiscard]] Data::ResolvedForwardDraft resolveForwardDraft(); void setForwardDraft(Data::ForwardDraft &&draft); History *migrateSibling() const; [[nodiscard]] bool useTopPromotion() const; int fixedOnTopIndex() const override; void updateChatListExistence() override; bool shouldBeInChatList() 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 QString &chatListNameSortKey() const override; const base::flat_set &chatListNameWords() const override; const base::flat_set &chatListFirstLetters() const override; void loadUserpic() override; void paintUserpic( Painter &p, std::shared_ptr &view, int x, int y, int size) const override; void refreshChatListNameSortKey(); void setFakeChatListMessageFrom(const MTPmessages_Messages &data); void checkChatListMessageRemoved(not_null item); void applyChatListGroup( ChannelId channelId, const MTPmessages_Messages &data); 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); [[nodiscard]] std::pair findItemAndOffset(int top) const; [[nodiscard]] MsgId nextNonHistoryEntryId(); bool folderKnown() const override; Data::Folder *folder() const override; void setFolder( not_null folder, HistoryItem *folderDialogItem = nullptr); void clearFolder(); // Interface for Data::Histories. void setInboxReadTill(MsgId upTo); std::optional countStillUnreadLocal(MsgId readTillId) const; [[nodiscard]] bool hasPinnedMessages() const; void setHasPinnedMessages(bool has); // 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; Ui::Text::String cloudDraftTextCache; Dialogs::Ui::MessageView lastItemDialogsView; 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) [[nodiscard]] Element *findScrollTopItem(int top) const; // 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 insertItem(std::unique_ptr item); not_null addNewItem( not_null item, bool unread); not_null addNewToBack( 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 addCreatedOlderSlice( const std::vector> &items); 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 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 setLastServerMessage(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); bool clearUnreadOnClientSide() const; bool skipUnreadUpdate() const; HistoryItem *lastAvailableMessage() const; void getNextFirstUnreadMessage(); bool nonEmptyCountMoreThan(int count) 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(); HistoryService *insertJoinedMessage(); void insertLocalMessage(not_null item); 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; std::optional _lastServerMessage; base::flat_set> _localMessages; std::unordered_set> _messages; // 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; QString _chatListNameSortKey; bool _unreadMark = false; bool _fakeUnreadWhileOpened = false; bool _hasPinnedMessages = 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; Data::HistoryDrafts _drafts; TimeId _acceptCloudDraftsAfter = 0; int _savingCloudDraftRequests = 0; Data::ForwardDraft _forwardDraft; QString _topPromotedMessage; QString _topPromotedType; HistoryView::SendActionPainter _sendActionPainter; std::deque> _notifications; }; 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; };