565 lines
16 KiB
C++
565 lines
16 KiB
C++
/*
|
|
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;
|
|
} // 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,
|
|
};
|
|
|
|
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<Data::Session*> owner, PeerId peerId);
|
|
History(const History &) = delete;
|
|
History &operator=(const History &) = delete;
|
|
|
|
Data::Session &owner() const;
|
|
AuthSession &session() const;
|
|
|
|
ChannelId channelId() const;
|
|
bool isChannel() const;
|
|
bool isMegagroup() const;
|
|
not_null<History*> 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<MsgId> collectMessagesFromUserToDelete(
|
|
not_null<UserData*> user) const;
|
|
|
|
void clear();
|
|
void markFullyLoaded();
|
|
void unloadBlocks();
|
|
void clearUpTill(MsgId availableMinId);
|
|
|
|
void applyGroupAdminChanges(
|
|
const base::flat_map<UserId, bool> &changes);
|
|
|
|
HistoryItem *addNewMessage(const MTPMessage &msg, NewMessageType type);
|
|
HistoryItem *addToHistory(const MTPMessage &msg);
|
|
not_null<HistoryItem*> addNewService(
|
|
MsgId msgId,
|
|
TimeId date,
|
|
const QString &text,
|
|
MTPDmessage::Flags flags = 0,
|
|
bool newMsg = true);
|
|
not_null<HistoryItem*> addNewForwarded(
|
|
MsgId id,
|
|
MTPDmessage::Flags flags,
|
|
TimeId date,
|
|
UserId from,
|
|
const QString &postAuthor,
|
|
not_null<HistoryMessage*> original);
|
|
not_null<HistoryItem*> addNewDocument(
|
|
MsgId id,
|
|
MTPDmessage::Flags flags,
|
|
UserId viaBotId,
|
|
MsgId replyTo,
|
|
TimeId date,
|
|
UserId from,
|
|
const QString &postAuthor,
|
|
not_null<DocumentData*> document,
|
|
const TextWithEntities &caption,
|
|
const MTPReplyMarkup &markup);
|
|
not_null<HistoryItem*> addNewPhoto(
|
|
MsgId id,
|
|
MTPDmessage::Flags flags,
|
|
UserId viaBotId,
|
|
MsgId replyTo,
|
|
TimeId date,
|
|
UserId from,
|
|
const QString &postAuthor,
|
|
not_null<PhotoData*> photo,
|
|
const TextWithEntities &caption,
|
|
const MTPReplyMarkup &markup);
|
|
not_null<HistoryItem*> addNewGame(
|
|
MsgId id,
|
|
MTPDmessage::Flags flags,
|
|
UserId viaBotId,
|
|
MsgId replyTo,
|
|
TimeId date,
|
|
UserId from,
|
|
const QString &postAuthor,
|
|
not_null<GameData*> game,
|
|
const MTPReplyMarkup &markup);
|
|
|
|
// Used only internally and for channel admin log.
|
|
HistoryItem *createItem(
|
|
const MTPMessage &message,
|
|
bool detachExistingItem);
|
|
std::vector<not_null<HistoryItem*>> createItems(
|
|
const QVector<MTPMessage> &data);
|
|
|
|
void addOlderSlice(const QVector<MTPMessage> &slice);
|
|
void addNewerSlice(const QVector<MTPMessage> &slice);
|
|
|
|
void newItemAdded(not_null<HistoryItem*> item);
|
|
|
|
int countUnread(MsgId upTo);
|
|
MsgId readInbox();
|
|
void inboxRead(MsgId upTo);
|
|
void inboxRead(not_null<const HistoryItem*> wasRead);
|
|
void outboxRead(MsgId upTo);
|
|
void outboxRead(not_null<const HistoryItem*> wasRead);
|
|
bool isServerSideUnread(not_null<const HistoryItem*> 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);
|
|
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<HistoryItem*> item);
|
|
void itemVanished(not_null<HistoryItem*> 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<UserData*> 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<Data::Draft> &&draft);
|
|
void takeLocalDraft(History *from);
|
|
void createLocalDraftFromCloud();
|
|
void setCloudDraft(std::unique_ptr<Data::Draft> &&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<Data::Draft> &&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;
|
|
int chatListUnreadCount() const override;
|
|
bool chatListUnreadMark() const override;
|
|
bool chatListMutedBadge() const override;
|
|
HistoryItem *chatListMessage() const override;
|
|
bool chatListMessageKnown() const override;
|
|
void requestChatListMessage() override;
|
|
const QString &chatListName() const override;
|
|
const base::flat_set<QString> &chatListNameWords() const override;
|
|
const base::flat_set<QChar> &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<HistoryItem*> 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<AdminLog::LocalIdManager> adminLogIdManager();
|
|
|
|
virtual ~History();
|
|
|
|
// Still public data.
|
|
std::deque<std::unique_ptr<HistoryBlock>> blocks;
|
|
|
|
not_null<PeerData*> peer;
|
|
|
|
typedef QList<HistoryItem*> 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<Flag>;
|
|
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<HistoryBlock*> block);
|
|
|
|
void clearBlocks(bool leaveItems);
|
|
|
|
not_null<HistoryItem*> addNewItem(
|
|
not_null<HistoryItem*> item,
|
|
bool unread);
|
|
not_null<HistoryItem*> addNewInTheMiddle(
|
|
not_null<HistoryItem*> 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<HistoryItem*> 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<HistoryItem*> added);
|
|
void mainViewRemoved(
|
|
not_null<HistoryBlock*> block,
|
|
not_null<Element*> view);
|
|
void removeNotification(not_null<HistoryItem*> 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<HistoryItem*> item,
|
|
const MTPMessage &original);
|
|
void applyServiceChanges(
|
|
not_null<HistoryItem*> 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<HistoryItem*> computeChatListMessageFromLast() const;
|
|
void setChatListMessageFromLast();
|
|
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<not_null<HistoryItem*>> &items);
|
|
void addEdgesToSharedMedia();
|
|
|
|
void addItemsToLists(const std::vector<not_null<HistoryItem*>> &items);
|
|
void clearSendAction(not_null<UserData*> from);
|
|
bool clearUnreadOnClientSide() const;
|
|
bool skipUnreadUpdate() const;
|
|
bool skipUnreadUpdateForClientSideUnread() 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<const Element*> was, Element *now);
|
|
|
|
not_null<Data::Session*> _owner;
|
|
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<MsgId> _inboxReadBefore;
|
|
std::optional<MsgId> _outboxReadBefore;
|
|
std::optional<int> _unreadCount;
|
|
std::optional<int> _unreadMentionsCount;
|
|
base::flat_set<MsgId> _unreadMentions;
|
|
std::optional<HistoryItem*> _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<HistoryItem*> _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<BuildingBlock> _buildingFrontBlock;
|
|
|
|
std::unique_ptr<Data::Draft> _localDraft, _cloudDraft;
|
|
std::unique_ptr<Data::Draft> _editDraft;
|
|
std::optional<QString> _lastSentDraftText;
|
|
TimeId _lastSentDraftTime = 0;
|
|
MessageIdsList _forwardDraft;
|
|
|
|
using TypingUsers = QMap<UserData*, TimeMs>;
|
|
TypingUsers _typing;
|
|
using SendActionUsers = QMap<UserData*, SendAction>;
|
|
SendActionUsers _sendActions;
|
|
QString _sendActionString;
|
|
Text _sendActionText;
|
|
Ui::SendActionAnimation _sendActionAnimation;
|
|
QMap<SendAction::Type, TimeMs> _mySendActions;
|
|
|
|
std::weak_ptr<AdminLog::LocalIdManager> _adminLogIdManager;
|
|
|
|
};
|
|
|
|
class HistoryBlock {
|
|
public:
|
|
using Element = HistoryView::Element;
|
|
|
|
HistoryBlock(not_null<History*> history);
|
|
HistoryBlock(const HistoryBlock &) = delete;
|
|
HistoryBlock &operator=(const HistoryBlock &) = delete;
|
|
~HistoryBlock();
|
|
|
|
std::vector<std::unique_ptr<Element>> messages;
|
|
|
|
void remove(not_null<Element*> view);
|
|
void refreshView(not_null<Element*> 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*> 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*> _history;
|
|
|
|
int _y = 0;
|
|
int _height = 0;
|
|
int _indexInHistory = -1;
|
|
|
|
};
|