tdesktop/Telegram/SourceFiles/history/history.h

709 lines
22 KiB
C++

/*
This file is part of Telegram Desktop,
the official desktop version of Telegram messaging app, see https://telegram.org
Telegram Desktop is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
It is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
In addition, as a special exception, the copyright holders give permission
to link the code of portions of this program with the OpenSSL library.
Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE
Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
*/
#pragma once
#include "structs.h"
#include "dialogs/dialogs_common.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"
void HistoryInit();
class HistoryItem;
using SelectedItemSet = QMap<int, not_null<HistoryItem*>>;
enum NewMessageType {
NewMessageUnread,
NewMessageLast,
NewMessageExisting,
};
class History;
class Histories {
public:
using Map = QHash<PeerId, History*>;
Map map;
Histories() : _a_typings(animation(this, &Histories::step_typings)) {
_selfDestructTimer.setCallback([this] { checkSelfDestructItems(); });
}
void regSendAction(History *history, UserData *user, const MTPSendMessageAction &action, TimeId when);
void step_typings(TimeMs ms, bool timer);
History *find(const PeerId &peerId);
not_null<History*> findOrInsert(const PeerId &peerId);
not_null<History*> findOrInsert(const PeerId &peerId, int32 unreadCount, int32 maxInboxRead, int32 maxOutboxRead);
void clear();
void remove(const PeerId &peer);
HistoryItem *addNewMessage(const MTPMessage &msg, NewMessageType type);
typedef QMap<History*, TimeMs> 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;
}
}
void setIsPinned(History *history, bool isPinned);
void clearPinned();
int pinnedCount() const;
QList<History*> getPinnedOrder() const;
void savePinnedToServer() const;
struct SendActionAnimationUpdate {
History *history;
int width;
int height;
bool textUpdated;
};
base::Observable<SendActionAnimationUpdate> &sendActionAnimationUpdated() {
return _sendActionAnimationUpdated;
}
void selfDestructIn(not_null<HistoryItem*> item, TimeMs delay);
private:
void checkSelfDestructItems();
int _unreadFull = 0;
int _unreadMuted = 0;
base::Observable<SendActionAnimationUpdate> _sendActionAnimationUpdated;
OrderedSet<History*> _pinnedDialogs;
base::Timer _selfDestructTimer;
std::vector<FullMsgId> _selfDestructItems;
};
class HistoryBlock;
enum HistoryMediaType {
MediaTypePhoto,
MediaTypeVideo,
MediaTypeContact,
MediaTypeCall,
MediaTypeFile,
MediaTypeGif,
MediaTypeSticker,
MediaTypeLocation,
MediaTypeWebPage,
MediaTypeMusicFile,
MediaTypeVoiceFile,
MediaTypeGame,
MediaTypeInvoice,
MediaTypeCount
};
enum MediaOverviewType {
OverviewPhotos = 0,
OverviewVideos = 1,
OverviewMusicFiles = 2,
OverviewFiles = 3,
OverviewVoiceFiles = 4,
OverviewLinks = 5,
OverviewChatPhotos = 6,
OverviewRoundVoiceFiles = 7,
OverviewGIFs = 8,
OverviewCount
};
struct TextWithTags {
struct Tag {
int offset, length;
QString id;
};
using Tags = QVector<Tag>;
QString text;
Tags tags;
};
inline bool operator==(const TextWithTags::Tag &a, const TextWithTags::Tag &b) {
return (a.offset == b.offset) && (a.length == b.length) && (a.id == b.id);
}
inline bool operator!=(const TextWithTags::Tag &a, const TextWithTags::Tag &b) {
return !(a == b);
}
inline bool operator==(const TextWithTags &a, const TextWithTags &b) {
return (a.text == b.text) && (a.tags == b.tags);
}
inline bool operator!=(const TextWithTags &a, const TextWithTags &b) {
return !(a == b);
}
namespace Data {
struct Draft;
} // namespace Data
class HistoryMedia;
class HistoryMessage;
enum AddToOverviewMethod {
AddToOverviewNew, // when new message is added to history
AddToOverviewFront, // when old messages slice was received
AddToOverviewBack, // when new messages slice was received and it is the last one, we index all media
};
namespace Dialogs {
class Row;
class IndexedList;
} // namespace Dialogs
class ChannelHistory;
class History {
public:
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<History*> migrateToOrMe() const;
History *migrateFrom() const;
bool isEmpty() const {
return blocks.isEmpty();
}
bool isDisplayedEmpty() const;
void clear(bool leaveItems = false);
virtual ~History();
HistoryItem *addNewService(MsgId msgId, QDateTime date, const QString &text, MTPDmessage::Flags flags = 0, bool newMsg = true);
HistoryItem *addNewMessage(const MTPMessage &msg, NewMessageType type);
HistoryItem *addToHistory(const MTPMessage &msg);
HistoryItem *addNewForwarded(MsgId id, MTPDmessage::Flags flags, QDateTime date, UserId from, const QString &postAuthor, HistoryMessage *item);
HistoryItem *addNewDocument(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, DocumentData *doc, const QString &caption, const MTPReplyMarkup &markup);
HistoryItem *addNewPhoto(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, PhotoData *photo, const QString &caption, const MTPReplyMarkup &markup);
HistoryItem *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 &msg, bool applyServiceAction, bool detachExistingItem);
void addOlderSlice(const QVector<MTPMessage> &slice);
void addNewerSlice(const QVector<MTPMessage> &slice);
bool addToOverview(MediaOverviewType type, MsgId msgId, AddToOverviewMethod method);
void eraseFromOverview(MediaOverviewType type, MsgId msgId);
void newItemAdded(HistoryItem *item);
int countUnread(MsgId upTo);
void updateShowFrom();
MsgId inboxRead(MsgId upTo);
MsgId inboxRead(HistoryItem *wasRead);
MsgId outboxRead(MsgId upTo);
MsgId outboxRead(HistoryItem *wasRead);
HistoryItem *lastAvailableMessage() const;
int unreadCount() const {
return _unreadCount;
}
void setUnreadCount(int newUnreadCount);
bool mute() const {
return _mute;
}
void setMute(bool newMute);
void getNextShowFrom(HistoryBlock *block, int i);
void addUnreadBar();
void destroyUnreadBar();
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);
bool needUpdateInChatList() const;
void updateChatListSortPosition();
void setChatsListDate(const QDateTime &date);
uint64 sortKeyInChatList() const {
return _sortKeyInChatList;
}
struct PositionInChatListChange {
int movedFrom;
int movedTo;
};
PositionInChatListChange adjustByPosInChatList(Dialogs::Mode list, Dialogs::IndexedList *indexed);
bool inChatList(Dialogs::Mode list) const {
return !chatListLinks(list).isEmpty();
}
int posInChatList(Dialogs::Mode list) const;
Dialogs::Row *addToChatList(Dialogs::Mode list, Dialogs::IndexedList *indexed);
void removeFromChatList(Dialogs::Mode list, Dialogs::IndexedList *indexed);
void removeChatListEntryByLetter(Dialogs::Mode list, QChar letter);
void addChatListEntryByLetter(Dialogs::Mode list, QChar letter, Dialogs::Row *row);
void updateChatListEntry() const;
bool isPinnedDialog() const {
return (_pinnedIndex > 0);
}
void setPinnedDialog(bool isPinned);
void setPinnedIndex(int newPinnedIndex);
int getPinnedIndex() const {
return _pinnedIndex;
}
MsgId minMsgId() const;
MsgId maxMsgId() const;
MsgId msgIdForRead() const;
int resizeGetHeight(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 setPendingResize() {
_flags |= Flag::f_pending_resize;
setHasPendingResizedItems();
}
void paintDialog(Painter &p, int32 w, bool sel) const;
bool updateSendActionNeedsAnimating(TimeMs ms, bool force = false);
void unregSendAction(UserData *from);
bool updateSendActionNeedsAnimating(UserData *user, const MTPSendMessageAction &action);
bool mySendActionUpdated(SendAction::Type type, bool doing);
bool paintSendAction(Painter &p, int x, int y, int availableWidth, int outerWidth, style::color color, TimeMs ms);
void clearLastKeyboard();
// optimization for userpics displayed on the left
// if this returns false there is no need to even try to handle them
bool canHaveFromPhotos() const;
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, AddToOverviewMethod method);
void eraseFromUnreadMentions(MsgId msgId);
void addUnreadMentionsSlice(const MTPmessages_Messages &result);
using Blocks = QList<HistoryBlock*>;
Blocks blocks;
int width = 0;
int height = 0;
int32 msgCount = 0;
MsgId inboxReadBefore = 1;
MsgId outboxReadBefore = 1;
HistoryItem *showFrom = nullptr;
HistoryItem *unreadBar = nullptr;
PeerData *peer;
bool oldLoaded = false;
bool newLoaded = true;
HistoryItem *lastMsg = nullptr;
HistoryItem *lastSentMsg = nullptr;
QDateTime lastMsgDate;
typedef QList<HistoryItem*> NotifyQueue;
NotifyQueue notifies;
Data::Draft *localDraft() {
return _localDraft.get();
}
Data::Draft *cloudDraft() {
return _cloudDraft.get();
}
Data::Draft *editDraft() {
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(Data::Draft *fromDraft);
void setEditDraft(std::unique_ptr<Data::Draft> &&draft);
void clearLocalDraft();
void clearCloudDraft();
void clearEditDraft();
void draftSavedToCloud();
Data::Draft *draft() {
return _editDraft ? editDraft() : localDraft();
}
QVector<FullMsgId> forwardDraft() const {
return _forwardDraft;
}
SelectedItemSet validateForwardDraft();
void setForwardDraft(const SelectedItemSet &items);
// some fields below are a property of a currently displayed instance of this
// conversation history not a property of the conversation history itself
public:
// 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
HistoryItem *scrollTopItem = nullptr;
int scrollTopOffset = 0;
void forgetScrollState() {
scrollTopItem = nullptr;
}
// find the correct scrollTopItem and scrollTopOffset using given top
// of the displayed window relative to the history start coord
void countScrollState(int top);
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);
public:
bool lastKeyboardInited = false;
bool lastKeyboardUsed = false;
MsgId lastKeyboardId = 0;
MsgId lastKeyboardHiddenId = 0;
PeerId lastKeyboardFrom = 0;
mtpRequestId sendRequestId = 0;
mutable const HistoryItem *textCachedFor = nullptr; // cache
mutable Text lastItemTextCache;
bool overviewCountLoaded(int32 overviewIndex) const {
return _overviewCountData[overviewIndex] >= 0;
}
bool overviewLoaded(int32 overviewIndex) const {
return overviewCount(overviewIndex) == _overview[overviewIndex].size();
}
int overviewCount(int32 overviewIndex, int32 defaultValue = -1) const {
auto result = _overviewCountData[overviewIndex];
auto loaded = _overview[overviewIndex].size();
if (result < 0) return defaultValue;
if (result < loaded) {
if (result > 0) {
const_cast<History*>(this)->_overviewCountData[overviewIndex] = 0;
}
return loaded;
}
return result;
}
const OrderedSet<MsgId> &overview(int32 overviewIndex) const {
return _overview[overviewIndex];
}
MsgId overviewMinId(int32 overviewIndex) const {
return _overview[overviewIndex].empty() ? 0 : *_overview[overviewIndex].begin();
}
void overviewSliceDone(int32 overviewIndex, const MTPmessages_Messages &result, bool onlyCounts = false);
bool overviewHasMsgId(int32 overviewIndex, MsgId msgId) const {
return _overview[overviewIndex].contains(msgId);
}
void changeMsgId(MsgId oldId, MsgId newId);
Text cloudDraftTextCache;
protected:
void clearOnDestroy();
HistoryItem *addNewToLastBlock(const MTPMessage &msg, NewMessageType type);
friend class HistoryBlock;
// 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(HistoryBlock *block);
void clearBlocks(bool leaveItems);
HistoryItem *createItemForwarded(MsgId id, MTPDmessage::Flags flags, QDateTime date, UserId from, const QString &postAuthor, HistoryMessage *msg);
HistoryItem *createItemDocument(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, DocumentData *doc, const QString &caption, const MTPReplyMarkup &markup);
HistoryItem *createItemPhoto(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, PhotoData *photo, const QString &caption, const MTPReplyMarkup &markup);
HistoryItem *createItemGame(MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, QDateTime date, UserId from, const QString &postAuthor, GameData *game, const MTPReplyMarkup &markup);
HistoryItem *addNewItem(HistoryItem *adding, bool newMsg);
HistoryItem *addNewInTheMiddle(HistoryItem *newItem, int32 blockIndex, int32 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(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);
HistoryBlock *finishBuildingFrontBlock(); // Returns the built block or nullptr if nothing was added.
bool isBuildingFrontBlock() const {
return _buildingFrontBlock != nullptr;
}
private:
// After adding a new history slice check the lastMsg and newLoaded.
void checkLastMsg();
// Add all items to the media overview if we were not loaded at bottom and now are.
void checkAddAllToOverview();
enum class Flag {
f_has_pending_resized_items = (1 << 0),
f_pending_resize = (1 << 1),
};
using Flags = QFlags<Flag>;
Q_DECL_CONSTEXPR friend inline QFlags<Flags::enum_type> operator|(Flags::enum_type f1, Flags::enum_type f2) noexcept {
return QFlags<Flags::enum_type>(f1) | f2;
}
Q_DECL_CONSTEXPR friend inline QFlags<Flags::enum_type> operator|(Flags::enum_type f1, QFlags<Flags::enum_type> f2) noexcept {
return f2 | f1;
}
Q_DECL_CONSTEXPR friend inline QFlags<Flags::enum_type> operator~(Flags::enum_type f) noexcept {
return ~QFlags<Flags::enum_type>(f);
}
Flags _flags = { 0 };
bool _mute = false;
int _unreadCount = 0;
base::optional<int> _unreadMentionsCount;
base::flat_set<MsgId> _unreadMentions;
Dialogs::RowsByLetter _chatListLinks[2];
Dialogs::RowsByLetter &chatListLinks(Dialogs::Mode list) {
return _chatListLinks[static_cast<int>(list)];
}
const Dialogs::RowsByLetter &chatListLinks(Dialogs::Mode list) const {
return _chatListLinks[static_cast<int>(list)];
}
Dialogs::Row *mainChatListLink(Dialogs::Mode list) const {
auto it = chatListLinks(list).constFind(0);
Assert(it != chatListLinks(list).cend());
return it.value();
}
uint64 _sortKeyInChatList = 0; // like ((unixtime) << 32) | (incremented counter)
OrderedSet<MsgId> _overview[OverviewCount];
int32 _overviewCountData[OverviewCount]; // -1 - not loaded, 0 - all loaded, > 0 - count, but not all loaded
// 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;
// Creates if necessary a new block for adding item.
// Depending on isBuildingFrontBlock() gets front or back block.
HistoryBlock *prepareBlockForAddingItem();
std::unique_ptr<Data::Draft> _localDraft, _cloudDraft;
std::unique_ptr<Data::Draft> _editDraft;
QVector<FullMsgId> _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;
int _pinnedIndex = 0; // > 0 for pinned dialogs
};
class HistoryJoined;
class ChannelHistory : public History {
public:
using History::History;
void messageDetached(HistoryItem *msg);
void getRangeDifference();
void getRangeDifferenceNext(int32 pts);
HistoryJoined *insertJoinedMessage(bool unread);
void checkJoinedMessage(bool createUnread = false);
const QDateTime &maxReadMessageDate();
~ChannelHistory();
private:
friend class History;
HistoryItem* addNewChannelMessage(const MTPMessage &msg, NewMessageType type);
HistoryItem *addNewToBlocks(const MTPMessage &msg, NewMessageType type);
void checkMaxReadMessageDate();
void cleared(bool leaveItems);
QDateTime _maxReadMessageDate;
HistoryJoined *_joinedMessage = nullptr;
MsgId _rangeDifferenceFromId, _rangeDifferenceToId;
int32 _rangeDifferencePts;
mtpRequestId _rangeDifferenceRequestId;
};
class HistoryBlock {
public:
HistoryBlock(not_null<History*> history) : _history(history) {
}
HistoryBlock(const HistoryBlock &) = delete;
HistoryBlock &operator=(const HistoryBlock &) = delete;
QVector<HistoryItem*> items;
void clear(bool leaveItems = false);
~HistoryBlock() {
clear();
}
void removeItem(HistoryItem *item);
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.at(_indexInHistory - 1) : nullptr;
}
HistoryBlock *nextBlock() const {
Expects(_indexInHistory >= 0);
return (_indexInHistory + 1 < _history->blocks.size()) ? _history->blocks.at(_indexInHistory + 1) : nullptr;
}
void setIndexInHistory(int index) {
_indexInHistory = index;
}
int indexInHistory() const {
Expects(_indexInHistory >= 0);
Expects(_indexInHistory < _history->blocks.size());
Expects(_history->blocks[_indexInHistory] == this);
return _indexInHistory;
}
protected:
const not_null<History*> _history;
int _y = 0;
int _height = 0;
int _indexInHistory = -1;
};