/* 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-2016 John Preston, https://desktop.telegram.org */ #pragma once void historyInit(); class HistoryItem; typedef QMap SelectedItemSet; #include "structs.h" enum NewMessageType { NewMessageUnread, NewMessageLast, NewMessageExisting, }; class History; class Histories { public: typedef QHash Map; Map map; Histories() : _a_typings(animation(this, &Histories::step_typings)), _unreadFull(0), _unreadMuted(0) { } void regSendAction(History *history, UserData *user, const MTPSendMessageAction &action); void step_typings(uint64 ms, bool timer); History *find(const PeerId &peerId); History *findOrInsert(const PeerId &peerId, int32 unreadCount, int32 maxInboxRead); void clear(); void remove(const PeerId &peer); ~Histories() { _unreadFull = _unreadMuted = 0; } HistoryItem *addNewMessage(const MTPMessage &msg, NewMessageType type); typedef QMap TypingHistories; // when typing in this history started TypingHistories typing; Animation _a_typings; int32 unreadBadge() const { return _unreadFull - (cIncludeMuted() ? 0 : _unreadMuted); } bool unreadOnlyMuted() const { return cIncludeMuted() ? (_unreadMuted >= _unreadFull) : false; } 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; } } private: int32 _unreadFull, _unreadMuted; }; class HistoryBlock; struct DialogRow { DialogRow(History *history = 0, DialogRow *prev = 0, DialogRow *next = 0, int32 pos = 0) : prev(prev), next(next), history(history), pos(pos), attached(0) { } void paint(Painter &p, int32 w, bool act, bool sel, bool onlyBackground) const; DialogRow *prev, *next; History *history; int32 pos; void *attached; // for any attached data, for example View in contacts list }; struct FakeDialogRow { FakeDialogRow(HistoryItem *item) : _item(item), _cacheFor(0), _cache(st::dlgRichMinWidth) { } void paint(Painter &p, int32 w, bool act, bool sel, bool onlyBackground) const; HistoryItem *_item; mutable const HistoryItem *_cacheFor; mutable Text _cache; }; enum HistoryMediaType { MediaTypePhoto, MediaTypeVideo, MediaTypeContact, MediaTypeFile, MediaTypeGif, MediaTypeSticker, MediaTypeLocation, MediaTypeWebPage, MediaTypeMusicFile, MediaTypeVoiceFile, MediaTypeCount }; enum MediaOverviewType { OverviewPhotos = 0, OverviewVideos = 1, OverviewMusicFiles = 2, OverviewFiles = 3, OverviewVoiceFiles = 4, OverviewLinks = 5, OverviewCount }; inline MTPMessagesFilter typeToMediaFilter(MediaOverviewType &type) { switch (type) { case OverviewPhotos: return MTP_inputMessagesFilterPhotos(); case OverviewVideos: return MTP_inputMessagesFilterVideo(); case OverviewMusicFiles: return MTP_inputMessagesFilterMusic(); case OverviewFiles: return MTP_inputMessagesFilterDocument(); case OverviewVoiceFiles: return MTP_inputMessagesFilterVoice(); case OverviewLinks: return MTP_inputMessagesFilterUrl(); default: type = OverviewCount; break; } return MTPMessagesFilter(); } enum SendActionType { SendActionTyping, SendActionRecordVideo, SendActionUploadVideo, SendActionRecordVoice, SendActionUploadVoice, SendActionUploadPhoto, SendActionUploadFile, SendActionChooseLocation, SendActionChooseContact, }; struct SendAction { SendAction(SendActionType type, uint64 until, int32 progress = 0) : type(type), until(until), progress(progress) { } SendActionType type; uint64 until; int32 progress; }; struct HistoryDraft { HistoryDraft() : msgId(0), previewCancelled(false) { } HistoryDraft(const QString &text, MsgId msgId, const MessageCursor &cursor, bool previewCancelled) : text(text) , msgId(msgId) , cursor(cursor) , previewCancelled(previewCancelled) { } HistoryDraft(const FlatTextarea &field, MsgId msgId, bool previewCancelled) : text(field.getLastText()) , msgId(msgId) , cursor(field) , previewCancelled(previewCancelled) { } QString text; MsgId msgId; // replyToId for message draft, editMsgId for edit draft MessageCursor cursor; bool previewCancelled; }; struct HistoryEditDraft : public HistoryDraft { HistoryEditDraft() : HistoryDraft() , saveRequest(0) { } HistoryEditDraft(const QString &text, MsgId msgId, const MessageCursor &cursor, bool previewCancelled, mtpRequestId saveRequest = 0) : HistoryDraft(text, msgId, cursor, previewCancelled) , saveRequest(saveRequest) { } HistoryEditDraft(const FlatTextarea &field, MsgId msgId, bool previewCancelled, mtpRequestId saveRequest = 0) : HistoryDraft(field, msgId, previewCancelled) , saveRequest(saveRequest) { } mtpRequestId saveRequest; }; 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 }; struct DialogsIndexed; 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; bool isEmpty() const { return blocks.isEmpty(); } 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, int32 from, HistoryMessage *item); HistoryItem *addNewDocument(MsgId id, MTPDmessage::Flags flags, int32 viaBotId, MsgId replyTo, QDateTime date, int32 from, DocumentData *doc, const QString &caption); HistoryItem *addNewPhoto(MsgId id, MTPDmessage::Flags flags, int32 viaBotId, MsgId replyTo, QDateTime date, int32 from, PhotoData *photo, const QString &caption); void addOlderSlice(const QVector &slice, const QVector *collapsed); void addNewerSlice(const QVector &slice, const QVector *collapsed); bool addToOverview(MediaOverviewType type, MsgId msgId, AddToOverviewMethod method); void eraseFromOverview(MediaOverviewType type, MsgId msgId); void newItemAdded(HistoryItem *item); void unregTyping(UserData *from); int countUnread(MsgId upTo); void updateShowFrom(); MsgId inboxRead(MsgId upTo); MsgId inboxRead(HistoryItem *wasRead); MsgId outboxRead(MsgId upTo); MsgId outboxRead(HistoryItem *wasRead); HistoryItem *lastImportantMessage() const; void setUnreadCount(int newUnreadCount, bool psUpdate = true); 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, MsgId &fixInScrollMsgId, int32 &fixInScrollMsgTop); // has messages for showing history at msgId void getReadyFor(MsgId msgId, MsgId &fixInScrollMsgId, int32 &fixInScrollMsgTop); void setLastMessage(HistoryItem *msg); void fixLastMessage(bool wasAtBottom); typedef QMap ChatListLinksMap; void setChatsListDate(const QDateTime &date); QPair adjustByPosInChatsList(DialogsIndexed &indexed); uint64 sortKeyInChatList() const { return _sortKeyInChatList; } bool inChatList() const { return !_chatListLinks.isEmpty(); } int32 posInChatList() const { return mainChatListLink()->pos; } DialogRow *addToChatList(DialogsIndexed &indexed); void removeFromChatList(DialogsIndexed &indexed); void removeChatListEntryByLetter(QChar letter); void addChatListEntryByLetter(QChar letter, DialogRow *row); void updateChatListEntry() const; 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 updateTyping(uint64 ms, bool force = false); 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; typedef QList Blocks; Blocks blocks; int32 width, height, msgCount, unreadCount; int32 inboxReadBefore, outboxReadBefore; HistoryItem *showFrom; HistoryItem *unreadBar; PeerData *peer; bool oldLoaded, newLoaded; HistoryItem *lastMsg; QDateTime lastMsgDate; typedef QList NotifyQueue; NotifyQueue notifies; HistoryDraft *msgDraft; HistoryEditDraft *editDraft; HistoryDraft *draft() { return editDraft ? editDraft : msgDraft; } void setMsgDraft(HistoryDraft *draft) { if (msgDraft) delete msgDraft; msgDraft = draft; } void setEditDraft(HistoryEditDraft *draft) { if (editDraft) delete editDraft; editDraft = draft; } // 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; // 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; int scrollTopOffset; 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 mute; bool lastKeyboardInited, lastKeyboardUsed; MsgId lastKeyboardId, lastKeyboardHiddenId; PeerId lastKeyboardFrom; mtpRequestId sendRequestId; mutable const HistoryItem *textCachedFor; // cache mutable Text lastItemTextCache; typedef QMap TypingUsers; TypingUsers typing; typedef QMap SendActionUsers; SendActionUsers sendActions; QString typingStr; Text typingText; uint32 typingDots; QMap mySendActions; typedef QList MediaOverview; MediaOverview overview[OverviewCount]; bool overviewCountLoaded(int32 overviewIndex) const { return overviewCountData[overviewIndex] >= 0; } bool overviewLoaded(int32 overviewIndex) const { return overviewCount(overviewIndex) == overview[overviewIndex].size(); } int32 overviewCount(int32 overviewIndex, int32 defaultValue = -1) const { int32 result = overviewCountData[overviewIndex], loaded = overview[overviewIndex].size(); if (result < 0) return defaultValue; if (result < loaded) { if (result > 0) { const_cast(this)->overviewCountData[overviewIndex] = 0; } return loaded; } return result; } MsgId overviewMinId(int32 overviewIndex) const { for (MediaOverviewIds::const_iterator i = overviewIds[overviewIndex].cbegin(), e = overviewIds[overviewIndex].cend(); i != e; ++i) { if (i.key() > 0) { return i.key(); } } return 0; } void overviewSliceDone(int32 overviewIndex, const MTPmessages_Messages &result, bool onlyCounts = false); bool overviewHasMsgId(int32 overviewIndex, MsgId msgId) const { return overviewIds[overviewIndex].constFind(msgId) != overviewIds[overviewIndex].cend(); } void changeMsgId(MsgId oldId, MsgId newId); 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 *createItem(const MTPMessage &msg, bool applyServiceAction, bool detachExistingItem); HistoryItem *createItemForwarded(MsgId id, MTPDmessage::Flags flags, QDateTime date, int32 from, HistoryMessage *msg); HistoryItem *createItemDocument(MsgId id, MTPDmessage::Flags flags, int32 viaBotId, MsgId replyTo, QDateTime date, int32 from, DocumentData *doc, const QString &caption); HistoryItem *createItemPhoto(MsgId id, MTPDmessage::Flags flags, int32 viaBotId, MsgId replyTo, QDateTime date, int32 from, PhotoData *photo, const QString &caption); 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. // If the previous item is a message group the new group is // not created but is just united with the previous one. // create(HistoryItem *previous) should return a new HistoryGroup* // unite(HistoryGroup *existing) should unite a new group with an existing template void addMessageGroup(CreateGroup create, UniteGroup unite); void addMessageGroup(const MTPDmessageGroup &group); // 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.isNull(); } private: enum class Flag { f_has_pending_resized_items = (1 << 0), f_pending_resize = (1 << 1), }; Q_DECLARE_FLAGS(Flags, Flag); Q_DECL_CONSTEXPR friend inline QFlags operator|(Flags::enum_type f1, Flags::enum_type f2) noexcept { return QFlags(f1) | f2; } Q_DECL_CONSTEXPR friend inline QFlags operator|(Flags::enum_type f1, QFlags f2) noexcept { return f2 | f1; } Q_DECL_CONSTEXPR friend inline QFlags operator~(Flags::enum_type f) noexcept { return ~QFlags(f); } Flags _flags; ChatListLinksMap _chatListLinks; DialogRow *mainChatListLink() const { auto it = _chatListLinks.constFind(0); t_assert(it != _chatListLinks.cend()); return it.value(); } uint64 _sortKeyInChatList; // like ((unixtime) << 32) | (incremented counter) typedef QMap MediaOverviewIds; MediaOverviewIds overviewIds[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; }; UniquePointer _buildingFrontBlock; // Creates if necessary a new block for adding item. // Depending on isBuildingFrontBlock() gets front or back block. HistoryBlock *prepareBlockForAddingItem(); }; class HistoryGroup; class HistoryCollapse; class HistoryJoined; class ChannelHistory : public History { public: ChannelHistory(const PeerId &peer); void messageDetached(HistoryItem *msg); void messageDeleted(HistoryItem *msg); void messageWithIdDeleted(MsgId msgId); bool isSwitchReadyFor(MsgId switchId, MsgId &fixInScrollMsgId, int32 &fixInScrollMsgTop); // has messages for showing history after switching mode at switchId void getSwitchReadyFor(MsgId switchId, MsgId &fixInScrollMsgId, int32 &fixInScrollMsgTop); void insertCollapseItem(MsgId wasMinId); void getRangeDifference(); void getRangeDifferenceNext(int32 pts); void addNewGroup(const MTPMessageGroup &group); int32 unreadCountAll; bool onlyImportant() const { return _onlyImportant; } HistoryCollapse *collapse() const { return _collapseMessage; } void clearOther() { _otherNewLoaded = true; _otherOldLoaded = false; _otherList.clear(); } 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 addNewToOther(HistoryItem *item, NewMessageType type); void checkMaxReadMessageDate(); HistoryGroup *findGroup(MsgId msgId) const; HistoryBlock *findGroupBlock(MsgId msgId) const; HistoryGroup *findGroupInOther(MsgId msgId) const; HistoryItem *findPrevItem(HistoryItem *item) const; void switchMode(); void cleared(); bool _onlyImportant; QDateTime _maxReadMessageDate; typedef QList OtherList; OtherList _otherList; bool _otherOldLoaded, _otherNewLoaded; HistoryCollapse *_collapseMessage; HistoryJoined *_joinedMessage; MsgId _rangeDifferenceFromId, _rangeDifferenceToId; int32 _rangeDifferencePts; mtpRequestId _rangeDifferenceRequestId; }; enum DialogsSortMode { DialogsSortByDate, DialogsSortByName, DialogsSortByAdd }; struct DialogsList { DialogsList(DialogsSortMode sortMode) : begin(&last), end(&last), sortMode(sortMode), count(0), current(&last) { } void adjustCurrent(int32 y, int32 h) const { int32 pos = (y > 0) ? (y / h) : 0; while (current->pos > pos && current != begin) { current = current->prev; } while (current->pos + 1 <= pos && current->next != end) { current = current->next; } } void paint(Painter &p, int32 w, int32 hFrom, int32 hTo, PeerData *act, PeerData *sel, bool onlyBackground) const { adjustCurrent(hFrom, st::dlgHeight); DialogRow *drawFrom = current; p.translate(0, drawFrom->pos * st::dlgHeight); while (drawFrom != end && drawFrom->pos * st::dlgHeight < hTo) { bool active = (drawFrom->history->peer == act) || (drawFrom->history->peer->migrateTo() && drawFrom->history->peer->migrateTo() == act); bool selected = (drawFrom->history->peer == sel); drawFrom->paint(p, w, active, selected, onlyBackground); drawFrom = drawFrom->next; p.translate(0, st::dlgHeight); } } DialogRow *rowAtY(int32 y, int32 h) const { if (!count) return 0; int32 pos = (y > 0) ? (y / h) : 0; adjustCurrent(y, h); return (pos == current->pos) ? current : 0; } DialogRow *addToEnd(History *history) { DialogRow *result = new DialogRow(history, end->prev, end, end->pos); end->pos++; if (begin == end) { begin = current = result; } else { end->prev->next = result; } rowByPeer.insert(history->peer->id, result); ++count; end->prev = result; if (sortMode == DialogsSortByDate) { adjustByPos(result); } return result; } bool insertBefore(DialogRow *row, DialogRow *before) { if (row == before) return false; if (current == row) current = row->prev; DialogRow *updateTill = row->prev; remove(row); // insert row row->next = before; // update row row->prev = before->prev; row->next->prev = row; // update row->next if (row->prev) { // update row->prev row->prev->next = row; } else { begin = row; } // update y for (DialogRow *n = row; n != updateTill; n = n->next) { n->next->pos++; row->pos--; } return true; } bool insertAfter(DialogRow *row, DialogRow *after) { if (row == after) return false; if (current == row) current = row->next; DialogRow *updateFrom = row->next; remove(row); // insert row row->prev = after; // update row row->next = after->next; row->prev->next = row; // update row->prev row->next->prev = row; // update row->next // update y for (DialogRow *n = updateFrom; n != row; n = n->next) { n->pos--; row->pos++; } return true; } DialogRow *adjustByName(const PeerData *peer) { if (sortMode != DialogsSortByName) return 0; RowByPeer::iterator i = rowByPeer.find(peer->id); if (i == rowByPeer.cend()) return 0; DialogRow *row = i.value(), *change = row; while (change->prev && change->prev->history->peer->name > peer->name) { change = change->prev; } if (!insertBefore(row, change)) { while (change->next != end && change->next->history->peer->name < peer->name) { change = change->next; } insertAfter(row, change); } return row; } DialogRow *addByName(History *history) { if (sortMode != DialogsSortByName) return 0; DialogRow *row = addToEnd(history), *change = row; const QString &peerName(history->peer->name); while (change->prev && change->prev->history->peer->name.compare(peerName, Qt::CaseInsensitive) > 0) { change = change->prev; } if (!insertBefore(row, change)) { while (change->next != end && change->next->history->peer->name.compare(peerName, Qt::CaseInsensitive) < 0) { change = change->next; } insertAfter(row, change); } return row; } void adjustByPos(DialogRow *row) { if (sortMode != DialogsSortByDate) return; DialogRow *change = row; if (change != begin && begin->history->sortKeyInChatList() < row->history->sortKeyInChatList()) { change = begin; } else while (change->prev && change->prev->history->sortKeyInChatList() < row->history->sortKeyInChatList()) { change = change->prev; } if (!insertBefore(row, change)) { if (change->next != end && end->prev->history->sortKeyInChatList() > row->history->sortKeyInChatList()) { change = end->prev; } else while (change->next != end && change->next->history->sortKeyInChatList() > row->history->sortKeyInChatList()) { change = change->next; } insertAfter(row, change); } } bool del(const PeerId &peerId, DialogRow *replacedBy = 0); void remove(DialogRow *row) { row->next->prev = row->prev; // update row->next if (row->prev) { // update row->prev row->prev->next = row->next; } else { begin = row->next; } } void clear() { while (begin != end) { current = begin; begin = begin->next; delete current; } current = begin; rowByPeer.clear(); count = 0; } ~DialogsList() { clear(); } DialogRow last; DialogRow *begin, *end; DialogsSortMode sortMode; int32 count; typedef QHash RowByPeer; RowByPeer rowByPeer; mutable DialogRow *current; // cache }; struct DialogsIndexed { DialogsIndexed(DialogsSortMode sortMode) : sortMode(sortMode), list(sortMode) { } History::ChatListLinksMap addToEnd(History *history) { History::ChatListLinksMap result; DialogsList::RowByPeer::const_iterator i = list.rowByPeer.find(history->peer->id); if (i == list.rowByPeer.cend()) { result.insert(0, list.addToEnd(history)); for (PeerData::NameFirstChars::const_iterator i = history->peer->chars.cbegin(), e = history->peer->chars.cend(); i != e; ++i) { DialogsIndex::iterator j = index.find(*i); if (j == index.cend()) { j = index.insert(*i, new DialogsList(sortMode)); } result.insert(*i, j.value()->addToEnd(history)); } } return result; } DialogRow *addByName(History *history) { DialogsList::RowByPeer::const_iterator i = list.rowByPeer.constFind(history->peer->id); if (i != list.rowByPeer.cend()) { return i.value(); } DialogRow *res = list.addByName(history); for (PeerData::NameFirstChars::const_iterator i = history->peer->chars.cbegin(), e = history->peer->chars.cend(); i != e; ++i) { DialogsIndex::iterator j = index.find(*i); if (j == index.cend()) { j = index.insert(*i, new DialogsList(sortMode)); } j.value()->addByName(history); } return res; } void adjustByPos(const History::ChatListLinksMap &links) { for (History::ChatListLinksMap::const_iterator i = links.cbegin(), e = links.cend(); i != e; ++i) { if (i.key() == QChar(0)) { list.adjustByPos(i.value()); } else { DialogsIndex::iterator j = index.find(i.key()); if (j != index.cend()) { j.value()->adjustByPos(i.value()); } } } } void peerNameChanged(PeerData *peer, const PeerData::Names &oldNames, const PeerData::NameFirstChars &oldChars); void del(const PeerData *peer, DialogRow *replacedBy = 0) { if (list.del(peer->id, replacedBy)) { for (PeerData::NameFirstChars::const_iterator i = peer->chars.cbegin(), e = peer->chars.cend(); i != e; ++i) { DialogsIndex::iterator j = index.find(*i); if (j != index.cend()) { j.value()->del(peer->id, replacedBy); } } } } ~DialogsIndexed() { clear(); } void clear(); DialogsSortMode sortMode; DialogsList list; typedef QMap DialogsIndex; DialogsIndex index; }; class HistoryBlock { public: HistoryBlock(History *hist) : y(0), height(0), history(hist), _indexInHistory(-1) { } HistoryBlock(const HistoryBlock &) = delete; HistoryBlock &operator=(const HistoryBlock &) = delete; typedef QVector Items; Items items; void clear(bool leaveItems = false); ~HistoryBlock() { clear(); } void removeItem(HistoryItem *item); int resizeGetHeight(int newWidth, bool resizeAllItems); int32 y, height; History *history; HistoryBlock *previous() const { t_assert(_indexInHistory >= 0); return (_indexInHistory > 0) ? history->blocks.at(_indexInHistory - 1) : nullptr; } void setIndexInHistory(int index) { _indexInHistory = index; } int indexInHistory() const { t_assert(_indexInHistory >= 0); t_assert(history->blocks.at(_indexInHistory) == this); return _indexInHistory; } protected: int _indexInHistory; }; class HistoryElem { public: HistoryElem() : _maxw(0), _minh(0), _height(0) { } int32 maxWidth() const { return _maxw; } int32 minHeight() const { return _minh; } int32 height() const { return _height; } virtual ~HistoryElem() { } protected: mutable int32 _maxw, _minh, _height; HistoryElem &operator=(const HistoryElem &); }; class HistoryMessage; // dynamic_cast optimize enum HistoryCursorState { HistoryDefaultCursorState, HistoryInTextCursorState, HistoryInDateCursorState, HistoryInForwardedCursorState, }; enum InfoDisplayType { InfoDisplayDefault, InfoDisplayOverImage, InfoDisplayOverBackground, }; inline bool isImportantChannelMessage(MsgId id, MTPDmessage::Flags flags) { // client-side important msgs always has_views or has_from_id return (flags & MTPDmessage::Flag::f_out) || (flags & MTPDmessage::Flag::f_mentioned) || (flags & MTPDmessage::Flag::f_post); } enum HistoryItemType { HistoryItemMsg = 0, HistoryItemGroup, HistoryItemCollapse, HistoryItemJoined }; struct HistoryMessageVia : public BaseComponent { void create(int32 userId); void resize(int32 availw) const; UserData *_bot = nullptr; mutable QString _text; mutable int _width = 0; mutable int _maxWidth = 0; ClickHandlerPtr _lnk; }; struct HistoryMessageViews : public BaseComponent { QString _viewsText; int _views = 0; int _viewsWidth = 0; }; struct HistoryMessageSigned : public BaseComponent { void create(UserData *from, const QDateTime &date); int maxWidth() const; Text _signature; }; struct HistoryMessageForwarded : public BaseComponent { void create(const HistoryMessageVia *via) const; PeerData *_authorOriginal = nullptr; PeerData *_fromOriginal = nullptr; MsgId _originalId = 0; mutable Text _text = { 1 }; }; struct HistoryMessageReply : public BaseComponent { HistoryMessageReply &operator=(HistoryMessageReply &&other) { replyToMsgId = other.replyToMsgId; std::swap(replyToMsg, other.replyToMsg); replyToLnk = std_::move(other.replyToLnk); replyToName = std_::move(other.replyToName); replyToText = std_::move(other.replyToText); replyToVersion = other.replyToVersion; _maxReplyWidth = other._maxReplyWidth; _replyToVia = std_::move(other._replyToVia); return *this; } ~HistoryMessageReply() { // clearData() should be called by holder t_assert(replyToMsg == nullptr); t_assert(_replyToVia.data() == nullptr); } bool updateData(HistoryMessage *holder, bool force = false); void clearData(HistoryMessage *holder); // must be called before destructor void checkNameUpdate() const; void updateName() const; void resize(int width) const; void itemRemoved(HistoryMessage *holder, HistoryItem *removed); enum PaintFlag { PaintInBubble = 0x01, PaintSelected = 0x02, }; Q_DECLARE_FLAGS(PaintFlags, PaintFlag); void paint(Painter &p, const HistoryItem *holder, int x, int y, int w, PaintFlags flags) const; MsgId replyToId() const { return replyToMsgId; } int replyToWidth() const { return _maxReplyWidth; } ClickHandlerPtr replyToLink() const { return replyToLnk; } MsgId replyToMsgId = 0; HistoryItem *replyToMsg = nullptr; ClickHandlerPtr replyToLnk; mutable Text replyToName, replyToText; mutable int replyToVersion = 0; mutable int _maxReplyWidth = 0; UniquePointer _replyToVia; int toWidth = 0; }; Q_DECLARE_OPERATORS_FOR_FLAGS(HistoryMessageReply::PaintFlags); class ReplyKeyboard; struct HistoryMessageReplyMarkup : public BaseComponent { HistoryMessageReplyMarkup() = default; HistoryMessageReplyMarkup(MTPDreplyKeyboardMarkup::Flags f) : flags(f) { } void create(const MTPReplyMarkup &markup); struct Button { enum Type { Default, Url, Callback, RequestPhone, RequestLocation, }; Type type; QString text; QByteArray data; }; using ButtonRow = QVector