/* 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; namespace HistoryView { class Element; } // namespace HistoryView namespace AdminLog { class LocalIdManager; } // namespace AdminLog enum NewMessageType : char { NewMessageUnread, NewMessageLast, NewMessageExisting, }; class Histories { public: using Map = QHash; Map map; Histories() : _a_typings(animation(this, &Histories::step_typings)) { _selfDestructTimer.setCallback([this] { checkSelfDestructItems(); }); } void registerSendAction( not_null history, not_null user, const MTPSendMessageAction &action, TimeId when); void step_typings(TimeMs ms, bool timer); History *find(const PeerId &peerId); not_null findOrInsert(const PeerId &peerId); not_null findOrInsert( const PeerId &peerId, int unreadCount, MsgId maxInboxRead, MsgId maxOutboxRead); void clear(); void remove(const PeerId &peer); HistoryItem *addNewMessage(const MTPMessage &msg, NewMessageType type); typedef QMap TypingHistories; // when typing in this history started TypingHistories typing; BasicAnimation _a_typings; int unreadBadge() const; int unreadMutedCount() const { return _unreadMuted; } bool unreadOnlyMuted() const; void unreadIncrement(int32 count, bool muted) { _unreadFull += count; if (muted) { _unreadMuted += count; } } void unreadMuteChanged(int32 count, bool muted) { if (muted) { _unreadMuted += count; } else { _unreadMuted -= count; } } 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 _unreadFull = 0; int _unreadMuted = 0; base::Observable _sendActionAnimationUpdated; base::Timer _selfDestructTimer; std::vector _selfDestructItems; }; class HistoryBlock; namespace Data { struct Draft; } // namespace Data class HistoryMedia; class HistoryMessage; enum class UnreadMentionType { New, // when new message is added to history Existing, // when some messages slice was received }; namespace Dialogs { class Row; class IndexedList; } // namespace Dialogs class ChannelHistory; 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 { return peerToChannel(peer->id); } bool isChannel() const { return peerIsChannel(peer->id); } bool isMegagroup() const { return peer->isMegagroup(); } ChannelHistory *asChannelHistory(); const ChannelHistory *asChannelHistory() const; not_null migrateToOrMe() const; History *migrateFrom() const; bool isEmpty() const { return blocks.empty(); } bool isDisplayedEmpty() const; bool hasOrphanMediaGroupPart() const; bool removeOrphanMediaGroupPart(); void clear(bool leaveItems = false); 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, QDateTime date, const QString &text, MTPDmessage::Flags flags = 0, bool newMsg = true); not_null addNewForwarded(MsgId id, MTPDmessage::Flags flags, QDateTime date, UserId from, const QString &postAuthor, HistoryMessage *item); not_null addNewDocument(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, DocumentData *doc, const TextWithEntities &caption, const MTPReplyMarkup &markup); not_null addNewPhoto(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, PhotoData *photo, const TextWithEntities &caption, const MTPReplyMarkup &markup); not_null addNewGame(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, GameData *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 inboxRead(MsgId upTo); MsgId inboxRead(HistoryItem *wasRead); MsgId outboxRead(MsgId upTo); MsgId outboxRead(HistoryItem *wasRead); int unreadCount() const { return _unreadCount; } void setUnreadCount(int newUnreadCount); bool mute() const { return _mute; } 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); void setLastMessage(HistoryItem *msg); void fixLastMessage(bool wasAtBottom); MsgId minMsgId() const; MsgId maxMsgId() const; MsgId msgIdForRead() const; void resizeToWidth(int newWidth); void removeNotification(HistoryItem *item) { if (!notifies.isEmpty()) { for (auto i = notifies.begin(), e = notifies.end(); i != e; ++i) { if ((*i) == item) { notifies.erase(i); break; } } } } HistoryItem *currentNotification() { return notifies.isEmpty() ? 0 : notifies.front(); } bool hasNotification() const { return !notifies.isEmpty(); } void skipNotification() { if (!notifies.isEmpty()) { notifies.pop_front(); } } void popNotification(HistoryItem *item) { if (!notifies.isEmpty() && notifies.back() == item) notifies.pop_back(); } bool hasPendingResizedItems() const { return _flags & Flag::f_has_pending_resized_items; } void setHasPendingResizedItems(); void paintDialog(Painter &p, int32 w, bool sel) const; 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(Data::Draft *fromDraft); 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); bool shouldBeInChatList() const override; bool toImportant() const override { return !mute(); } int chatListUnreadCount() 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; int height() const; int32 msgCount = 0; MsgId inboxReadBefore = 1; MsgId outboxReadBefore = 1; not_null peer; bool oldLoaded = false; bool newLoaded = true; HistoryItem *lastMsg = nullptr; HistoryItem *lastSentMsg = nullptr; 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; protected: // 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); void clearOnDestroy(); 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 createItemForwarded(MsgId id, MTPDmessage::Flags flags, QDateTime date, UserId from, const QString &postAuthor, HistoryMessage *msg); not_null createItemDocument(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, DocumentData *doc, const TextWithEntities &caption, const MTPReplyMarkup &markup); not_null createItemPhoto(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, PhotoData *photo, const TextWithEntities &caption, const MTPReplyMarkup &markup); not_null createItemGame(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, GameData *game, const MTPReplyMarkup &markup); 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); HistoryBlock *finishBuildingFrontBlock(); // Returns the built block or nullptr if nothing was added. bool isBuildingFrontBlock() const { return _buildingFrontBlock != nullptr; } 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; }; void mainViewRemoved( not_null block, not_null view); QDateTime adjustChatListDate() const override; void changedInChatListHook(Dialogs::Mode list, bool added) override; void changedChatListPinHook() override; void applyMessageChanges( not_null item, const MTPMessage &original); void applyServiceChanges( not_null item, const MTPDmessageService &data); // After adding a new history slice check the lastMsg and newLoaded. void checkLastMsg(); // Add all items to the unread mentions if we were not loaded at bottom and now are. void checkAddAllToUnreadMentions(); template void addToSharedMedia(std::vector (&medias)[kSharedMediaTypeCount], bool force); void addToSharedMedia(const std::vector> &items); void addEdgesToSharedMedia(); void addItemsToLists(const std::vector> &items); void clearSendAction(not_null from); 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 _unreadCount = 0; int _width = 0; int _height = 0; Element *_unreadBarView = nullptr; Element *_firstUnreadView = nullptr; base::optional _unreadMentionsCount; base::flat_set _unreadMentions; // 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; MessageIdsList _forwardDraft; using TypingUsers = QMap; TypingUsers _typing; using SendActionUsers = QMap; SendActionUsers _sendActions; QString _sendActionString; Text _sendActionText; Ui::SendActionAnimation _sendActionAnimation; QMap _mySendActions; int _pinnedIndex = 0; // > 0 for pinned dialogs std::weak_ptr _adminLogIdManager; }; class HistoryJoined; class ChannelHistory : public History { public: using History::History; void messageDetached(not_null message); void getRangeDifference(); void getRangeDifferenceNext(int32 pts); HistoryJoined *insertJoinedMessage(bool unread); void checkJoinedMessage(bool createUnread = false); const QDateTime &maxReadMessageDate(); ~ChannelHistory(); private: friend class History; void checkMaxReadMessageDate(); void cleared(bool leaveItems); QDateTime _maxReadMessageDate; HistoryJoined *_joinedMessage = nullptr; MsgId _rangeDifferenceFromId, _rangeDifferenceToId; int32 _rangeDifferencePts; mtpRequestId _rangeDifferenceRequestId; }; 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 clear(bool leaveItems = false); 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; };