mirror of
https://github.com/vale981/tdesktop
synced 2025-03-08 19:21:39 -05:00

When we load previous messages in chat history we add all authors of the messages to lastAuthors in regular groups (so that we can suggest them in mention autocomplete). The same logic was (blindly) applied to supergroups lastParticipants list which is used not only for the mention autocomplete but also in Profile members list. That way we were showing there users who could've already left the group.
2281 lines
69 KiB
C++
2281 lines
69 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
|
|
*/
|
|
#include "stdafx.h"
|
|
#include "history.h"
|
|
|
|
#include "history/history_media_types.h"
|
|
#include "dialogs/dialogs_indexed_list.h"
|
|
#include "styles/style_dialogs.h"
|
|
#include "data/data_drafts.h"
|
|
#include "lang.h"
|
|
#include "apiwrap.h"
|
|
#include "mainwidget.h"
|
|
#include "mainwindow.h"
|
|
#include "localstorage.h"
|
|
#include "window/top_bar_widget.h"
|
|
#include "observer_peer.h"
|
|
#include "auth_session.h"
|
|
|
|
namespace {
|
|
|
|
constexpr int kStatusShowClientsideTyping = 6000;
|
|
constexpr int kStatusShowClientsideRecordVideo = 6000;
|
|
constexpr int kStatusShowClientsideUploadVideo = 6000;
|
|
constexpr int kStatusShowClientsideRecordVoice = 6000;
|
|
constexpr int kStatusShowClientsideUploadVoice = 6000;
|
|
constexpr int kStatusShowClientsideUploadPhoto = 6000;
|
|
constexpr int kStatusShowClientsideUploadFile = 6000;
|
|
constexpr int kStatusShowClientsideChooseLocation = 6000;
|
|
constexpr int kStatusShowClientsideChooseContact = 6000;
|
|
constexpr int kStatusShowClientsidePlayGame = 10000;
|
|
constexpr int kSetMyActionForMs = 10000;
|
|
|
|
auto GlobalPinnedIndex = 0;
|
|
|
|
HistoryItem *createUnsupportedMessage(History *history, MsgId msgId, MTPDmessage::Flags flags, MsgId replyTo, int32 viaBotId, QDateTime date, int32 from) {
|
|
QString text(lng_message_unsupported(lt_link, qsl("https://desktop.telegram.org")));
|
|
EntitiesInText entities;
|
|
textParseEntities(text, _historyTextNoMonoOptions.flags, &entities);
|
|
entities.push_front(EntityInText(EntityInTextItalic, 0, text.size()));
|
|
return HistoryMessage::create(history, msgId, flags, replyTo, viaBotId, date, from, { text, entities });
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void historyInit() {
|
|
historyInitMessages();
|
|
historyInitMedia();
|
|
}
|
|
|
|
History::History(const PeerId &peerId)
|
|
: peer(App::peer(peerId))
|
|
, lastItemTextCache(st::dialogsTextWidthMin)
|
|
, cloudDraftTextCache(st::dialogsTextWidthMin)
|
|
, _mute(isNotifyMuted(peer->notify))
|
|
, _sendActionText(st::dialogsTextWidthMin) {
|
|
if (peer->isUser() && peer->asUser()->botInfo) {
|
|
outboxReadBefore = INT_MAX;
|
|
}
|
|
for (auto &countData : overviewCountData) {
|
|
countData = -1; // not loaded yet
|
|
}
|
|
}
|
|
|
|
void History::clearLastKeyboard() {
|
|
if (lastKeyboardId) {
|
|
if (lastKeyboardId == lastKeyboardHiddenId) {
|
|
lastKeyboardHiddenId = 0;
|
|
}
|
|
lastKeyboardId = 0;
|
|
if (auto main = App::main()) {
|
|
main->updateBotKeyboard(this);
|
|
}
|
|
}
|
|
lastKeyboardInited = true;
|
|
lastKeyboardFrom = 0;
|
|
}
|
|
|
|
bool History::canHaveFromPhotos() const {
|
|
if (peer->isUser() && !Adaptive::ChatWide()) {
|
|
return false;
|
|
} else if (isChannel() && !peer->isMegagroup()) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void History::setHasPendingResizedItems() {
|
|
_flags |= Flag::f_has_pending_resized_items;
|
|
Global::RefHandleHistoryUpdate().call();
|
|
}
|
|
|
|
void History::setLocalDraft(std::unique_ptr<Data::Draft> &&draft) {
|
|
_localDraft = std::move(draft);
|
|
}
|
|
|
|
void History::takeLocalDraft(History *from) {
|
|
if (auto &draft = from->_localDraft) {
|
|
if (!draft->textWithTags.text.isEmpty() && !_localDraft) {
|
|
_localDraft = std::move(draft);
|
|
|
|
// Edit and reply to drafts can't migrate.
|
|
// Cloud drafts do not migrate automatically.
|
|
_localDraft->msgId = 0;
|
|
}
|
|
from->clearLocalDraft();
|
|
App::api()->saveDraftToCloudDelayed(from);
|
|
}
|
|
}
|
|
|
|
void History::createLocalDraftFromCloud() {
|
|
auto draft = cloudDraft();
|
|
if (Data::draftIsNull(draft) || !draft->date.isValid()) return;
|
|
|
|
auto existing = localDraft();
|
|
if (Data::draftIsNull(existing) || !existing->date.isValid() || draft->date >= existing->date) {
|
|
if (!existing) {
|
|
setLocalDraft(std::make_unique<Data::Draft>(draft->textWithTags, draft->msgId, draft->cursor, draft->previewCancelled));
|
|
existing = localDraft();
|
|
} else if (existing != draft) {
|
|
existing->textWithTags = draft->textWithTags;
|
|
existing->msgId = draft->msgId;
|
|
existing->cursor = draft->cursor;
|
|
existing->previewCancelled = draft->previewCancelled;
|
|
}
|
|
existing->date = draft->date;
|
|
}
|
|
}
|
|
|
|
void History::setCloudDraft(std::unique_ptr<Data::Draft> &&draft) {
|
|
_cloudDraft = std::move(draft);
|
|
cloudDraftTextCache.clear();
|
|
}
|
|
|
|
Data::Draft *History::createCloudDraft(Data::Draft *fromDraft) {
|
|
if (Data::draftIsNull(fromDraft)) {
|
|
setCloudDraft(std::make_unique<Data::Draft>(TextWithTags(), 0, MessageCursor(), false));
|
|
cloudDraft()->date = QDateTime();
|
|
} else {
|
|
auto existing = cloudDraft();
|
|
if (!existing) {
|
|
setCloudDraft(std::make_unique<Data::Draft>(fromDraft->textWithTags, fromDraft->msgId, fromDraft->cursor, fromDraft->previewCancelled));
|
|
existing = cloudDraft();
|
|
} else if (existing != fromDraft) {
|
|
existing->textWithTags = fromDraft->textWithTags;
|
|
existing->msgId = fromDraft->msgId;
|
|
existing->cursor = fromDraft->cursor;
|
|
existing->previewCancelled = fromDraft->previewCancelled;
|
|
}
|
|
existing->date = ::date(myunixtime());
|
|
}
|
|
|
|
cloudDraftTextCache.clear();
|
|
updateChatListSortPosition();
|
|
|
|
return cloudDraft();
|
|
}
|
|
|
|
void History::setEditDraft(std::unique_ptr<Data::Draft> &&draft) {
|
|
_editDraft = std::move(draft);
|
|
}
|
|
|
|
void History::clearLocalDraft() {
|
|
_localDraft = nullptr;
|
|
}
|
|
|
|
void History::clearCloudDraft() {
|
|
if (_cloudDraft) {
|
|
_cloudDraft = nullptr;
|
|
cloudDraftTextCache.clear();
|
|
updateChatListSortPosition();
|
|
}
|
|
}
|
|
|
|
void History::clearEditDraft() {
|
|
_editDraft = nullptr;
|
|
}
|
|
|
|
void History::draftSavedToCloud() {
|
|
updateChatListEntry();
|
|
if (App::main()) App::main()->writeDrafts(this);
|
|
}
|
|
|
|
bool History::updateSendActionNeedsAnimating(UserData *user, const MTPSendMessageAction &action) {
|
|
using Type = SendAction::Type;
|
|
if (action.type() == mtpc_sendMessageCancelAction) {
|
|
unregSendAction(user);
|
|
return false;
|
|
}
|
|
|
|
auto ms = getms();
|
|
switch (action.type()) {
|
|
case mtpc_sendMessageTypingAction: _typing.insert(user, ms + kStatusShowClientsideTyping); break;
|
|
case mtpc_sendMessageRecordVideoAction: _sendActions.insert(user, { Type::RecordVideo, ms + kStatusShowClientsideRecordVideo }); break;
|
|
case mtpc_sendMessageUploadVideoAction: _sendActions.insert(user, { Type::UploadVideo, ms + kStatusShowClientsideUploadVideo, action.c_sendMessageUploadVideoAction().vprogress.v }); break;
|
|
case mtpc_sendMessageRecordAudioAction: _sendActions.insert(user, { Type::RecordVoice, ms + kStatusShowClientsideRecordVoice }); break;
|
|
case mtpc_sendMessageUploadAudioAction: _sendActions.insert(user, { Type::UploadVoice, ms + kStatusShowClientsideUploadVoice, action.c_sendMessageUploadAudioAction().vprogress.v }); break;
|
|
case mtpc_sendMessageUploadPhotoAction: _sendActions.insert(user, { Type::UploadPhoto, ms + kStatusShowClientsideUploadPhoto, action.c_sendMessageUploadPhotoAction().vprogress.v }); break;
|
|
case mtpc_sendMessageUploadDocumentAction: _sendActions.insert(user, { Type::UploadFile, ms + kStatusShowClientsideUploadFile, action.c_sendMessageUploadDocumentAction().vprogress.v }); break;
|
|
case mtpc_sendMessageGeoLocationAction: _sendActions.insert(user, { Type::ChooseLocation, ms + kStatusShowClientsideChooseLocation }); break;
|
|
case mtpc_sendMessageChooseContactAction: _sendActions.insert(user, { Type::ChooseContact, ms + kStatusShowClientsideChooseContact }); break;
|
|
case mtpc_sendMessageGamePlayAction: {
|
|
auto it = _sendActions.find(user);
|
|
if (it == _sendActions.end() || it->type == Type::PlayGame || it->until <= ms) {
|
|
_sendActions.insert(user, { Type::PlayGame, ms + kStatusShowClientsidePlayGame });
|
|
}
|
|
} break;
|
|
default: return false;
|
|
}
|
|
return updateSendActionNeedsAnimating(ms, true);
|
|
}
|
|
|
|
bool History::mySendActionUpdated(SendAction::Type type, bool doing) {
|
|
auto ms = getms(true);
|
|
auto i = _mySendActions.find(type);
|
|
if (doing) {
|
|
if (i == _mySendActions.cend()) {
|
|
_mySendActions.insert(type, ms + kSetMyActionForMs);
|
|
} else if (i.value() > ms + (kSetMyActionForMs / 2)) {
|
|
return false;
|
|
} else {
|
|
i.value() = ms + kSetMyActionForMs;
|
|
}
|
|
} else {
|
|
if (i == _mySendActions.cend()) {
|
|
return false;
|
|
} else if (i.value() <= ms) {
|
|
return false;
|
|
} else {
|
|
_mySendActions.erase(i);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool History::paintSendAction(Painter &p, int x, int y, int availableWidth, int outerWidth, style::color color, TimeMs ms) {
|
|
if (_sendActionAnimation) {
|
|
_sendActionAnimation.paint(p, color, x, y + st::normalFont->ascent, outerWidth, ms);
|
|
auto animationWidth = _sendActionAnimation.width();
|
|
x += animationWidth;
|
|
availableWidth -= animationWidth;
|
|
p.setPen(color);
|
|
_sendActionText.drawElided(p, x, y, availableWidth);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool History::updateSendActionNeedsAnimating(TimeMs ms, bool force) {
|
|
auto changed = force;
|
|
for (auto i = _typing.begin(), e = _typing.end(); i != e;) {
|
|
if (ms >= i.value()) {
|
|
i = _typing.erase(i);
|
|
changed = true;
|
|
} else {
|
|
++i;
|
|
}
|
|
}
|
|
for (auto i = _sendActions.begin(); i != _sendActions.cend();) {
|
|
if (ms >= i.value().until) {
|
|
i = _sendActions.erase(i);
|
|
changed = true;
|
|
} else {
|
|
++i;
|
|
}
|
|
}
|
|
if (changed) {
|
|
QString newTypingString;
|
|
auto typingCount = _typing.size();
|
|
if (typingCount > 2) {
|
|
newTypingString = lng_many_typing(lt_count, typingCount);
|
|
} else if (typingCount > 1) {
|
|
newTypingString = lng_users_typing(lt_user, _typing.begin().key()->firstName, lt_second_user, (_typing.end() - 1).key()->firstName);
|
|
} else if (typingCount) {
|
|
newTypingString = peer->isUser() ? lang(lng_typing) : lng_user_typing(lt_user, _typing.begin().key()->firstName);
|
|
} else if (!_sendActions.isEmpty()) {
|
|
// Handles all actions except game playing.
|
|
using Type = SendAction::Type;
|
|
auto sendActionString = [](Type type, const QString &name) -> QString {
|
|
switch (type) {
|
|
case Type::RecordVideo: return name.isEmpty() ? lang(lng_send_action_record_video) : lng_user_action_record_video(lt_user, name);
|
|
case Type::UploadVideo: return name.isEmpty() ? lang(lng_send_action_upload_video) : lng_user_action_upload_video(lt_user, name);
|
|
case Type::RecordVoice: return name.isEmpty() ? lang(lng_send_action_record_audio) : lng_user_action_record_audio(lt_user, name);
|
|
case Type::UploadVoice: return name.isEmpty() ? lang(lng_send_action_upload_audio) : lng_user_action_upload_audio(lt_user, name);
|
|
case Type::UploadPhoto: return name.isEmpty() ? lang(lng_send_action_upload_photo) : lng_user_action_upload_photo(lt_user, name);
|
|
case Type::UploadFile: return name.isEmpty() ? lang(lng_send_action_upload_file) : lng_user_action_upload_file(lt_user, name);
|
|
case Type::ChooseLocation: return name.isEmpty() ? lang(lng_send_action_geo_location) : lng_user_action_geo_location(lt_user, name);
|
|
case Type::ChooseContact: return name.isEmpty() ? lang(lng_send_action_choose_contact) : lng_user_action_choose_contact(lt_user, name);
|
|
default: break;
|
|
};
|
|
return QString();
|
|
};
|
|
for (auto i = _sendActions.cbegin(), e = _sendActions.cend(); i != e; ++i) {
|
|
newTypingString = sendActionString(i->type, peer->isUser() ? QString() : i.key()->firstName);
|
|
if (!newTypingString.isEmpty()) {
|
|
_sendActionAnimation.start(i->type);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Everyone in sendActions are playing a game.
|
|
if (newTypingString.isEmpty()) {
|
|
int playingCount = _sendActions.size();
|
|
if (playingCount > 2) {
|
|
newTypingString = lng_many_playing_game(lt_count, playingCount);
|
|
} else if (playingCount > 1) {
|
|
newTypingString = lng_users_playing_game(lt_user, _sendActions.begin().key()->firstName, lt_second_user, (_sendActions.end() - 1).key()->firstName);
|
|
} else {
|
|
newTypingString = peer->isUser() ? lang(lng_playing_game) : lng_user_playing_game(lt_user, _sendActions.begin().key()->firstName);
|
|
}
|
|
_sendActionAnimation.start(Type::PlayGame);
|
|
}
|
|
}
|
|
if (typingCount > 0) {
|
|
_sendActionAnimation.start(SendAction::Type::Typing);
|
|
} else if (newTypingString.isEmpty()) {
|
|
_sendActionAnimation.stop();
|
|
}
|
|
if (_sendActionString != newTypingString) {
|
|
_sendActionString = newTypingString;
|
|
_sendActionText.setText(st::dialogsTextStyle, _sendActionString, _textNameOptions);
|
|
}
|
|
}
|
|
auto result = (!_typing.isEmpty() || !_sendActions.isEmpty());
|
|
if (changed || result) {
|
|
App::histories().sendActionAnimationUpdated().notify({
|
|
this,
|
|
_sendActionAnimation.width(),
|
|
st::normalFont->height,
|
|
changed
|
|
});
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void ChannelHistory::getRangeDifference() {
|
|
auto fromId = MsgId(0), toId = MsgId(0);
|
|
for (auto blockIndex = 0, blocksCount = blocks.size(); blockIndex < blocksCount; ++blockIndex) {
|
|
auto block = blocks.at(blockIndex);
|
|
for (auto itemIndex = 0, itemsCount = block->items.size(); itemIndex < itemsCount; ++itemIndex) {
|
|
auto item = block->items.at(itemIndex);
|
|
if (item->id > 0) {
|
|
fromId = item->id;
|
|
break;
|
|
}
|
|
}
|
|
if (fromId) break;
|
|
}
|
|
if (!fromId) return;
|
|
|
|
for (auto blockIndex = blocks.size(); blockIndex > 0;) {
|
|
auto block = blocks.at(--blockIndex);
|
|
for (auto itemIndex = block->items.size(); itemIndex > 0;) {
|
|
auto item = block->items.at(--itemIndex);
|
|
if (item->id > 0) {
|
|
toId = item->id;
|
|
break;
|
|
}
|
|
}
|
|
if (toId) break;
|
|
}
|
|
if (fromId > 0 && peer->asChannel()->pts() > 0) {
|
|
if (_rangeDifferenceRequestId) {
|
|
MTP::cancel(_rangeDifferenceRequestId);
|
|
}
|
|
_rangeDifferenceFromId = fromId;
|
|
_rangeDifferenceToId = toId;
|
|
|
|
MTP_LOG(0, ("getChannelDifference { good - after channelDifferenceTooLong was received, validating history part }%1").arg(cTestMode() ? " TESTMODE" : ""));
|
|
getRangeDifferenceNext(peer->asChannel()->pts());
|
|
}
|
|
}
|
|
|
|
void ChannelHistory::getRangeDifferenceNext(int32 pts) {
|
|
if (!App::main() || _rangeDifferenceToId < _rangeDifferenceFromId) return;
|
|
|
|
int limit = _rangeDifferenceToId + 1 - _rangeDifferenceFromId;
|
|
|
|
auto filter = MTP_channelMessagesFilter(MTP_flags(MTPDchannelMessagesFilter::Flags(0)), MTP_vector<MTPMessageRange>(1, MTP_messageRange(MTP_int(_rangeDifferenceFromId), MTP_int(_rangeDifferenceToId))));
|
|
MTPupdates_GetChannelDifference::Flags flags = MTPupdates_GetChannelDifference::Flag::f_force;
|
|
_rangeDifferenceRequestId = MTP::send(MTPupdates_GetChannelDifference(MTP_flags(flags), peer->asChannel()->inputChannel, filter, MTP_int(pts), MTP_int(limit)), App::main()->rpcDone(&MainWidget::gotRangeDifference, peer->asChannel()));
|
|
}
|
|
|
|
HistoryJoined *ChannelHistory::insertJoinedMessage(bool unread) {
|
|
if (_joinedMessage || !peer->asChannel()->amIn() || (peer->isMegagroup() && peer->asChannel()->mgInfo->joinedMessageFound)) {
|
|
return _joinedMessage;
|
|
}
|
|
|
|
UserData *inviter = (peer->asChannel()->inviter > 0) ? App::userLoaded(peer->asChannel()->inviter) : nullptr;
|
|
if (!inviter) return nullptr;
|
|
|
|
MTPDmessage::Flags flags = 0;
|
|
if (inviter->id == AuthSession::CurrentUserPeerId()) {
|
|
unread = false;
|
|
//} else if (unread) {
|
|
// flags |= MTPDmessage::Flag::f_unread;
|
|
}
|
|
|
|
QDateTime inviteDate = peer->asChannel()->inviteDate;
|
|
if (unread) _maxReadMessageDate = inviteDate;
|
|
if (isEmpty()) {
|
|
_joinedMessage = HistoryJoined::create(this, inviteDate, inviter, flags);
|
|
addNewItem(_joinedMessage, unread);
|
|
return _joinedMessage;
|
|
}
|
|
|
|
for (auto blockIndex = blocks.size(); blockIndex > 0;) {
|
|
auto block = blocks.at(--blockIndex);
|
|
for (auto itemIndex = block->items.size(); itemIndex > 0;) {
|
|
auto item = block->items.at(--itemIndex);
|
|
|
|
// Due to a server bug sometimes inviteDate is less (before) than the
|
|
// first message in the megagroup (message about migration), let us
|
|
// ignore that and think, that the inviteDate is always greater-or-equal.
|
|
if (item->isGroupMigrate() && peer->isMegagroup() && peer->migrateFrom()) {
|
|
peer->asChannel()->mgInfo->joinedMessageFound = true;
|
|
return nullptr;
|
|
}
|
|
if (item->date <= inviteDate) {
|
|
++itemIndex;
|
|
_joinedMessage = HistoryJoined::create(this, inviteDate, inviter, flags);
|
|
addNewInTheMiddle(_joinedMessage, blockIndex, itemIndex);
|
|
if (lastMsgDate.isNull() || inviteDate >= lastMsgDate) {
|
|
setLastMessage(_joinedMessage);
|
|
if (unread) {
|
|
newItemAdded(_joinedMessage);
|
|
}
|
|
}
|
|
return _joinedMessage;
|
|
}
|
|
}
|
|
}
|
|
|
|
startBuildingFrontBlock();
|
|
|
|
_joinedMessage = HistoryJoined::create(this, inviteDate, inviter, flags);
|
|
addItemToBlock(_joinedMessage);
|
|
|
|
finishBuildingFrontBlock();
|
|
|
|
return _joinedMessage;
|
|
}
|
|
|
|
void ChannelHistory::checkJoinedMessage(bool createUnread) {
|
|
if (_joinedMessage || peer->asChannel()->inviter <= 0) {
|
|
return;
|
|
}
|
|
if (isEmpty()) {
|
|
if (loadedAtTop() && loadedAtBottom()) {
|
|
if (insertJoinedMessage(createUnread)) {
|
|
if (!_joinedMessage->detached()) {
|
|
setLastMessage(_joinedMessage);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
QDateTime inviteDate = peer->asChannel()->inviteDate;
|
|
QDateTime firstDate, lastDate;
|
|
if (!blocks.isEmpty()) {
|
|
firstDate = blocks.front()->items.front()->date;
|
|
lastDate = blocks.back()->items.back()->date;
|
|
}
|
|
if (!firstDate.isNull() && !lastDate.isNull() && (firstDate <= inviteDate || loadedAtTop()) && (lastDate > inviteDate || loadedAtBottom())) {
|
|
bool willBeLastMsg = (inviteDate >= lastDate);
|
|
if (insertJoinedMessage(createUnread && willBeLastMsg) && willBeLastMsg) {
|
|
if (!_joinedMessage->detached()) {
|
|
setLastMessage(_joinedMessage);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ChannelHistory::checkMaxReadMessageDate() {
|
|
if (_maxReadMessageDate.isValid()) return;
|
|
|
|
for (int blockIndex = blocks.size(); blockIndex > 0;) {
|
|
HistoryBlock *block = blocks.at(--blockIndex);
|
|
for (int itemIndex = block->items.size(); itemIndex > 0;) {
|
|
HistoryItem *item = block->items.at(--itemIndex);
|
|
if (!item->unread()) {
|
|
_maxReadMessageDate = item->date;
|
|
if (item->isGroupMigrate() && isMegagroup() && peer->migrateFrom()) {
|
|
_maxReadMessageDate = date(MTP_int(peer->asChannel()->date + 1)); // no report spam panel
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
if (loadedAtTop() && (!isMegagroup() || !isEmpty())) {
|
|
_maxReadMessageDate = date(MTP_int(peer->asChannel()->date));
|
|
}
|
|
}
|
|
|
|
const QDateTime &ChannelHistory::maxReadMessageDate() {
|
|
return _maxReadMessageDate;
|
|
}
|
|
|
|
HistoryItem *ChannelHistory::addNewChannelMessage(const MTPMessage &msg, NewMessageType type) {
|
|
if (type == NewMessageExisting) return addToHistory(msg);
|
|
|
|
return addNewToBlocks(msg, type);
|
|
}
|
|
|
|
HistoryItem *ChannelHistory::addNewToBlocks(const MTPMessage &msg, NewMessageType type) {
|
|
if (!loadedAtBottom()) {
|
|
HistoryItem *item = addToHistory(msg);
|
|
if (item) {
|
|
setLastMessage(item);
|
|
if (type == NewMessageUnread) {
|
|
newItemAdded(item);
|
|
}
|
|
}
|
|
return item;
|
|
}
|
|
|
|
return addNewToLastBlock(msg, type);
|
|
}
|
|
|
|
void ChannelHistory::cleared(bool leaveItems) {
|
|
_joinedMessage = nullptr;
|
|
}
|
|
|
|
void ChannelHistory::messageDetached(HistoryItem *msg) {
|
|
if (_joinedMessage == msg) {
|
|
_joinedMessage = nullptr;
|
|
}
|
|
}
|
|
|
|
ChannelHistory::~ChannelHistory() {
|
|
// all items must be destroyed before ChannelHistory is destroyed
|
|
// or they will call history()->asChannelHistory() -> undefined behaviour
|
|
clearOnDestroy();
|
|
}
|
|
|
|
History *Histories::find(const PeerId &peerId) {
|
|
Map::const_iterator i = map.constFind(peerId);
|
|
return (i == map.cend()) ? 0 : i.value();
|
|
}
|
|
|
|
History *Histories::findOrInsert(const PeerId &peerId) {
|
|
auto i = map.constFind(peerId);
|
|
if (i == map.cend()) {
|
|
auto history = peerIsChannel(peerId) ? static_cast<History*>(new ChannelHistory(peerId)) : (new History(peerId));
|
|
i = map.insert(peerId, history);
|
|
}
|
|
return i.value();
|
|
}
|
|
|
|
History *Histories::findOrInsert(const PeerId &peerId, int32 unreadCount, int32 maxInboxRead, int32 maxOutboxRead) {
|
|
auto i = map.constFind(peerId);
|
|
if (i == map.cend()) {
|
|
auto history = peerIsChannel(peerId) ? static_cast<History*>(new ChannelHistory(peerId)) : (new History(peerId));
|
|
i = map.insert(peerId, history);
|
|
history->setUnreadCount(unreadCount);
|
|
history->inboxReadBefore = maxInboxRead + 1;
|
|
history->outboxReadBefore = maxOutboxRead + 1;
|
|
} else {
|
|
auto history = i.value();
|
|
if (unreadCount > history->unreadCount()) {
|
|
history->setUnreadCount(unreadCount);
|
|
}
|
|
accumulate_max(history->inboxReadBefore, maxInboxRead + 1);
|
|
accumulate_max(history->outboxReadBefore, maxOutboxRead + 1);
|
|
}
|
|
return i.value();
|
|
}
|
|
|
|
void Histories::clear() {
|
|
App::historyClearMsgs();
|
|
|
|
_pinnedDialogs.clear();
|
|
auto temp = base::take(map);
|
|
for_const (auto history, temp) {
|
|
delete history;
|
|
}
|
|
|
|
_unreadFull = _unreadMuted = 0;
|
|
Notify::unreadCounterUpdated();
|
|
App::historyClearItems();
|
|
typing.clear();
|
|
}
|
|
|
|
void Histories::regSendAction(History *history, UserData *user, const MTPSendMessageAction &action, TimeId when) {
|
|
if (history->updateSendActionNeedsAnimating(user, action)) {
|
|
user->madeAction(when);
|
|
|
|
auto i = typing.find(history);
|
|
if (i == typing.cend()) {
|
|
typing.insert(history, getms());
|
|
_a_typings.start();
|
|
}
|
|
}
|
|
}
|
|
|
|
void Histories::step_typings(TimeMs ms, bool timer) {
|
|
for (auto i = typing.begin(), e = typing.end(); i != e;) {
|
|
if (i.key()->updateSendActionNeedsAnimating(ms)) {
|
|
++i;
|
|
} else {
|
|
i = typing.erase(i);
|
|
}
|
|
}
|
|
if (typing.isEmpty()) {
|
|
_a_typings.stop();
|
|
}
|
|
}
|
|
|
|
void Histories::remove(const PeerId &peer) {
|
|
Map::iterator i = map.find(peer);
|
|
if (i != map.cend()) {
|
|
typing.remove(i.value());
|
|
delete i.value();
|
|
map.erase(i);
|
|
}
|
|
}
|
|
|
|
namespace {
|
|
|
|
void checkForSwitchInlineButton(HistoryItem *item) {
|
|
if (item->out() || !item->hasSwitchInlineButton()) {
|
|
return;
|
|
}
|
|
if (UserData *user = item->history()->peer->asUser()) {
|
|
if (!user->botInfo || !user->botInfo->inlineReturnPeerId) {
|
|
return;
|
|
}
|
|
if (auto markup = item->Get<HistoryMessageReplyMarkup>()) {
|
|
for_const (auto &row, markup->rows) {
|
|
for_const (auto &button, row) {
|
|
if (button.type == HistoryMessageReplyMarkup::Button::Type::SwitchInline) {
|
|
Notify::switchInlineBotButtonReceived(QString::fromUtf8(button.data));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
HistoryItem *Histories::addNewMessage(const MTPMessage &msg, NewMessageType type) {
|
|
auto peer = peerFromMessage(msg);
|
|
if (!peer) return nullptr;
|
|
|
|
auto result = App::history(peer)->addNewMessage(msg, type);
|
|
if (result && type == NewMessageUnread) {
|
|
checkForSwitchInlineButton(result);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
int Histories::unreadBadge() const {
|
|
return _unreadFull - (Global::IncludeMuted() ? 0 : _unreadMuted);
|
|
}
|
|
|
|
bool Histories::unreadOnlyMuted() const {
|
|
return Global::IncludeMuted() ? (_unreadMuted >= _unreadFull) : false;
|
|
}
|
|
|
|
void Histories::setIsPinned(History *history, bool isPinned) {
|
|
if (isPinned) {
|
|
_pinnedDialogs.insert(history);
|
|
if (_pinnedDialogs.size() > Global::PinnedDialogsCountMax()) {
|
|
auto minIndex = GlobalPinnedIndex + 1;
|
|
auto minIndexHistory = (History*)nullptr;
|
|
for_const (auto pinned, _pinnedDialogs) {
|
|
if (pinned->getPinnedIndex() < minIndex) {
|
|
minIndex = pinned->getPinnedIndex();
|
|
minIndexHistory = pinned;
|
|
}
|
|
}
|
|
t_assert(minIndexHistory != nullptr);
|
|
minIndexHistory->setPinnedDialog(false);
|
|
}
|
|
} else {
|
|
_pinnedDialogs.remove(history);
|
|
}
|
|
}
|
|
|
|
void Histories::clearPinned() {
|
|
for (auto pinned : base::take(_pinnedDialogs)) {
|
|
pinned->setPinnedDialog(false);
|
|
}
|
|
}
|
|
|
|
int Histories::pinnedCount() const {
|
|
return _pinnedDialogs.size();
|
|
}
|
|
|
|
QList<History*> Histories::getPinnedOrder() const {
|
|
QMap<int, History*> sorter;
|
|
for_const (auto pinned, _pinnedDialogs) {
|
|
sorter.insert(pinned->getPinnedIndex(), pinned);
|
|
}
|
|
QList<History*> result;
|
|
for (auto i = sorter.cend(), e = sorter.cbegin(); i != e;) {
|
|
--i;
|
|
result.push_back(i.value());
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void Histories::savePinnedToServer() const {
|
|
auto order = getPinnedOrder();
|
|
auto peers = QVector<MTPInputPeer>();
|
|
peers.reserve(order.size());
|
|
for_const (auto history, order) {
|
|
peers.push_back(history->peer->input);
|
|
}
|
|
auto flags = MTPmessages_ReorderPinnedDialogs::Flag::f_force;
|
|
MTP::send(MTPmessages_ReorderPinnedDialogs(MTP_flags(qFlags(flags)), MTP_vector(peers)));
|
|
}
|
|
|
|
HistoryItem *History::createItem(const MTPMessage &msg, bool applyServiceAction, bool detachExistingItem) {
|
|
auto msgId = MsgId(0);
|
|
switch (msg.type()) {
|
|
case mtpc_messageEmpty: msgId = msg.c_messageEmpty().vid.v; break;
|
|
case mtpc_message: msgId = msg.c_message().vid.v; break;
|
|
case mtpc_messageService: msgId = msg.c_messageService().vid.v; break;
|
|
}
|
|
if (!msgId) return nullptr;
|
|
|
|
auto result = App::histItemById(channelId(), msgId);
|
|
if (result) {
|
|
if (!result->detached() && detachExistingItem) {
|
|
result->detach();
|
|
}
|
|
if (msg.type() == mtpc_message) {
|
|
result->updateMedia(msg.c_message().has_media() ? (&msg.c_message().vmedia) : 0);
|
|
if (applyServiceAction) {
|
|
App::checkSavedGif(result);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
switch (msg.type()) {
|
|
case mtpc_messageEmpty:
|
|
result = HistoryService::create(this, msg.c_messageEmpty().vid.v, date(), lang(lng_message_empty));
|
|
break;
|
|
|
|
case mtpc_message: {
|
|
auto &m = msg.c_message();
|
|
enum class MediaCheckResult {
|
|
Good,
|
|
Unsupported,
|
|
Empty,
|
|
};
|
|
auto badMedia = MediaCheckResult::Good;
|
|
if (m.has_media()) switch (m.vmedia.type()) {
|
|
case mtpc_messageMediaEmpty:
|
|
case mtpc_messageMediaContact: break;
|
|
case mtpc_messageMediaGeo:
|
|
switch (m.vmedia.c_messageMediaGeo().vgeo.type()) {
|
|
case mtpc_geoPoint: break;
|
|
case mtpc_geoPointEmpty: badMedia = MediaCheckResult::Empty; break;
|
|
default: badMedia = MediaCheckResult::Unsupported; break;
|
|
}
|
|
break;
|
|
case mtpc_messageMediaVenue:
|
|
switch (m.vmedia.c_messageMediaVenue().vgeo.type()) {
|
|
case mtpc_geoPoint: break;
|
|
case mtpc_geoPointEmpty: badMedia = MediaCheckResult::Empty; break;
|
|
default: badMedia = MediaCheckResult::Unsupported; break;
|
|
}
|
|
break;
|
|
case mtpc_messageMediaPhoto:
|
|
switch (m.vmedia.c_messageMediaPhoto().vphoto.type()) {
|
|
case mtpc_photo: break;
|
|
case mtpc_photoEmpty: badMedia = MediaCheckResult::Empty; break;
|
|
default: badMedia = MediaCheckResult::Unsupported; break;
|
|
}
|
|
break;
|
|
case mtpc_messageMediaDocument:
|
|
switch (m.vmedia.c_messageMediaDocument().vdocument.type()) {
|
|
case mtpc_document: break;
|
|
case mtpc_documentEmpty: badMedia = MediaCheckResult::Empty; break;
|
|
default: badMedia = MediaCheckResult::Unsupported; break;
|
|
}
|
|
break;
|
|
case mtpc_messageMediaWebPage:
|
|
switch (m.vmedia.c_messageMediaWebPage().vwebpage.type()) {
|
|
case mtpc_webPage:
|
|
case mtpc_webPageEmpty:
|
|
case mtpc_webPagePending: break;
|
|
case mtpc_webPageNotModified:
|
|
default: badMedia = MediaCheckResult::Unsupported; break;
|
|
}
|
|
break;
|
|
case mtpc_messageMediaGame:
|
|
switch (m.vmedia.c_messageMediaGame().vgame.type()) {
|
|
case mtpc_game: break;
|
|
default: badMedia = MediaCheckResult::Unsupported; break;
|
|
}
|
|
break;
|
|
case mtpc_messageMediaUnsupported:
|
|
default: badMedia = MediaCheckResult::Unsupported; break;
|
|
}
|
|
if (badMedia == MediaCheckResult::Unsupported) {
|
|
result = createUnsupportedMessage(this, m.vid.v, m.vflags.v, m.vreply_to_msg_id.v, m.vvia_bot_id.v, date(m.vdate), m.vfrom_id.v);
|
|
} else if (badMedia == MediaCheckResult::Empty) {
|
|
result = HistoryService::create(this, m.vid.v, date(m.vdate), lang(lng_message_empty), m.vflags.v, m.has_from_id() ? m.vfrom_id.v : 0);
|
|
} else {
|
|
result = HistoryMessage::create(this, m);
|
|
}
|
|
} break;
|
|
|
|
case mtpc_messageService: {
|
|
auto &m = msg.c_messageService();
|
|
if (m.vaction.type() == mtpc_messageActionPhoneCall) {
|
|
auto viaBotId = 0;
|
|
result = createUnsupportedMessage(this, m.vid.v, mtpCastFlags(m.vflags.v), m.vreply_to_msg_id.v, viaBotId, date(m.vdate), m.vfrom_id.v);
|
|
} else {
|
|
result = HistoryService::create(this, m);
|
|
}
|
|
|
|
if (applyServiceAction) {
|
|
auto &action = m.vaction;
|
|
switch (action.type()) {
|
|
case mtpc_messageActionChatAddUser: {
|
|
auto &d = action.c_messageActionChatAddUser();
|
|
if (peer->isMegagroup()) {
|
|
auto &v = d.vusers.c_vector().v;
|
|
for (auto i = 0, l = v.size(); i != l; ++i) {
|
|
if (auto user = App::userLoaded(peerFromUser(v[i]))) {
|
|
if (peer->asChannel()->mgInfo->lastParticipants.indexOf(user) < 0) {
|
|
peer->asChannel()->mgInfo->lastParticipants.push_front(user);
|
|
peer->asChannel()->mgInfo->lastParticipantsStatus |= MegagroupInfo::LastParticipantsAdminsOutdated;
|
|
Notify::peerUpdatedDelayed(peer, Notify::PeerUpdate::Flag::MembersChanged);
|
|
}
|
|
if (user->botInfo) {
|
|
peer->asChannel()->mgInfo->bots.insert(user);
|
|
if (peer->asChannel()->mgInfo->botStatus != 0 && peer->asChannel()->mgInfo->botStatus < 2) {
|
|
peer->asChannel()->mgInfo->botStatus = 2;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} break;
|
|
|
|
case mtpc_messageActionChatJoinedByLink: {
|
|
auto &d = action.c_messageActionChatJoinedByLink();
|
|
if (peer->isMegagroup()) {
|
|
if (result->from()->isUser()) {
|
|
if (peer->asChannel()->mgInfo->lastParticipants.indexOf(result->from()->asUser()) < 0) {
|
|
peer->asChannel()->mgInfo->lastParticipants.push_front(result->from()->asUser());
|
|
Notify::peerUpdatedDelayed(peer, Notify::PeerUpdate::Flag::MembersChanged);
|
|
}
|
|
if (result->from()->asUser()->botInfo) {
|
|
peer->asChannel()->mgInfo->bots.insert(result->from()->asUser());
|
|
if (peer->asChannel()->mgInfo->botStatus != 0 && peer->asChannel()->mgInfo->botStatus < 2) {
|
|
peer->asChannel()->mgInfo->botStatus = 2;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} break;
|
|
|
|
case mtpc_messageActionChatDeletePhoto: {
|
|
auto chat = peer->asChat();
|
|
if (chat) chat->setPhoto(MTP_chatPhotoEmpty());
|
|
} break;
|
|
|
|
case mtpc_messageActionChatDeleteUser: {
|
|
auto &d = action.c_messageActionChatDeleteUser();
|
|
auto uid = peerFromUser(d.vuser_id);
|
|
if (lastKeyboardFrom == uid) {
|
|
clearLastKeyboard();
|
|
}
|
|
if (peer->isMegagroup()) {
|
|
if (auto user = App::userLoaded(uid)) {
|
|
auto channel = peer->asChannel();
|
|
auto megagroupInfo = channel->mgInfo;
|
|
|
|
int32 index = megagroupInfo->lastParticipants.indexOf(user);
|
|
if (index >= 0) {
|
|
megagroupInfo->lastParticipants.removeAt(index);
|
|
Notify::peerUpdatedDelayed(peer, Notify::PeerUpdate::Flag::MembersChanged);
|
|
}
|
|
if (peer->asChannel()->membersCount() > 1) {
|
|
peer->asChannel()->setMembersCount(channel->membersCount() - 1);
|
|
} else {
|
|
megagroupInfo->lastParticipantsStatus |= MegagroupInfo::LastParticipantsCountOutdated;
|
|
megagroupInfo->lastParticipantsCount = 0;
|
|
}
|
|
if (megagroupInfo->lastAdmins.contains(user)) {
|
|
megagroupInfo->lastAdmins.remove(user);
|
|
if (channel->adminsCount() > 1) {
|
|
channel->setAdminsCount(channel->adminsCount() - 1);
|
|
}
|
|
Notify::peerUpdatedDelayed(peer, Notify::PeerUpdate::Flag::AdminsChanged);
|
|
}
|
|
megagroupInfo->bots.remove(user);
|
|
if (megagroupInfo->bots.isEmpty() && megagroupInfo->botStatus > 0) {
|
|
megagroupInfo->botStatus = -1;
|
|
}
|
|
}
|
|
}
|
|
} break;
|
|
|
|
case mtpc_messageActionChatEditPhoto: {
|
|
const auto &d(action.c_messageActionChatEditPhoto());
|
|
if (d.vphoto.type() == mtpc_photo) {
|
|
const auto &sizes(d.vphoto.c_photo().vsizes.c_vector().v);
|
|
if (!sizes.isEmpty()) {
|
|
PhotoData *photo = App::feedPhoto(d.vphoto.c_photo());
|
|
if (photo) photo->peer = peer;
|
|
const auto &smallSize(sizes.front()), &bigSize(sizes.back());
|
|
const MTPFileLocation *smallLoc = 0, *bigLoc = 0;
|
|
switch (smallSize.type()) {
|
|
case mtpc_photoSize: smallLoc = &smallSize.c_photoSize().vlocation; break;
|
|
case mtpc_photoCachedSize: smallLoc = &smallSize.c_photoCachedSize().vlocation; break;
|
|
}
|
|
switch (bigSize.type()) {
|
|
case mtpc_photoSize: bigLoc = &bigSize.c_photoSize().vlocation; break;
|
|
case mtpc_photoCachedSize: bigLoc = &bigSize.c_photoCachedSize().vlocation; break;
|
|
}
|
|
if (smallLoc && bigLoc) {
|
|
if (peer->isChat()) {
|
|
peer->asChat()->setPhoto(MTP_chatPhoto(*smallLoc, *bigLoc), photo ? photo->id : 0);
|
|
} else if (peer->isChannel()) {
|
|
peer->asChannel()->setPhoto(MTP_chatPhoto(*smallLoc, *bigLoc), photo ? photo->id : 0);
|
|
}
|
|
peer->loadUserpic();
|
|
}
|
|
}
|
|
}
|
|
} break;
|
|
|
|
case mtpc_messageActionChatEditTitle: {
|
|
auto &d = action.c_messageActionChatEditTitle();
|
|
if (auto chat = peer->asChat()) {
|
|
chat->setName(qs(d.vtitle));
|
|
}
|
|
} break;
|
|
|
|
case mtpc_messageActionChatMigrateTo: {
|
|
peer->asChat()->flags |= MTPDchat::Flag::f_deactivated;
|
|
|
|
//auto &d = action.c_messageActionChatMigrateTo();
|
|
//auto channel = App::channelLoaded(d.vchannel_id.v);
|
|
} break;
|
|
|
|
case mtpc_messageActionChannelMigrateFrom: {
|
|
//auto &d = action.c_messageActionChannelMigrateFrom();
|
|
//auto chat = App::chatLoaded(d.vchat_id.v);
|
|
} break;
|
|
|
|
case mtpc_messageActionPinMessage: {
|
|
if (m.has_reply_to_msg_id() && result && result->history()->peer->isMegagroup()) {
|
|
result->history()->peer->asChannel()->mgInfo->pinnedMsgId = m.vreply_to_msg_id.v;
|
|
if (App::main()) emit App::main()->peerUpdated(result->history()->peer);
|
|
}
|
|
} break;
|
|
}
|
|
}
|
|
} break;
|
|
}
|
|
|
|
if (applyServiceAction) {
|
|
App::checkSavedGif(result);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
HistoryItem *History::createItemForwarded(MsgId id, MTPDmessage::Flags flags, QDateTime date, int32 from, HistoryMessage *msg) {
|
|
return HistoryMessage::create(this, id, flags, date, from, msg);
|
|
}
|
|
|
|
HistoryItem *History::createItemDocument(MsgId id, MTPDmessage::Flags flags, int32 viaBotId, MsgId replyTo, QDateTime date, int32 from, DocumentData *doc, const QString &caption, const MTPReplyMarkup &markup) {
|
|
return HistoryMessage::create(this, id, flags, replyTo, viaBotId, date, from, doc, caption, markup);
|
|
}
|
|
|
|
HistoryItem *History::createItemPhoto(MsgId id, MTPDmessage::Flags flags, int32 viaBotId, MsgId replyTo, QDateTime date, int32 from, PhotoData *photo, const QString &caption, const MTPReplyMarkup &markup) {
|
|
return HistoryMessage::create(this, id, flags, replyTo, viaBotId, date, from, photo, caption, markup);
|
|
}
|
|
|
|
HistoryItem *History::createItemGame(MsgId id, MTPDmessage::Flags flags, int32 viaBotId, MsgId replyTo, QDateTime date, int32 from, GameData *game, const MTPReplyMarkup &markup) {
|
|
return HistoryMessage::create(this, id, flags, replyTo, viaBotId, date, from, game, markup);
|
|
}
|
|
|
|
HistoryItem *History::addNewService(MsgId msgId, QDateTime date, const QString &text, MTPDmessage::Flags flags, bool newMsg) {
|
|
return addNewItem(HistoryService::create(this, msgId, date, text, flags), newMsg);
|
|
}
|
|
|
|
HistoryItem *History::addNewMessage(const MTPMessage &msg, NewMessageType type) {
|
|
if (isChannel()) return asChannelHistory()->addNewChannelMessage(msg, type);
|
|
|
|
if (type == NewMessageExisting) return addToHistory(msg);
|
|
if (!loadedAtBottom() || peer->migrateTo()) {
|
|
HistoryItem *item = addToHistory(msg);
|
|
if (item) {
|
|
setLastMessage(item);
|
|
if (type == NewMessageUnread) {
|
|
newItemAdded(item);
|
|
}
|
|
}
|
|
return item;
|
|
}
|
|
|
|
return addNewToLastBlock(msg, type);
|
|
}
|
|
|
|
HistoryItem *History::addNewToLastBlock(const MTPMessage &msg, NewMessageType type) {
|
|
auto applyServiceAction = (type == NewMessageUnread);
|
|
auto detachExistingItem = (type != NewMessageLast);
|
|
auto item = createItem(msg, applyServiceAction, detachExistingItem);
|
|
if (!item || !item->detached()) {
|
|
return item;
|
|
}
|
|
return addNewItem(item, (type == NewMessageUnread));
|
|
}
|
|
|
|
HistoryItem *History::addToHistory(const MTPMessage &msg) {
|
|
return createItem(msg, false, false);
|
|
}
|
|
|
|
HistoryItem *History::addNewForwarded(MsgId id, MTPDmessage::Flags flags, QDateTime date, int32 from, HistoryMessage *item) {
|
|
return addNewItem(createItemForwarded(id, flags, date, from, item), true);
|
|
}
|
|
|
|
HistoryItem *History::addNewDocument(MsgId id, MTPDmessage::Flags flags, int32 viaBotId, MsgId replyTo, QDateTime date, int32 from, DocumentData *doc, const QString &caption, const MTPReplyMarkup &markup) {
|
|
return addNewItem(createItemDocument(id, flags, viaBotId, replyTo, date, from, doc, caption, markup), true);
|
|
}
|
|
|
|
HistoryItem *History::addNewPhoto(MsgId id, MTPDmessage::Flags flags, int32 viaBotId, MsgId replyTo, QDateTime date, int32 from, PhotoData *photo, const QString &caption, const MTPReplyMarkup &markup) {
|
|
return addNewItem(createItemPhoto(id, flags, viaBotId, replyTo, date, from, photo, caption, markup), true);
|
|
}
|
|
|
|
HistoryItem *History::addNewGame(MsgId id, MTPDmessage::Flags flags, int32 viaBotId, MsgId replyTo, QDateTime date, int32 from, GameData *game, const MTPReplyMarkup &markup) {
|
|
return addNewItem(createItemGame(id, flags, viaBotId, replyTo, date, from, game, markup), true);
|
|
}
|
|
|
|
bool History::addToOverview(MediaOverviewType type, MsgId msgId, AddToOverviewMethod method) {
|
|
bool adding = false;
|
|
switch (method) {
|
|
case AddToOverviewNew:
|
|
case AddToOverviewFront: adding = (overviewIds[type].constFind(msgId) == overviewIds[type].cend()); break;
|
|
case AddToOverviewBack: adding = (overviewCountData[type] != 0); break;
|
|
}
|
|
if (!adding) return false;
|
|
|
|
overviewIds[type].insert(msgId);
|
|
switch (method) {
|
|
case AddToOverviewNew:
|
|
case AddToOverviewBack: overview[type].push_back(msgId); break;
|
|
case AddToOverviewFront: overview[type].push_front(msgId); break;
|
|
}
|
|
if (method == AddToOverviewNew) {
|
|
if (overviewCountData[type] > 0) {
|
|
++overviewCountData[type];
|
|
}
|
|
Notify::mediaOverviewUpdated(peer, type);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void History::eraseFromOverview(MediaOverviewType type, MsgId msgId) {
|
|
if (overviewIds[type].isEmpty()) return;
|
|
|
|
auto i = overviewIds[type].find(msgId);
|
|
if (i == overviewIds[type].cend()) return;
|
|
|
|
overviewIds[type].erase(i);
|
|
for (auto i = overview[type].begin(), e = overview[type].end(); i != e; ++i) {
|
|
if ((*i) == msgId) {
|
|
overview[type].erase(i);
|
|
if (overviewCountData[type] > 0) {
|
|
--overviewCountData[type];
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
Notify::mediaOverviewUpdated(peer, type);
|
|
}
|
|
|
|
HistoryItem *History::addNewItem(HistoryItem *adding, bool newMsg) {
|
|
t_assert(!isBuildingFrontBlock());
|
|
addItemToBlock(adding);
|
|
|
|
setLastMessage(adding);
|
|
if (newMsg) {
|
|
newItemAdded(adding);
|
|
}
|
|
|
|
adding->addToOverview(AddToOverviewNew);
|
|
if (adding->from()->id) {
|
|
if (adding->from()->isUser()) {
|
|
QList<UserData*> *lastAuthors = 0;
|
|
if (peer->isChat()) {
|
|
lastAuthors = &peer->asChat()->lastAuthors;
|
|
} else if (peer->isMegagroup()) {
|
|
lastAuthors = &peer->asChannel()->mgInfo->lastParticipants;
|
|
if (adding->from()->asUser()->botInfo) {
|
|
peer->asChannel()->mgInfo->bots.insert(adding->from()->asUser());
|
|
if (peer->asChannel()->mgInfo->botStatus != 0 && peer->asChannel()->mgInfo->botStatus < 2) {
|
|
peer->asChannel()->mgInfo->botStatus = 2;
|
|
}
|
|
}
|
|
}
|
|
if (lastAuthors) {
|
|
int prev = lastAuthors->indexOf(adding->from()->asUser());
|
|
if (prev > 0) {
|
|
lastAuthors->removeAt(prev);
|
|
} else if (prev < 0 && peer->isMegagroup()) { // nothing is outdated if just reordering
|
|
peer->asChannel()->mgInfo->lastParticipantsStatus |= MegagroupInfo::LastParticipantsAdminsOutdated;
|
|
}
|
|
if (prev) {
|
|
lastAuthors->push_front(adding->from()->asUser());
|
|
}
|
|
if (peer->isMegagroup()) {
|
|
Notify::peerUpdatedDelayed(peer, Notify::PeerUpdate::Flag::MembersChanged);
|
|
}
|
|
}
|
|
}
|
|
if (adding->definesReplyKeyboard()) {
|
|
MTPDreplyKeyboardMarkup::Flags markupFlags = adding->replyKeyboardFlags();
|
|
if (!(markupFlags & MTPDreplyKeyboardMarkup::Flag::f_selective) || adding->mentionsMe()) {
|
|
OrderedSet<PeerData*> *markupSenders = 0;
|
|
if (peer->isChat()) {
|
|
markupSenders = &peer->asChat()->markupSenders;
|
|
} else if (peer->isMegagroup()) {
|
|
markupSenders = &peer->asChannel()->mgInfo->markupSenders;
|
|
}
|
|
if (markupSenders) {
|
|
markupSenders->insert(adding->from());
|
|
}
|
|
if (markupFlags & MTPDreplyKeyboardMarkup_ClientFlag::f_zero) { // zero markup means replyKeyboardHide
|
|
if (lastKeyboardFrom == adding->from()->id || (!lastKeyboardInited && !peer->isChat() && !peer->isMegagroup() && !adding->out())) {
|
|
clearLastKeyboard();
|
|
}
|
|
} else {
|
|
bool botNotInChat = false;
|
|
if (peer->isChat()) {
|
|
botNotInChat = adding->from()->isUser() && (!peer->canWrite() || !peer->asChat()->participants.isEmpty()) && !peer->asChat()->participants.contains(adding->from()->asUser());
|
|
} else if (peer->isMegagroup()) {
|
|
botNotInChat = adding->from()->isUser() && (!peer->canWrite() || peer->asChannel()->mgInfo->botStatus != 0) && !peer->asChannel()->mgInfo->bots.contains(adding->from()->asUser());
|
|
}
|
|
if (botNotInChat) {
|
|
clearLastKeyboard();
|
|
} else {
|
|
lastKeyboardInited = true;
|
|
lastKeyboardId = adding->id;
|
|
lastKeyboardFrom = adding->from()->id;
|
|
lastKeyboardUsed = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return adding;
|
|
}
|
|
|
|
void History::unregSendAction(UserData *from) {
|
|
auto updateAtMs = TimeMs(0);
|
|
auto i = _typing.find(from);
|
|
if (i != _typing.cend()) {
|
|
updateAtMs = getms();
|
|
i.value() = updateAtMs;
|
|
}
|
|
auto j = _sendActions.find(from);
|
|
if (j != _sendActions.cend()) {
|
|
if (!updateAtMs) updateAtMs = getms();
|
|
j.value().until = updateAtMs;
|
|
}
|
|
if (updateAtMs) {
|
|
updateSendActionNeedsAnimating(updateAtMs, true);
|
|
}
|
|
}
|
|
|
|
void History::newItemAdded(HistoryItem *item) {
|
|
App::checkImageCacheSize();
|
|
if (item->from() && item->from()->isUser()) {
|
|
if (item->from() == item->author()) {
|
|
unregSendAction(item->from()->asUser());
|
|
}
|
|
MTPint itemServerTime;
|
|
toServerTime(item->date.toTime_t(), itemServerTime);
|
|
item->from()->asUser()->madeAction(itemServerTime.v);
|
|
}
|
|
if (item->out()) {
|
|
if (unreadBar) unreadBar->destroyUnreadBar();
|
|
if (!item->unread()) {
|
|
outboxRead(item);
|
|
}
|
|
} else if (item->unread()) {
|
|
if (!isChannel() || peer->asChannel()->amIn()) {
|
|
notifies.push_back(item);
|
|
App::main()->newUnreadMsg(this, item);
|
|
}
|
|
} else if (!item->isGroupMigrate() || !peer->isMegagroup()) {
|
|
inboxRead(item);
|
|
}
|
|
}
|
|
|
|
HistoryBlock *History::prepareBlockForAddingItem() {
|
|
if (isBuildingFrontBlock()) {
|
|
if (_buildingFrontBlock->block) {
|
|
return _buildingFrontBlock->block;
|
|
}
|
|
|
|
auto result = _buildingFrontBlock->block = new HistoryBlock(this);
|
|
if (_buildingFrontBlock->expectedItemsCount > 0) {
|
|
result->items.reserve(_buildingFrontBlock->expectedItemsCount + 1);
|
|
}
|
|
result->setIndexInHistory(0);
|
|
blocks.push_front(result);
|
|
for (int i = 1, l = blocks.size(); i < l; ++i) {
|
|
blocks.at(i)->setIndexInHistory(i);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
bool addNewBlock = blocks.isEmpty() || (blocks.back()->items.size() >= MessagesPerPage);
|
|
if (!addNewBlock) {
|
|
return blocks.back();
|
|
}
|
|
|
|
auto result = new HistoryBlock(this);
|
|
result->setIndexInHistory(blocks.size());
|
|
blocks.push_back(result);
|
|
|
|
result->items.reserve(MessagesPerPage);
|
|
return result;
|
|
};
|
|
|
|
void History::addItemToBlock(HistoryItem *item) {
|
|
t_assert(item != nullptr);
|
|
t_assert(item->detached());
|
|
|
|
auto block = prepareBlockForAddingItem();
|
|
|
|
item->attachToBlock(block, block->items.size());
|
|
block->items.push_back(item);
|
|
item->previousItemChanged();
|
|
|
|
if (isBuildingFrontBlock() && _buildingFrontBlock->expectedItemsCount > 0) {
|
|
--_buildingFrontBlock->expectedItemsCount;
|
|
}
|
|
}
|
|
|
|
void History::addOlderSlice(const QVector<MTPMessage> &slice) {
|
|
if (slice.isEmpty()) {
|
|
oldLoaded = true;
|
|
if (isChannel()) {
|
|
asChannelHistory()->checkJoinedMessage();
|
|
asChannelHistory()->checkMaxReadMessageDate();
|
|
}
|
|
return;
|
|
}
|
|
|
|
startBuildingFrontBlock(slice.size());
|
|
|
|
for (auto i = slice.cend(), e = slice.cbegin(); i != e;) {
|
|
--i;
|
|
auto adding = createItem(*i, false, true);
|
|
if (!adding) continue;
|
|
|
|
addItemToBlock(adding);
|
|
}
|
|
|
|
auto block = finishBuildingFrontBlock();
|
|
if (!block) {
|
|
// If no items were added it means we've loaded everything old.
|
|
oldLoaded = true;
|
|
} else if (loadedAtBottom()) { // add photos to overview and authors to lastAuthors
|
|
bool channel = isChannel();
|
|
int32 mask = 0;
|
|
QList<UserData*> *lastAuthors = nullptr;
|
|
OrderedSet<PeerData*> *markupSenders = nullptr;
|
|
if (peer->isChat()) {
|
|
lastAuthors = &peer->asChat()->lastAuthors;
|
|
markupSenders = &peer->asChat()->markupSenders;
|
|
} else if (peer->isMegagroup()) {
|
|
// We don't add users to mgInfo->lastParticipants here.
|
|
// We're scrolling back and we see messages from users that
|
|
// could be gone from the megagroup already. It is fine for
|
|
// chat->lastAuthors, because they're used only for field
|
|
// autocomplete, but this is bad for megagroups, because its
|
|
// lastParticipants are displayed in Profile as members list.
|
|
markupSenders = &peer->asChannel()->mgInfo->markupSenders;
|
|
}
|
|
for (int32 i = block->items.size(); i > 0; --i) {
|
|
auto item = block->items[i - 1];
|
|
mask |= item->addToOverview(AddToOverviewFront);
|
|
if (item->from()->id) {
|
|
if (lastAuthors) { // chats
|
|
if (auto user = item->from()->asUser()) {
|
|
if (!lastAuthors->contains(user)) {
|
|
lastAuthors->push_back(user);
|
|
if (peer->isMegagroup()) {
|
|
peer->asChannel()->mgInfo->lastParticipantsStatus |= MegagroupInfo::LastParticipantsAdminsOutdated;
|
|
Notify::peerUpdatedDelayed(peer, Notify::PeerUpdate::Flag::MembersChanged);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (item->author()->id) {
|
|
if (markupSenders) { // chats with bots
|
|
if (!lastKeyboardInited && item->definesReplyKeyboard() && !item->out()) {
|
|
auto markupFlags = item->replyKeyboardFlags();
|
|
if (!(markupFlags & MTPDreplyKeyboardMarkup::Flag::f_selective) || item->mentionsMe()) {
|
|
bool wasKeyboardHide = markupSenders->contains(item->author());
|
|
if (!wasKeyboardHide) {
|
|
markupSenders->insert(item->author());
|
|
}
|
|
if (!(markupFlags & MTPDreplyKeyboardMarkup_ClientFlag::f_zero)) {
|
|
if (!lastKeyboardInited) {
|
|
bool botNotInChat = false;
|
|
if (peer->isChat()) {
|
|
botNotInChat = (!peer->canWrite() || !peer->asChat()->participants.isEmpty()) && item->author()->isUser() && !peer->asChat()->participants.contains(item->author()->asUser());
|
|
} else if (peer->isMegagroup()) {
|
|
botNotInChat = (!peer->canWrite() || peer->asChannel()->mgInfo->botStatus != 0) && item->author()->isUser() && !peer->asChannel()->mgInfo->bots.contains(item->author()->asUser());
|
|
}
|
|
if (wasKeyboardHide || botNotInChat) {
|
|
clearLastKeyboard();
|
|
} else {
|
|
lastKeyboardInited = true;
|
|
lastKeyboardId = item->id;
|
|
lastKeyboardFrom = item->author()->id;
|
|
lastKeyboardUsed = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if (!lastKeyboardInited && item->definesReplyKeyboard() && !item->out()) { // conversations with bots
|
|
MTPDreplyKeyboardMarkup::Flags markupFlags = item->replyKeyboardFlags();
|
|
if (!(markupFlags & MTPDreplyKeyboardMarkup::Flag::f_selective) || item->mentionsMe()) {
|
|
if (markupFlags & MTPDreplyKeyboardMarkup_ClientFlag::f_zero) {
|
|
clearLastKeyboard();
|
|
} else {
|
|
lastKeyboardInited = true;
|
|
lastKeyboardId = item->id;
|
|
lastKeyboardFrom = item->author()->id;
|
|
lastKeyboardUsed = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (mask) {
|
|
Notify::PeerUpdate update(peer);
|
|
update.flags |= Notify::PeerUpdate::Flag::SharedMediaChanged;
|
|
update.mediaTypesMask |= mask;
|
|
Notify::peerUpdatedDelayed(update);
|
|
}
|
|
}
|
|
|
|
if (isChannel()) {
|
|
asChannelHistory()->checkJoinedMessage();
|
|
asChannelHistory()->checkMaxReadMessageDate();
|
|
}
|
|
checkLastMsg();
|
|
}
|
|
|
|
void History::addNewerSlice(const QVector<MTPMessage> &slice) {
|
|
bool wasEmpty = isEmpty(), wasLoadedAtBottom = loadedAtBottom();
|
|
|
|
if (slice.isEmpty()) {
|
|
newLoaded = true;
|
|
if (!lastMsg) {
|
|
setLastMessage(lastAvailableMessage());
|
|
}
|
|
}
|
|
|
|
t_assert(!isBuildingFrontBlock());
|
|
if (!slice.isEmpty()) {
|
|
bool atLeastOneAdded = false;
|
|
for (auto i = slice.cend(), e = slice.cbegin(); i != e;) {
|
|
--i;
|
|
auto adding = createItem(*i, false, true);
|
|
if (!adding) continue;
|
|
|
|
addItemToBlock(adding);
|
|
atLeastOneAdded = true;
|
|
}
|
|
|
|
if (!atLeastOneAdded) {
|
|
newLoaded = true;
|
|
setLastMessage(lastAvailableMessage());
|
|
}
|
|
}
|
|
|
|
if (!wasLoadedAtBottom) {
|
|
checkAddAllToOverview();
|
|
}
|
|
|
|
if (isChannel()) asChannelHistory()->checkJoinedMessage();
|
|
checkLastMsg();
|
|
}
|
|
|
|
void History::checkLastMsg() {
|
|
if (lastMsg) {
|
|
if (!newLoaded && !lastMsg->detached()) {
|
|
newLoaded = true;
|
|
checkAddAllToOverview();
|
|
}
|
|
} else if (newLoaded) {
|
|
setLastMessage(lastAvailableMessage());
|
|
}
|
|
}
|
|
|
|
void History::checkAddAllToOverview() {
|
|
if (!loadedAtBottom()) {
|
|
return;
|
|
}
|
|
|
|
int32 mask = 0;
|
|
for (int32 i = 0; i < OverviewCount; ++i) {
|
|
if (overviewCountData[i] == 0) continue; // all loaded
|
|
if (!overview[i].isEmpty() || !overviewIds[i].isEmpty()) {
|
|
overview[i].clear();
|
|
overviewIds[i].clear();
|
|
mask |= (1 << i);
|
|
}
|
|
}
|
|
for_const (HistoryBlock *block, blocks) {
|
|
for_const (HistoryItem *item, block->items) {
|
|
mask |= item->addToOverview(AddToOverviewBack);
|
|
}
|
|
}
|
|
if (mask) {
|
|
Notify::PeerUpdate update(peer);
|
|
update.flags |= Notify::PeerUpdate::Flag::SharedMediaChanged;
|
|
update.mediaTypesMask |= mask;
|
|
Notify::peerUpdatedDelayed(update);
|
|
}
|
|
}
|
|
|
|
int History::countUnread(MsgId upTo) {
|
|
int result = 0;
|
|
for (auto i = blocks.cend(), e = blocks.cbegin(); i != e;) {
|
|
--i;
|
|
for (auto j = (*i)->items.cend(), en = (*i)->items.cbegin(); j != en;) {
|
|
--j;
|
|
if ((*j)->id > 0 && (*j)->id <= upTo) {
|
|
break;
|
|
} else if (!(*j)->out() && (*j)->unread() && (*j)->id > upTo) {
|
|
++result;
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void History::updateShowFrom() {
|
|
if (showFrom) return;
|
|
|
|
for (auto i = blocks.cend(); i != blocks.cbegin();) {
|
|
--i;
|
|
for (auto j = (*i)->items.cend(); j != (*i)->items.cbegin();) {
|
|
--j;
|
|
if ((*j)->id > 0 && (!(*j)->out() || !showFrom)) {
|
|
if ((*j)->id >= inboxReadBefore) {
|
|
showFrom = *j;
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
MsgId History::inboxRead(MsgId upTo) {
|
|
if (upTo < 0) return upTo;
|
|
if (unreadCount()) {
|
|
if (upTo && loadedAtBottom()) App::main()->historyToDown(this);
|
|
setUnreadCount(upTo ? countUnread(upTo) : 0);
|
|
}
|
|
|
|
if (!upTo) upTo = msgIdForRead();
|
|
accumulate_max(inboxReadBefore, upTo + 1);
|
|
|
|
updateChatListEntry();
|
|
if (peer->migrateTo()) {
|
|
if (auto migrateTo = App::historyLoaded(peer->migrateTo()->id)) {
|
|
migrateTo->updateChatListEntry();
|
|
}
|
|
}
|
|
|
|
showFrom = nullptr;
|
|
App::wnd()->notifyClear(this);
|
|
|
|
return upTo;
|
|
}
|
|
|
|
MsgId History::inboxRead(HistoryItem *wasRead) {
|
|
return inboxRead(wasRead ? wasRead->id : 0);
|
|
}
|
|
|
|
MsgId History::outboxRead(int32 upTo) {
|
|
if (upTo < 0) return upTo;
|
|
if (!upTo) upTo = msgIdForRead();
|
|
accumulate_max(outboxReadBefore, upTo + 1);
|
|
|
|
return upTo;
|
|
}
|
|
|
|
MsgId History::outboxRead(HistoryItem *wasRead) {
|
|
return outboxRead(wasRead ? wasRead->id : 0);
|
|
}
|
|
|
|
HistoryItem *History::lastAvailableMessage() const {
|
|
return isEmpty() ? nullptr : blocks.back()->items.back();
|
|
}
|
|
|
|
void History::setUnreadCount(int newUnreadCount) {
|
|
if (_unreadCount != newUnreadCount) {
|
|
if (newUnreadCount == 1) {
|
|
if (loadedAtBottom()) showFrom = lastAvailableMessage();
|
|
inboxReadBefore = qMax(inboxReadBefore, msgIdForRead());
|
|
} else if (!newUnreadCount) {
|
|
showFrom = nullptr;
|
|
inboxReadBefore = qMax(inboxReadBefore, msgIdForRead() + 1);
|
|
} else {
|
|
if (!showFrom && !unreadBar && loadedAtBottom()) updateShowFrom();
|
|
}
|
|
if (inChatList(Dialogs::Mode::All)) {
|
|
App::histories().unreadIncrement(newUnreadCount - _unreadCount, mute());
|
|
if (!mute() || Global::IncludeMuted()) {
|
|
Notify::unreadCounterUpdated();
|
|
}
|
|
}
|
|
_unreadCount = newUnreadCount;
|
|
if (auto main = App::main()) {
|
|
main->unreadCountChanged(this);
|
|
}
|
|
if (unreadBar) {
|
|
int32 count = _unreadCount;
|
|
if (peer->migrateTo()) {
|
|
if (History *h = App::historyLoaded(peer->migrateTo()->id)) {
|
|
count += h->unreadCount();
|
|
}
|
|
}
|
|
if (count > 0) {
|
|
unreadBar->setUnreadBarCount(count);
|
|
} else {
|
|
unreadBar->setUnreadBarFreezed();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void History::setMute(bool newMute) {
|
|
if (_mute != newMute) {
|
|
_mute = newMute;
|
|
if (inChatList(Dialogs::Mode::All)) {
|
|
if (_unreadCount) {
|
|
App::histories().unreadMuteChanged(_unreadCount, newMute);
|
|
Notify::unreadCounterUpdated();
|
|
}
|
|
Notify::historyMuteUpdated(this);
|
|
}
|
|
updateChatListEntry();
|
|
}
|
|
}
|
|
|
|
void History::getNextShowFrom(HistoryBlock *block, int i) {
|
|
if (i >= 0) {
|
|
auto l = block->items.size();
|
|
for (++i; i < l; ++i) {
|
|
if (block->items[i]->id > 0) {
|
|
showFrom = block->items.at(i);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (auto j = block->indexInHistory() + 1, s = blocks.size(); j < s; ++j) {
|
|
block = blocks.at(j);
|
|
for_const (auto item, block->items) {
|
|
if (item->id > 0) {
|
|
showFrom = item;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
showFrom = nullptr;
|
|
}
|
|
|
|
void History::countScrollState(int top) {
|
|
countScrollTopItem(top);
|
|
if (scrollTopItem) {
|
|
scrollTopOffset = (top - scrollTopItem->block()->y - scrollTopItem->y);
|
|
}
|
|
}
|
|
|
|
void History::countScrollTopItem(int top) {
|
|
if (isEmpty()) {
|
|
forgetScrollState();
|
|
return;
|
|
}
|
|
|
|
int itemIndex = 0, blockIndex = 0, itemTop = 0;
|
|
if (scrollTopItem && !scrollTopItem->detached()) {
|
|
itemIndex = scrollTopItem->indexInBlock();
|
|
blockIndex = scrollTopItem->block()->indexInHistory();
|
|
itemTop = blocks.at(blockIndex)->y + scrollTopItem->y;
|
|
}
|
|
if (itemTop > top) {
|
|
// go backward through history while we don't find an item that starts above
|
|
do {
|
|
HistoryBlock *block = blocks.at(blockIndex);
|
|
for (--itemIndex; itemIndex >= 0; --itemIndex) {
|
|
HistoryItem *item = block->items.at(itemIndex);
|
|
itemTop = block->y + item->y;
|
|
if (itemTop <= top) {
|
|
scrollTopItem = item;
|
|
return;
|
|
}
|
|
}
|
|
if (--blockIndex >= 0) {
|
|
itemIndex = blocks.at(blockIndex)->items.size();
|
|
} else {
|
|
break;
|
|
}
|
|
} while (true);
|
|
|
|
scrollTopItem = blocks.front()->items.front();
|
|
} else {
|
|
// go forward through history while we don't find the last item that starts above
|
|
for (int blocksCount = blocks.size(); blockIndex < blocksCount; ++blockIndex) {
|
|
HistoryBlock *block = blocks.at(blockIndex);
|
|
for (int itemsCount = block->items.size(); itemIndex < itemsCount; ++itemIndex) {
|
|
HistoryItem *item = block->items.at(itemIndex);
|
|
itemTop = block->y + item->y;
|
|
if (itemTop > top) {
|
|
t_assert(itemIndex > 0 || blockIndex > 0);
|
|
if (itemIndex > 0) {
|
|
scrollTopItem = block->items.at(itemIndex - 1);
|
|
} else {
|
|
scrollTopItem = blocks.at(blockIndex - 1)->items.back();
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
itemIndex = 0;
|
|
}
|
|
scrollTopItem = blocks.back()->items.back();
|
|
}
|
|
}
|
|
|
|
void History::getNextScrollTopItem(HistoryBlock *block, int32 i) {
|
|
++i;
|
|
if (i > 0 && i < block->items.size()) {
|
|
scrollTopItem = block->items.at(i);
|
|
return;
|
|
}
|
|
int j = block->indexInHistory() + 1;
|
|
if (j > 0 && j < blocks.size()) {
|
|
scrollTopItem = blocks.at(j)->items.front();
|
|
return;
|
|
}
|
|
scrollTopItem = nullptr;
|
|
}
|
|
|
|
void History::addUnreadBar() {
|
|
if (unreadBar || !showFrom || showFrom->detached() || !unreadCount()) return;
|
|
|
|
int32 count = unreadCount();
|
|
if (peer->migrateTo()) {
|
|
if (History *h = App::historyLoaded(peer->migrateTo()->id)) {
|
|
count += h->unreadCount();
|
|
}
|
|
}
|
|
showFrom->setUnreadBarCount(count);
|
|
unreadBar = showFrom;
|
|
}
|
|
|
|
void History::destroyUnreadBar() {
|
|
if (unreadBar) {
|
|
unreadBar->destroyUnreadBar();
|
|
}
|
|
}
|
|
|
|
HistoryItem *History::addNewInTheMiddle(HistoryItem *newItem, int32 blockIndex, int32 itemIndex) {
|
|
t_assert(blockIndex >= 0);
|
|
t_assert(blockIndex < blocks.size());
|
|
t_assert(itemIndex >= 0);
|
|
t_assert(itemIndex <= blocks[blockIndex]->items.size());
|
|
|
|
auto block = blocks.at(blockIndex);
|
|
|
|
newItem->attachToBlock(block, itemIndex);
|
|
block->items.insert(itemIndex, newItem);
|
|
newItem->previousItemChanged();
|
|
if (itemIndex + 1 < block->items.size()) {
|
|
for (int i = itemIndex + 1, l = block->items.size(); i < l; ++i) {
|
|
block->items[i]->setIndexInBlock(i);
|
|
}
|
|
block->items[itemIndex + 1]->previousItemChanged();
|
|
} else if (blockIndex + 1 < blocks.size() && !blocks[blockIndex + 1]->items.empty()) {
|
|
blocks[blockIndex + 1]->items.front()->previousItemChanged();
|
|
} else {
|
|
newItem->nextItemChanged();
|
|
}
|
|
|
|
return newItem;
|
|
}
|
|
|
|
void History::startBuildingFrontBlock(int expectedItemsCount) {
|
|
t_assert(!isBuildingFrontBlock());
|
|
t_assert(expectedItemsCount > 0);
|
|
|
|
_buildingFrontBlock.reset(new BuildingBlock());
|
|
_buildingFrontBlock->expectedItemsCount = expectedItemsCount;
|
|
}
|
|
|
|
HistoryBlock *History::finishBuildingFrontBlock() {
|
|
t_assert(isBuildingFrontBlock());
|
|
|
|
// Some checks if there was some message history already
|
|
auto block = _buildingFrontBlock->block;
|
|
if (block) {
|
|
if (blocks.size() > 1) {
|
|
auto last = block->items.back(); // ... item, item, item, last ], [ first, item, item ...
|
|
auto first = blocks.at(1)->items.front();
|
|
|
|
// we've added a new front block, so previous item for
|
|
// the old first item of a first block was changed
|
|
first->previousItemChanged();
|
|
} else {
|
|
block->items.back()->nextItemChanged();
|
|
}
|
|
}
|
|
|
|
_buildingFrontBlock = nullptr;
|
|
return block;
|
|
}
|
|
|
|
void History::clearNotifications() {
|
|
notifies.clear();
|
|
}
|
|
|
|
bool History::loadedAtBottom() const {
|
|
return newLoaded;
|
|
}
|
|
|
|
bool History::loadedAtTop() const {
|
|
return oldLoaded;
|
|
}
|
|
|
|
bool History::isReadyFor(MsgId msgId) {
|
|
if (msgId < 0 && -msgId < ServerMaxMsgId && peer->migrateFrom()) { // old group history
|
|
return App::history(peer->migrateFrom()->id)->isReadyFor(-msgId);
|
|
}
|
|
|
|
if (msgId == ShowAtTheEndMsgId) {
|
|
return loadedAtBottom();
|
|
}
|
|
if (msgId == ShowAtUnreadMsgId) {
|
|
if (peer->migrateFrom()) { // old group history
|
|
if (History *h = App::historyLoaded(peer->migrateFrom()->id)) {
|
|
if (h->unreadCount()) {
|
|
return h->isReadyFor(msgId);
|
|
}
|
|
}
|
|
}
|
|
if (unreadCount()) {
|
|
if (!isEmpty()) {
|
|
return (loadedAtTop() || minMsgId() <= inboxReadBefore) && (loadedAtBottom() || maxMsgId() >= inboxReadBefore);
|
|
}
|
|
return false;
|
|
}
|
|
return loadedAtBottom();
|
|
}
|
|
HistoryItem *item = App::histItemById(channelId(), msgId);
|
|
return item && (item->history() == this) && !item->detached();
|
|
}
|
|
|
|
void History::getReadyFor(MsgId msgId) {
|
|
if (msgId < 0 && -msgId < ServerMaxMsgId && peer->migrateFrom()) {
|
|
History *h = App::history(peer->migrateFrom()->id);
|
|
h->getReadyFor(-msgId);
|
|
if (h->isEmpty()) {
|
|
clear(true);
|
|
}
|
|
return;
|
|
}
|
|
if (msgId == ShowAtUnreadMsgId && peer->migrateFrom()) {
|
|
if (History *h = App::historyLoaded(peer->migrateFrom()->id)) {
|
|
if (h->unreadCount()) {
|
|
clear(true);
|
|
h->getReadyFor(msgId);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
if (!isReadyFor(msgId)) {
|
|
clear(true);
|
|
|
|
if (msgId == ShowAtTheEndMsgId) {
|
|
newLoaded = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
void History::setNotLoadedAtBottom() {
|
|
newLoaded = false;
|
|
}
|
|
|
|
namespace {
|
|
uint32 _dialogsPosToTopShift = 0x80000000UL;
|
|
}
|
|
|
|
inline uint64 dialogPosFromDate(const QDateTime &date) {
|
|
if (date.isNull()) return 0;
|
|
return (uint64(date.toTime_t()) << 32) | (++_dialogsPosToTopShift);
|
|
}
|
|
|
|
inline uint64 pinnedDialogPos(int pinnedIndex) {
|
|
return 0xFFFFFFFF00000000ULL + pinnedIndex;
|
|
}
|
|
|
|
void History::setLastMessage(HistoryItem *msg) {
|
|
if (msg) {
|
|
if (!lastMsg) Local::removeSavedPeer(peer);
|
|
lastMsg = msg;
|
|
setChatsListDate(msg->date);
|
|
} else {
|
|
lastMsg = 0;
|
|
updateChatListEntry();
|
|
}
|
|
}
|
|
|
|
bool History::needUpdateInChatList() const {
|
|
if (inChatList(Dialogs::Mode::All) || isPinnedDialog()) {
|
|
return true;
|
|
} else if (peer->migrateTo()) {
|
|
return false;
|
|
}
|
|
return (!peer->isChannel() || peer->asChannel()->amIn());
|
|
}
|
|
|
|
void History::setChatsListDate(const QDateTime &date) {
|
|
if (!lastMsgDate.isNull() && lastMsgDate >= date) {
|
|
if (!needUpdateInChatList() || !inChatList(Dialogs::Mode::All)) {
|
|
return;
|
|
}
|
|
}
|
|
lastMsgDate = date;
|
|
updateChatListSortPosition();
|
|
}
|
|
|
|
void History::updateChatListSortPosition() {
|
|
auto chatListDate = [this]() {
|
|
if (auto draft = cloudDraft()) {
|
|
if (!Data::draftIsNull(draft) && draft->date > lastMsgDate) {
|
|
return draft->date;
|
|
}
|
|
}
|
|
return lastMsgDate;
|
|
};
|
|
|
|
_sortKeyInChatList = isPinnedDialog() ? pinnedDialogPos(_pinnedIndex) : dialogPosFromDate(chatListDate());
|
|
if (auto m = App::main()) {
|
|
if (needUpdateInChatList()) {
|
|
if (_sortKeyInChatList) {
|
|
m->createDialog(this);
|
|
updateChatListEntry();
|
|
} else {
|
|
m->deleteConversation(peer, false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void History::fixLastMessage(bool wasAtBottom) {
|
|
setLastMessage(wasAtBottom ? lastAvailableMessage() : 0);
|
|
}
|
|
|
|
MsgId History::minMsgId() const {
|
|
for_const (const HistoryBlock *block, blocks) {
|
|
for_const (const HistoryItem *item, block->items) {
|
|
if (item->id > 0) {
|
|
return item->id;
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
MsgId History::maxMsgId() const {
|
|
for (auto i = blocks.cend(), e = blocks.cbegin(); i != e;) {
|
|
--i;
|
|
for (auto j = (*i)->items.cend(), en = (*i)->items.cbegin(); j != en;) {
|
|
--j;
|
|
if ((*j)->id > 0) {
|
|
return (*j)->id;
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
MsgId History::msgIdForRead() const {
|
|
MsgId result = (lastMsg && lastMsg->id > 0) ? lastMsg->id : 0;
|
|
if (loadedAtBottom()) result = qMax(result, maxMsgId());
|
|
return result;
|
|
}
|
|
|
|
int History::resizeGetHeight(int newWidth) {
|
|
bool resizeAllItems = (_flags & Flag::f_pending_resize) || (width != newWidth);
|
|
|
|
if (!resizeAllItems && !hasPendingResizedItems()) {
|
|
return height;
|
|
}
|
|
_flags &= ~(Flag::f_pending_resize | Flag::f_has_pending_resized_items);
|
|
|
|
width = newWidth;
|
|
int y = 0;
|
|
for_const (HistoryBlock *block, blocks) {
|
|
block->y = y;
|
|
y += block->resizeGetHeight(newWidth, resizeAllItems);
|
|
}
|
|
height = y;
|
|
return height;
|
|
}
|
|
|
|
ChannelHistory *History::asChannelHistory() {
|
|
return isChannel() ? static_cast<ChannelHistory*>(this) : 0;
|
|
}
|
|
|
|
const ChannelHistory *History::asChannelHistory() const {
|
|
return isChannel() ? static_cast<const ChannelHistory*>(this) : 0;
|
|
}
|
|
|
|
bool History::isDisplayedEmpty() const {
|
|
return isEmpty() || ((blocks.size() == 1) && blocks.front()->items.size() == 1 && blocks.front()->items.front()->isEmpty());
|
|
}
|
|
|
|
void History::clear(bool leaveItems) {
|
|
if (unreadBar) {
|
|
unreadBar = nullptr;
|
|
}
|
|
if (showFrom) {
|
|
showFrom = nullptr;
|
|
}
|
|
if (lastSentMsg) {
|
|
lastSentMsg = nullptr;
|
|
}
|
|
if (scrollTopItem) {
|
|
forgetScrollState();
|
|
}
|
|
if (!leaveItems) {
|
|
setLastMessage(nullptr);
|
|
notifies.clear();
|
|
auto &pending = Global::RefPendingRepaintItems();
|
|
for (auto i = pending.begin(); i != pending.end();) {
|
|
if ((*i)->history() == this) {
|
|
i = pending.erase(i);
|
|
} else {
|
|
++i;
|
|
}
|
|
}
|
|
}
|
|
for (int32 i = 0; i < OverviewCount; ++i) {
|
|
if (!overview[i].isEmpty() || !overviewIds[i].isEmpty()) {
|
|
if (leaveItems) {
|
|
if (overviewCountData[i] == 0) {
|
|
overviewCountData[i] = overview[i].size();
|
|
}
|
|
} else {
|
|
overviewCountData[i] = -1; // not loaded yet
|
|
}
|
|
overview[i].clear();
|
|
overviewIds[i].clear();
|
|
if (!App::quitting()) Notify::mediaOverviewUpdated(peer, MediaOverviewType(i));
|
|
}
|
|
}
|
|
clearBlocks(leaveItems);
|
|
if (leaveItems) {
|
|
lastKeyboardInited = false;
|
|
} else {
|
|
setUnreadCount(0);
|
|
if (peer->isMegagroup()) {
|
|
peer->asChannel()->mgInfo->pinnedMsgId = 0;
|
|
}
|
|
clearLastKeyboard();
|
|
}
|
|
setPendingResize();
|
|
|
|
newLoaded = oldLoaded = false;
|
|
forgetScrollState();
|
|
|
|
if (peer->isChat()) {
|
|
peer->asChat()->lastAuthors.clear();
|
|
peer->asChat()->markupSenders.clear();
|
|
} else if (isChannel()) {
|
|
asChannelHistory()->cleared(leaveItems);
|
|
if (isMegagroup()) {
|
|
peer->asChannel()->mgInfo->markupSenders.clear();
|
|
}
|
|
}
|
|
if (leaveItems && App::main()) App::main()->historyCleared(this);
|
|
}
|
|
|
|
void History::clearBlocks(bool leaveItems) {
|
|
Blocks lst;
|
|
std::swap(lst, blocks);
|
|
for_const (HistoryBlock *block, lst) {
|
|
if (leaveItems) {
|
|
block->clear(true);
|
|
}
|
|
delete block;
|
|
}
|
|
}
|
|
|
|
void History::clearOnDestroy() {
|
|
clearBlocks(false);
|
|
}
|
|
|
|
History::PositionInChatListChange History::adjustByPosInChatList(Dialogs::Mode list, Dialogs::IndexedList *indexed) {
|
|
t_assert(indexed != nullptr);
|
|
Dialogs::Row *lnk = mainChatListLink(list);
|
|
int32 movedFrom = lnk->pos();
|
|
indexed->adjustByPos(chatListLinks(list));
|
|
int32 movedTo = lnk->pos();
|
|
return { movedFrom, movedTo };
|
|
}
|
|
|
|
int History::posInChatList(Dialogs::Mode list) const {
|
|
return mainChatListLink(list)->pos();
|
|
}
|
|
|
|
Dialogs::Row *History::addToChatList(Dialogs::Mode list, Dialogs::IndexedList *indexed) {
|
|
t_assert(indexed != nullptr);
|
|
if (!inChatList(list)) {
|
|
chatListLinks(list) = indexed->addToEnd(this);
|
|
if (list == Dialogs::Mode::All && unreadCount()) {
|
|
App::histories().unreadIncrement(unreadCount(), mute());
|
|
Notify::unreadCounterUpdated();
|
|
}
|
|
}
|
|
return mainChatListLink(list);
|
|
}
|
|
|
|
void History::removeFromChatList(Dialogs::Mode list, Dialogs::IndexedList *indexed) {
|
|
t_assert(indexed != nullptr);
|
|
if (inChatList(list)) {
|
|
indexed->del(peer);
|
|
chatListLinks(list).clear();
|
|
if (list == Dialogs::Mode::All && unreadCount()) {
|
|
App::histories().unreadIncrement(-unreadCount(), mute());
|
|
Notify::unreadCounterUpdated();
|
|
}
|
|
}
|
|
}
|
|
|
|
void History::removeChatListEntryByLetter(Dialogs::Mode list, QChar letter) {
|
|
t_assert(letter != 0);
|
|
if (inChatList(list)) {
|
|
chatListLinks(list).remove(letter);
|
|
}
|
|
}
|
|
|
|
void History::addChatListEntryByLetter(Dialogs::Mode list, QChar letter, Dialogs::Row *row) {
|
|
t_assert(letter != 0);
|
|
if (inChatList(list)) {
|
|
chatListLinks(list).insert(letter, row);
|
|
}
|
|
}
|
|
|
|
void History::updateChatListEntry() const {
|
|
if (auto main = App::main()) {
|
|
if (inChatList(Dialogs::Mode::All)) {
|
|
main->dlgUpdated(Dialogs::Mode::All, mainChatListLink(Dialogs::Mode::All));
|
|
if (inChatList(Dialogs::Mode::Important)) {
|
|
main->dlgUpdated(Dialogs::Mode::Important, mainChatListLink(Dialogs::Mode::Important));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void History::setPinnedDialog(bool isPinned) {
|
|
setPinnedIndex(isPinned ? (++GlobalPinnedIndex) : 0);
|
|
}
|
|
|
|
void History::setPinnedIndex(int pinnedIndex) {
|
|
if (_pinnedIndex != pinnedIndex) {
|
|
auto wasPinned = isPinnedDialog();
|
|
_pinnedIndex = pinnedIndex;
|
|
updateChatListSortPosition();
|
|
updateChatListEntry();
|
|
if (wasPinned != isPinnedDialog()) {
|
|
Notify::peerUpdatedDelayed(peer, Notify::PeerUpdate::Flag::PinnedChanged);
|
|
}
|
|
App::histories().setIsPinned(this, isPinnedDialog());
|
|
}
|
|
}
|
|
|
|
void History::overviewSliceDone(int32 overviewIndex, const MTPmessages_Messages &result, bool onlyCounts) {
|
|
const QVector<MTPMessage> *v = 0;
|
|
switch (result.type()) {
|
|
case mtpc_messages_messages: {
|
|
auto &d(result.c_messages_messages());
|
|
App::feedUsers(d.vusers);
|
|
App::feedChats(d.vchats);
|
|
v = &d.vmessages.c_vector().v;
|
|
overviewCountData[overviewIndex] = 0;
|
|
} break;
|
|
|
|
case mtpc_messages_messagesSlice: {
|
|
auto &d(result.c_messages_messagesSlice());
|
|
App::feedUsers(d.vusers);
|
|
App::feedChats(d.vchats);
|
|
overviewCountData[overviewIndex] = d.vcount.v;
|
|
v = &d.vmessages.c_vector().v;
|
|
} break;
|
|
|
|
case mtpc_messages_channelMessages: {
|
|
auto &d(result.c_messages_channelMessages());
|
|
if (peer->isChannel()) {
|
|
peer->asChannel()->ptsReceived(d.vpts.v);
|
|
} else {
|
|
LOG(("API Error: received messages.channelMessages when no channel was passed! (History::overviewSliceDone, onlyCounts %1)").arg(Logs::b(onlyCounts)));
|
|
}
|
|
App::feedUsers(d.vusers);
|
|
App::feedChats(d.vchats);
|
|
overviewCountData[overviewIndex] = d.vcount.v;
|
|
v = &d.vmessages.c_vector().v;
|
|
} break;
|
|
|
|
default: return;
|
|
}
|
|
|
|
if (!onlyCounts && v->isEmpty()) {
|
|
overviewCountData[overviewIndex] = 0;
|
|
} else if (overviewCountData[overviewIndex] > 0) {
|
|
for_const (auto msgId, overviewIds[overviewIndex]) {
|
|
if (msgId < 0) {
|
|
++overviewCountData[overviewIndex];
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (QVector<MTPMessage>::const_iterator i = v->cbegin(), e = v->cend(); i != e; ++i) {
|
|
HistoryItem *item = App::histories().addNewMessage(*i, NewMessageExisting);
|
|
if (item && overviewIds[overviewIndex].constFind(item->id) == overviewIds[overviewIndex].cend()) {
|
|
overviewIds[overviewIndex].insert(item->id);
|
|
overview[overviewIndex].push_front(item->id);
|
|
}
|
|
}
|
|
}
|
|
|
|
void History::changeMsgId(MsgId oldId, MsgId newId) {
|
|
for (int32 i = 0; i < OverviewCount; ++i) {
|
|
auto j = overviewIds[i].find(oldId);
|
|
if (j != overviewIds[i].cend()) {
|
|
overviewIds[i].erase(j);
|
|
int32 index = overview[i].indexOf(oldId);
|
|
if (overviewIds[i].constFind(newId) == overviewIds[i].cend()) {
|
|
overviewIds[i].insert(newId);
|
|
if (index >= 0) {
|
|
overview[i][index] = newId;
|
|
} else {
|
|
overview[i].push_back(newId);
|
|
}
|
|
} else if (index >= 0) {
|
|
overview[i].removeAt(index);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void History::removeBlock(HistoryBlock *block) {
|
|
t_assert(block->items.isEmpty());
|
|
|
|
if (_buildingFrontBlock && block == _buildingFrontBlock->block) {
|
|
_buildingFrontBlock->block = nullptr;
|
|
}
|
|
|
|
int index = block->indexInHistory();
|
|
blocks.removeAt(index);
|
|
if (index < blocks.size()) {
|
|
for (int i = index, l = blocks.size(); i < l; ++i) {
|
|
blocks.at(i)->setIndexInHistory(i);
|
|
}
|
|
blocks.at(index)->items.front()->previousItemChanged();
|
|
} else if (!blocks.empty() && !blocks.back()->items.empty()) {
|
|
blocks.back()->items.back()->nextItemChanged();
|
|
}
|
|
}
|
|
|
|
History::~History() {
|
|
clearOnDestroy();
|
|
}
|
|
|
|
int HistoryBlock::resizeGetHeight(int newWidth, bool resizeAllItems) {
|
|
int y = 0;
|
|
for_const (HistoryItem *item, items) {
|
|
item->y = y;
|
|
if (resizeAllItems || item->pendingResize()) {
|
|
y += item->resizeGetHeight(newWidth);
|
|
} else {
|
|
y += item->height();
|
|
}
|
|
}
|
|
height = y;
|
|
return height;
|
|
}
|
|
|
|
void HistoryBlock::clear(bool leaveItems) {
|
|
Items lst;
|
|
std::swap(lst, items);
|
|
|
|
if (leaveItems) {
|
|
for_const (HistoryItem *item, lst) {
|
|
item->detachFast();
|
|
}
|
|
} else {
|
|
for_const (HistoryItem *item, lst) {
|
|
delete item;
|
|
}
|
|
}
|
|
}
|
|
|
|
void HistoryBlock::removeItem(HistoryItem *item) {
|
|
t_assert(item->block() == this);
|
|
|
|
int blockIndex = indexInHistory();
|
|
int itemIndex = item->indexInBlock();
|
|
if (history->showFrom == item) {
|
|
history->getNextShowFrom(this, itemIndex);
|
|
}
|
|
if (history->lastSentMsg == item) {
|
|
history->lastSentMsg = nullptr;
|
|
}
|
|
if (history->unreadBar == item) {
|
|
history->unreadBar = nullptr;
|
|
}
|
|
if (history->scrollTopItem == item) {
|
|
history->getNextScrollTopItem(this, itemIndex);
|
|
}
|
|
|
|
item->detachFast();
|
|
items.remove(itemIndex);
|
|
for (int i = itemIndex, l = items.size(); i < l; ++i) {
|
|
items.at(i)->setIndexInBlock(i);
|
|
}
|
|
if (items.isEmpty()) {
|
|
history->removeBlock(this);
|
|
} else if (itemIndex < items.size()) {
|
|
items.at(itemIndex)->previousItemChanged();
|
|
} else if (blockIndex + 1 < history->blocks.size()) {
|
|
history->blocks.at(blockIndex + 1)->items.front()->previousItemChanged();
|
|
} else if (!history->blocks.empty() && !history->blocks.back()->items.empty()) {
|
|
history->blocks.back()->items.back()->nextItemChanged();
|
|
}
|
|
|
|
if (items.isEmpty()) {
|
|
delete this;
|
|
}
|
|
}
|