/* 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; } // namespace Data namespace Dialogs { class Row; class IndexedList; } // namespace Dialogs namespace HistoryView { class Element; } // namespace HistoryView namespace AdminLog { class LocalIdManager; } // namespace AdminLog enum NewMessageType : char { NewMessageUnread, NewMessageLast, NewMessageExisting, }; class Histories { public: Histories(); void registerSendAction( not_null history, not_null user, const MTPSendMessageAction &action, TimeId when); void step_typings(TimeMs ms, bool timer); History *find(PeerId peerId) const; not_null findOrInsert(PeerId peerId); void clear(); void remove(const PeerId &peer); HistoryItem *addNewMessage(const MTPMessage &msg, NewMessageType type); // When typing in this history started. typedef QMap TypingHistories; TypingHistories typing; BasicAnimation _a_typings; int unreadBadge() const; bool unreadBadgeMuted() const; int unreadBadgeIgnoreOne(History *history) const; bool unreadBadgeMutedIgnoreOne(History *history) const; int unreadOnlyMutedBadge() const; void unreadIncrement(int count, bool muted); void unreadMuteChanged(int count, bool muted); void unreadEntriesChanged( int withUnreadDelta, int mutedWithUnreadDelta); struct SendActionAnimationUpdate { History *history; int width; int height; bool textUpdated; }; base::Observable &sendActionAnimationUpdated() { return _sendActionAnimationUpdated; } void selfDestructIn(not_null item, TimeMs delay); private: void checkSelfDestructItems(); int computeUnreadBadge( int full, int muted, int entriesFull, int entriesMuted) const; bool computeUnreadBadgeMuted( int full, int muted, int entriesFull, int entriesMuted) const; std::unordered_map> _map; int _unreadFull = 0; int _unreadMuted = 0; int _unreadEntriesFull = 0; int _unreadEntriesMuted = 0; base::Observable _sendActionAnimationUpdated; base::Timer _selfDestructTimer; std::vector _selfDestructItems; }; enum class UnreadMentionType { New, // when new message is added to history Existing, // when some messages slice was received }; class History : public Dialogs::Entry { public: using Element = HistoryView::Element; History(const PeerId &peerId); History(const History &) = delete; History &operator=(const History &) = delete; 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; bool hasOrphanMediaGroupPart() const; bool removeOrphanMediaGroupPart(); QVector collectMessagesFromUserToDelete( not_null user) const; void clear(); void markFullyLoaded(); void unloadBlocks(); 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); int countUnread(MsgId upTo); MsgId readInbox(); void inboxRead(MsgId upTo); 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 changeUnreadCount(int delta); void setUnreadMark(bool unread); bool unreadMark() const; int historiesUnreadCount() 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(const MTPDdialog &data); void applyDialogFields( int unreadCount, MsgId maxInboxRead, MsgId maxOutboxRead); 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, TimeMs ms); // Interface for Histories bool updateSendActionNeedsAnimating(TimeMs ms, 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 createLocalDraftFromCloud(); void setCloudDraft(std::unique_ptr &&draft); Data::Draft *createCloudDraft(const Data::Draft *fromDraft); bool skipCloudDraft(const QString &text, TimeId date) const; void setSentDraftText(const QString &text); void clearSentDraftText(const QString &text); void setEditDraft(std::unique_ptr &&draft); void clearLocalDraft(); void clearCloudDraft(); 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 override; void updateChatListExistence() override; bool shouldBeInChatList() const override; bool toImportant() const override { return !mute(); } int chatListUnreadCount() const override; bool chatListUnreadMark() const override; bool chatListMutedBadge() const override; HistoryItem *chatsListItem() const override; const QString &chatsListName() const override; const base::flat_set &chatsListNameWords() const override; const base::flat_set &chatsListFirstLetters() const override; void loadUserpic() override; void paintUserpic( Painter &p, int x, int y, int size) const override; 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(); virtual ~History(); // Still public data. std::deque> blocks; not_null peer; typedef QList NotifyQueue; NotifyQueue notifies; // 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 clearBlocks(bool leaveItems); 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 mainViewRemoved( not_null block, not_null view); void removeNotification(not_null item); TimeId adjustChatListTimeId() const override; void changedInChatListHook(Dialogs::Mode list, bool added) override; void changedChatListPinHook() override; void setInboxReadTill(MsgId upTo); void setOutboxReadTill(MsgId upTo); 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); // 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; bool skipUnreadUpdateForClientSideUnread() const; HistoryItem *lastAvailableMessage() const; void getNextFirstUnreadMessage(); // 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); 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 _inboxReadBefore; std::optional _outboxReadBefore; std::optional _unreadCount; std::optional _unreadMentionsCount; base::flat_set _unreadMentions; std::optional _lastMessage; 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; using TypingUsers = QMap; TypingUsers _typing; using SendActionUsers = QMap; SendActionUsers _sendActions; QString _sendActionString; Text _sendActionText; Ui::SendActionAnimation _sendActionAnimation; QMap _mySendActions; 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; };