/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "history/history.h" #include "history/view/history_view_element.h" #include "history/admin_log/history_admin_log_section.h" #include "history/history_message.h" #include "history/history_media_types.h" #include "history/history_service.h" #include "history/history_item_components.h" #include "history/history_inner_widget.h" #include "dialogs/dialogs_indexed_list.h" #include "styles/style_dialogs.h" #include "data/data_drafts.h" #include "data/data_session.h" #include "lang/lang_keys.h" #include "apiwrap.h" #include "mainwidget.h" #include "mainwindow.h" #include "storage/localstorage.h" #include "observer_peer.h" #include "auth_session.h" #include "window/notifications_manager.h" #include "calls/calls_instance.h" #include "storage/storage_facade.h" #include "storage/storage_shared_media.h" #include "storage/storage_feed_messages.h" #include "data/data_channel_admins.h" #include "data/data_feed.h" #include "ui/text_options.h" #include "core/crash_reports.h" namespace { constexpr auto kStatusShowClientsideTyping = 6000; constexpr auto kStatusShowClientsideRecordVideo = 6000; constexpr auto kStatusShowClientsideUploadVideo = 6000; constexpr auto kStatusShowClientsideRecordVoice = 6000; constexpr auto kStatusShowClientsideUploadVoice = 6000; constexpr auto kStatusShowClientsideRecordRound = 6000; constexpr auto kStatusShowClientsideUploadRound = 6000; constexpr auto kStatusShowClientsideUploadPhoto = 6000; constexpr auto kStatusShowClientsideUploadFile = 6000; constexpr auto kStatusShowClientsideChooseLocation = 6000; constexpr auto kStatusShowClientsideChooseContact = 6000; constexpr auto kStatusShowClientsidePlayGame = 10000; constexpr auto kSetMyActionForMs = 10000; constexpr auto kNewBlockEachMessage = 50; constexpr auto kSkipCloudDraftsFor = TimeId(3); void checkForSwitchInlineButton(HistoryItem *item) { if (item->out() || !item->hasSwitchInlineButton()) { return; } if (const auto user = item->history()->peer->asUser()) { if (!user->botInfo || !user->botInfo->inlineReturnPeerId) { return; } if (const auto markup = item->Get()) { for_const (auto &row, markup->rows) { for_const (auto &button, row) { if (button.type == HistoryMessageMarkupButton::Type::SwitchInline) { Notify::switchInlineBotButtonReceived(QString::fromUtf8(button.data)); return; } } } } } } } // namespace Histories::Histories() : _a_typings(animation(this, &Histories::step_typings)) , _selfDestructTimer([this] { checkSelfDestructItems(); }) { } History *Histories::find(PeerId peerId) const { if (const auto i = _map.find(peerId); i != _map.end()) { return i->second.get(); } return nullptr; } not_null Histories::findOrInsert(PeerId peerId) { if (const auto result = find(peerId)) { return result; } const auto [i, ok] = _map.emplace( peerId, std::make_unique(peerId)); return i->second.get(); } void Histories::clear() { for (const auto &[peerId, history] : _map) { history->unloadBlocks(); } App::historyClearMsgs(); _map.clear(); _unreadFull = _unreadMuted = 0; Notify::unreadCounterUpdated(); App::historyClearItems(); typing.clear(); } void Histories::registerSendAction( not_null history, not_null 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) { const auto i = _map.find(peer); if (i != _map.cend()) { typing.remove(i->second.get()); _map.erase(i); } } 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); } int Histories::unreadMutedCount() const { return _unreadMuted; } void Histories::unreadIncrement(int count, bool muted) { _unreadFull += count; if (muted) { _unreadMuted += count; } if (!muted || Global::IncludeMuted()) { Notify::unreadCounterUpdated(); } } void Histories::unreadMuteChanged(int count, bool muted) { if (muted) { _unreadMuted += count; } else { _unreadMuted -= count; } Notify::unreadCounterUpdated(); } bool Histories::unreadOnlyMuted() const { return Global::IncludeMuted() ? (_unreadMuted >= _unreadFull) : false; } void Histories::selfDestructIn(not_null item, TimeMs delay) { _selfDestructItems.push_back(item->fullId()); if (!_selfDestructTimer.isActive() || _selfDestructTimer.remainingTime() > delay) { _selfDestructTimer.callOnce(delay); } } void Histories::checkSelfDestructItems() { auto now = getms(true); auto nextDestructIn = TimeMs(0); for (auto i = _selfDestructItems.begin(); i != _selfDestructItems.cend();) { if (auto item = App::histItemById(*i)) { if (auto destructIn = item->getSelfDestructIn(now)) { if (nextDestructIn > 0) { accumulate_min(nextDestructIn, destructIn); } else { nextDestructIn = destructIn; } ++i; } else { i = _selfDestructItems.erase(i); } } else { i = _selfDestructItems.erase(i); } } if (nextDestructIn > 0) { _selfDestructTimer.callOnce(nextDestructIn); } } History::History(const PeerId &peerId) : Entry(this) , peer(App::peer(peerId)) , cloudDraftTextCache(st::dialogsTextWidthMin) , _mute(Auth().data().notifyIsMuted(peer)) , _sendActionText(st::dialogsTextWidthMin) { if (const auto user = peer->asUser()) { if (user->botInfo) { _outboxReadBefore = std::numeric_limits::max(); } } } void History::clearLastKeyboard() { if (lastKeyboardId) { if (lastKeyboardId == lastKeyboardHiddenId) { lastKeyboardHiddenId = 0; } lastKeyboardId = 0; if (auto main = App::main()) { main->updateBotKeyboard(this); } } lastKeyboardInited = true; lastKeyboardFrom = 0; } int History::height() const { return _height; } void History::removeNotification(not_null item) { if (!notifies.isEmpty()) { for (auto i = notifies.begin(), e = notifies.end(); i != e; ++i) { if ((*i) == item) { notifies.erase(i); break; } } } } HistoryItem *History::currentNotification() { return notifies.isEmpty() ? 0 : notifies.front(); } bool History::hasNotification() const { return !notifies.isEmpty(); } void History::skipNotification() { if (!notifies.isEmpty()) { notifies.pop_front(); } } void History::popNotification(HistoryItem *item) { if (!notifies.isEmpty() && notifies.back() == item) notifies.pop_back(); } bool History::hasPendingResizedItems() const { return _flags & Flag::f_has_pending_resized_items; } void History::setHasPendingResizedItems() { _flags |= Flag::f_has_pending_resized_items; } void History::itemRemoved(not_null item) { item->removeMainView(); if (lastMessage() == item) { _lastMessage = base::none; if (loadedAtBottom()) { if (const auto last = lastAvailableMessage()) { setLastMessage(last); } } if (const auto channel = peer->asChannel()) { if (const auto feed = channel->feed()) { // Must be after history->lastMessage() is updated. // Otherwise feed last message will be this value again. feed->messageRemoved(item); } } } itemVanished(item); } void History::itemVanished(not_null item) { removeNotification(item); if (lastKeyboardId == item->id) { clearLastKeyboard(); } if ((!item->out() || item->isPost()) && item->unread() && unreadCount() > 0) { changeUnreadCount(-1); } if (const auto channel = peer->asChannel()) { if (channel->pinnedMessageId() == item->id) { channel->clearPinnedMessage(); } } } void History::setLocalDraft(std::unique_ptr &&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(); Auth().api().saveDraftToCloudDelayed(from); } } void History::createLocalDraftFromCloud() { auto draft = cloudDraft(); if (Data::draftIsNull(draft) || !draft->date) { return; } auto existing = localDraft(); if (Data::draftIsNull(existing) || !existing->date || draft->date >= existing->date) { if (!existing) { setLocalDraft(std::make_unique( 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 &&draft) { _cloudDraft = std::move(draft); cloudDraftTextCache.clear(); } Data::Draft *History::createCloudDraft(Data::Draft *fromDraft) { if (Data::draftIsNull(fromDraft)) { setCloudDraft(std::make_unique( TextWithTags(), 0, MessageCursor(), false)); cloudDraft()->date = TimeId(0); } else { auto existing = cloudDraft(); if (!existing) { setCloudDraft(std::make_unique( 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 = unixtime(); } cloudDraftTextCache.clear(); updateChatListSortPosition(); return cloudDraft(); } bool History::skipCloudDraft(const QString &text, TimeId date) const { if (_lastSentDraftText && *_lastSentDraftText == text) { return true; } else if (date <= _lastSentDraftTime + kSkipCloudDraftsFor) { return true; } return false; } void History::setSentDraftText(const QString &text) { _lastSentDraftText = text; } void History::clearSentDraftText() { _lastSentDraftText = base::none; accumulate_max(_lastSentDraftTime, unixtime()); } void History::setEditDraft(std::unique_ptr &&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); } HistoryItemsList History::validateForwardDraft() { auto result = Auth().data().idsToItems(_forwardDraft); if (result.size() != _forwardDraft.size()) { setForwardDraft(Auth().data().itemsToIds(result)); } return result; } void History::setForwardDraft(MessageIdsList &&items) { _forwardDraft = std::move(items); } bool History::updateSendActionNeedsAnimating( not_null user, const MTPSendMessageAction &action) { if (peer->isSelf()) { return false; } using Type = SendAction::Type; if (action.type() == mtpc_sendMessageCancelAction) { clearSendAction(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_sendMessageRecordRoundAction: _sendActions.insert(user, { Type::RecordRound, ms + kStatusShowClientsideRecordRound }); break; case mtpc_sendMessageUploadRoundAction: _sendActions.insert(user, { Type::UploadRound, ms + kStatusShowClientsideUploadRound }); 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::RecordRound: return name.isEmpty() ? lang(lng_send_action_record_round) : lng_user_action_record_round(lt_user, name); case Type::UploadRound: return name.isEmpty() ? lang(lng_send_action_upload_round) : lng_user_action_upload_round(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: case Type::ChooseContact: return name.isEmpty() ? lang(lng_typing) : lng_user_typing(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, Ui::NameTextOptions()); } } auto result = (!_typing.isEmpty() || !_sendActions.isEmpty()); if (changed || result) { App::histories().sendActionAnimationUpdated().notify({ this, _sendActionAnimation.width(), st::normalFont->height, changed }); } return result; } HistoryItem *History::createItem( const MTPMessage &message, bool detachExistingItem) { const auto messageId = idFromMessage(message); if (!messageId) { return nullptr; } if (const auto result = App::histItemById(channelId(), messageId)) { if (detachExistingItem) { result->removeMainView(); } if (message.type() == mtpc_message) { const auto media = message.c_message().has_media() ? &message.c_message().vmedia : nullptr; result->updateSentMedia(media); } return result; } return HistoryItem::Create(this, message); } std::vector> History::createItems( const QVector &data) { auto result = std::vector>(); result.reserve(data.size()); for (auto i = data.cend(), e = data.cbegin(); i != e;) { const auto detachExistingItem = true; if (const auto item = createItem(*--i, detachExistingItem)) { result.push_back(item); } } return result; } not_null History::addNewService( MsgId msgId, TimeId date, const QString &text, MTPDmessage::Flags flags, bool unread) { auto message = HistoryService::PreparedText { text }; return addNewItem( new HistoryService(this, msgId, date, message, flags), unread); } HistoryItem *History::addNewMessage( const MTPMessage &msg, NewMessageType type) { if (type == NewMessageExisting) { return addToHistory(msg); } if (!loadedAtBottom() || peer->migrateTo()) { if (const auto item = addToHistory(msg)) { setLastMessage(item); if (type == NewMessageUnread) { newItemAdded(item); } return item; } return nullptr; } return addNewToLastBlock(msg, type); } HistoryItem *History::addNewToLastBlock( const MTPMessage &msg, NewMessageType type) { Expects(type != NewMessageExisting); const auto detachExistingItem = (type != NewMessageLast); const auto item = createItem(msg, detachExistingItem); if (!item || item->mainView()) { return item; } const auto newUnreadMessage = (type == NewMessageUnread); if (newUnreadMessage) { applyMessageChanges(item, msg); } const auto result = addNewItem(item, newUnreadMessage); if (type == NewMessageLast) { // When we add just one last item, like we do while loading dialogs, // we want to remove a single added grouped media, otherwise it will // jump once we open the message history (first we show only that // media, then we load the rest of the group and show the group). // // That way when we open the message history we show nothing until a // whole history part is loaded, it certainly will contain the group. removeOrphanMediaGroupPart(); } return result; } HistoryItem *History::addToHistory(const MTPMessage &msg) { const auto detachExistingItem = false; return createItem(msg, detachExistingItem); } not_null History::addNewForwarded( MsgId id, MTPDmessage::Flags flags, TimeId date, UserId from, const QString &postAuthor, not_null original) { return addNewItem( new HistoryMessage( this, id, flags, date, from, postAuthor, original), true); } not_null History::addNewDocument( MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, TimeId date, UserId from, const QString &postAuthor, not_null document, const TextWithEntities &caption, const MTPReplyMarkup &markup) { return addNewItem( new HistoryMessage( this, id, flags, replyTo, viaBotId, date, from, postAuthor, document, caption, markup), true); } not_null History::addNewPhoto( MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, TimeId date, UserId from, const QString &postAuthor, not_null photo, const TextWithEntities &caption, const MTPReplyMarkup &markup) { return addNewItem( new HistoryMessage( this, id, flags, replyTo, viaBotId, date, from, postAuthor, photo, caption, markup), true); } not_null History::addNewGame( MsgId id, MTPDmessage::Flags flags, UserId viaBotId, MsgId replyTo, TimeId date, UserId from, const QString &postAuthor, not_null game, const MTPReplyMarkup &markup) { return addNewItem( new HistoryMessage( this, id, flags, replyTo, viaBotId, date, from, postAuthor, game, markup), true); } void History::setUnreadMentionsCount(int count) { if (_unreadMentions.size() > count) { LOG(("API Warning: real mentions count is greater than received mentions count")); count = _unreadMentions.size(); } _unreadMentionsCount = count; } bool History::addToUnreadMentions( MsgId msgId, UnreadMentionType type) { if (peer->isChannel() && !peer->isMegagroup()) { return false; } auto allLoaded = _unreadMentionsCount ? (_unreadMentions.size() >= *_unreadMentionsCount) : false; if (allLoaded) { if (type == UnreadMentionType::New) { ++*_unreadMentionsCount; _unreadMentions.insert(msgId); return true; } } else if (!_unreadMentions.empty() && type != UnreadMentionType::New) { _unreadMentions.insert(msgId); return true; } return false; } void History::eraseFromUnreadMentions(MsgId msgId) { _unreadMentions.remove(msgId); if (_unreadMentionsCount) { if (*_unreadMentionsCount > 0) { --*_unreadMentionsCount; } } Notify::peerUpdatedDelayed(peer, Notify::PeerUpdate::Flag::UnreadMentionsChanged); } void History::addUnreadMentionsSlice(const MTPmessages_Messages &result) { auto count = 0; auto messages = (const QVector*)nullptr; auto getMessages = [](auto &list) { App::feedUsers(list.vusers); App::feedChats(list.vchats); return &list.vmessages.v; }; switch (result.type()) { case mtpc_messages_messages: { auto &d = result.c_messages_messages(); messages = getMessages(d); count = messages->size(); } break; case mtpc_messages_messagesSlice: { auto &d = result.c_messages_messagesSlice(); messages = getMessages(d); count = d.vcount.v; } break; case mtpc_messages_channelMessages: { LOG(("API Error: unexpected messages.channelMessages! (History::addUnreadMentionsSlice)")); auto &d = result.c_messages_channelMessages(); messages = getMessages(d); count = d.vcount.v; } break; case mtpc_messages_messagesNotModified: { LOG(("API Error: received messages.messagesNotModified! (History::addUnreadMentionsSlice)")); } break; default: Unexpected("type in History::addUnreadMentionsSlice"); } auto added = false; if (messages) { for (auto &message : *messages) { if (auto item = addToHistory(message)) { if (item->mentionsMe() && item->isMediaUnread()) { _unreadMentions.insert(item->id); added = true; } } } } if (!added) { count = _unreadMentions.size(); } setUnreadMentionsCount(count); Notify::peerUpdatedDelayed(peer, Notify::PeerUpdate::Flag::UnreadMentionsChanged); } not_null History::addNewItem( not_null item, bool unread) { Expects(!isBuildingFrontBlock()); addItemToBlock(item); if (!unread && IsServerMsgId(item->id)) { if (const auto sharedMediaTypes = item->sharedMediaTypes()) { auto from = loadedAtTop() ? 0 : minMsgId(); auto till = loadedAtBottom() ? ServerMaxMsgId : maxMsgId(); Auth().storage().add(Storage::SharedMediaAddExisting( peer->id, sharedMediaTypes, item->id, { from, till })); } } if (item->from()->id) { if (auto user = item->from()->asUser()) { auto getLastAuthors = [this]() -> std::deque>* { if (auto chat = peer->asChat()) { return &chat->lastAuthors; } else if (auto channel = peer->asMegagroup()) { return &channel->mgInfo->lastParticipants; } return nullptr; }; if (auto megagroup = peer->asMegagroup()) { if (user->botInfo) { auto mgInfo = megagroup->mgInfo.get(); Assert(mgInfo != nullptr); mgInfo->bots.insert(user); if (mgInfo->botStatus != 0 && mgInfo->botStatus < 2) { mgInfo->botStatus = 2; } } } if (auto lastAuthors = getLastAuthors()) { auto prev = ranges::find( *lastAuthors, user, [](not_null user) { return user.get(); }); auto index = (prev != lastAuthors->end()) ? (lastAuthors->end() - prev) : -1; if (index > 0) { lastAuthors->erase(prev); } else if (index < 0 && peer->isMegagroup()) { // nothing is outdated if just reordering // admins information outdated } if (index) { lastAuthors->push_front(user); } if (auto megagroup = peer->asMegagroup()) { Notify::peerUpdatedDelayed(peer, Notify::PeerUpdate::Flag::MembersChanged); Auth().data().addNewMegagroupParticipant(megagroup, user); } } } if (item->definesReplyKeyboard()) { auto markupFlags = item->replyKeyboardFlags(); if (!(markupFlags & MTPDreplyKeyboardMarkup::Flag::f_selective) || item->mentionsMe()) { auto getMarkupSenders = [this]() -> base::flat_set>* { if (auto chat = peer->asChat()) { return &chat->markupSenders; } else if (auto channel = peer->asMegagroup()) { return &channel->mgInfo->markupSenders; } return nullptr; }; if (auto markupSenders = getMarkupSenders()) { markupSenders->insert(item->from()); } if (markupFlags & MTPDreplyKeyboardMarkup_ClientFlag::f_zero) { // zero markup means replyKeyboardHide if (lastKeyboardFrom == item->from()->id || (!lastKeyboardInited && !peer->isChat() && !peer->isMegagroup() && !item->out())) { clearLastKeyboard(); } } else { bool botNotInChat = false; if (peer->isChat()) { botNotInChat = item->from()->isUser() && (!peer->asChat()->participants.empty() || !peer->canWrite()) && !peer->asChat()->participants.contains( item->from()->asUser()); } else if (peer->isMegagroup()) { botNotInChat = item->from()->isUser() && (peer->asChannel()->mgInfo->botStatus != 0 || !peer->canWrite()) && !peer->asChannel()->mgInfo->bots.contains( item->from()->asUser()); } if (botNotInChat) { clearLastKeyboard(); } else { lastKeyboardInited = true; lastKeyboardId = item->id; lastKeyboardFrom = item->from()->id; lastKeyboardUsed = false; } } } } } setLastMessage(item); if (unread) { newItemAdded(item); } Auth().data().notifyHistoryChangeDelayed(this); return item; } void History::applyMessageChanges( not_null item, const MTPMessage &data) { if (data.type() == mtpc_messageService) { applyServiceChanges(item, data.c_messageService()); } App::checkSavedGif(item); } void History::applyServiceChanges( not_null item, const MTPDmessageService &data) { auto &action = data.vaction; switch (action.type()) { case mtpc_messageActionChatAddUser: { auto &d = action.c_messageActionChatAddUser(); if (auto megagroup = peer->asMegagroup()) { auto mgInfo = megagroup->mgInfo.get(); Assert(mgInfo != nullptr); auto &v = d.vusers.v; for (auto i = 0, l = v.size(); i != l; ++i) { if (auto user = App::userLoaded(peerFromUser(v[i]))) { if (!base::contains(mgInfo->lastParticipants, user)) { mgInfo->lastParticipants.push_front(user); Notify::peerUpdatedDelayed(peer, Notify::PeerUpdate::Flag::MembersChanged); Auth().data().addNewMegagroupParticipant(megagroup, user); } 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 (auto megagroup = peer->asMegagroup()) { auto mgInfo = megagroup->mgInfo.get(); Assert(mgInfo != nullptr); if (auto user = item->from()->asUser()) { if (!base::contains(mgInfo->lastParticipants, user)) { mgInfo->lastParticipants.push_front(user); Notify::peerUpdatedDelayed(peer, Notify::PeerUpdate::Flag::MembersChanged); Auth().data().addNewMegagroupParticipant(megagroup, user); } if (user->botInfo) { mgInfo->bots.insert(user); if (mgInfo->botStatus != 0 && mgInfo->botStatus < 2) { mgInfo->botStatus = 2; } } } } } break; case mtpc_messageActionChatDeletePhoto: { if (const auto chat = peer->asChat()) { chat->setPhoto(MTP_chatPhotoEmpty()); } } break; case mtpc_messageActionChatDeleteUser: { auto &d = action.c_messageActionChatDeleteUser(); auto uid = peerFromUser(d.vuser_id); if (lastKeyboardFrom == uid) { clearLastKeyboard(); } if (auto megagroup = peer->asMegagroup()) { if (auto user = App::userLoaded(uid)) { auto mgInfo = megagroup->mgInfo.get(); Assert(mgInfo != nullptr); auto i = ranges::find( mgInfo->lastParticipants, user, [](not_null user) { return user.get(); }); if (i != mgInfo->lastParticipants.end()) { mgInfo->lastParticipants.erase(i); Notify::peerUpdatedDelayed(peer, Notify::PeerUpdate::Flag::MembersChanged); } Auth().data().removeMegagroupParticipant(megagroup, user); if (megagroup->membersCount() > 1) { megagroup->setMembersCount(megagroup->membersCount() - 1); } else { mgInfo->lastParticipantsStatus |= MegagroupInfo::LastParticipantsCountOutdated; mgInfo->lastParticipantsCount = 0; } if (mgInfo->lastAdmins.contains(user)) { mgInfo->lastAdmins.remove(user); if (megagroup->adminsCount() > 1) { megagroup->setAdminsCount(megagroup->adminsCount() - 1); } Notify::peerUpdatedDelayed(peer, Notify::PeerUpdate::Flag::AdminsChanged); } mgInfo->bots.remove(user); if (mgInfo->bots.empty() && mgInfo->botStatus > 0) { mgInfo->botStatus = -1; } } Data::ChannelAdminChanges(megagroup).feed(uid, false); } } break; case mtpc_messageActionChatEditPhoto: { auto &d = action.c_messageActionChatEditPhoto(); if (d.vphoto.type() == mtpc_photo) { auto &sizes = d.vphoto.c_photo().vsizes.v; if (!sizes.isEmpty()) { auto photo = Auth().data().photo(d.vphoto.c_photo()); if (photo) photo->peer = peer; auto &smallSize = sizes.front(); auto &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) { const auto newPhotoId = photo ? photo->id : 0; if (const auto chat = peer->asChat()) { chat->setPhoto(newPhotoId, MTP_chatPhoto(*smallLoc, *bigLoc)); } else if (const auto channel = peer->asChannel()) { channel->setPhoto(newPhotoId, MTP_chatPhoto(*smallLoc, *bigLoc)); } 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: { if (auto chat = peer->asChat()) { chat->addFlags(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 (data.has_reply_to_msg_id() && item) { if (auto channel = item->history()->peer->asChannel()) { channel->setPinnedMessageId(data.vreply_to_msg_id.v); } } } break; case mtpc_messageActionPhoneCall: { Calls::Current().newServiceMessage().notify(item->fullId()); } break; } } void History::clearSendAction(not_null 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::mainViewRemoved( not_null block, not_null view) { if (_joinedMessage == view->data()) { _joinedMessage = nullptr; } if (_firstUnreadView == view) { getNextFirstUnreadMessage(); } if (_unreadBarView == view) { _unreadBarView = nullptr; } if (scrollTopItem == view) { getNextScrollTopItem(block, view->indexInBlock()); } } void History::newItemAdded(not_null item) { App::checkImageCacheSize(); item->indexAsNewItem(); if (const auto from = item->from() ? item->from()->asUser() : nullptr) { if (from == item->author()) { clearSendAction(from); } from->madeAction(item->date()); } if (item->out()) { 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; } blocks.push_front(std::make_unique(this)); for (auto i = 0, l = int(blocks.size()); i != l; ++i) { blocks[i]->setIndexInHistory(i); } _buildingFrontBlock->block = blocks.front().get(); if (_buildingFrontBlock->expectedItemsCount > 0) { _buildingFrontBlock->block->messages.reserve( _buildingFrontBlock->expectedItemsCount + 1); } return _buildingFrontBlock->block; } const auto addNewBlock = blocks.empty() || (blocks.back()->messages.size() >= kNewBlockEachMessage); if (addNewBlock) { blocks.push_back(std::make_unique(this)); blocks.back()->setIndexInHistory(blocks.size() - 1); blocks.back()->messages.reserve(kNewBlockEachMessage); } return blocks.back().get(); } void History::viewReplaced(not_null was, Element *now) { if (scrollTopItem == was) scrollTopItem= now; if (_firstUnreadView == was) _firstUnreadView= now; if (_unreadBarView == was) _unreadBarView = now; } void History::addItemToBlock(not_null item) { Expects(!item->mainView()); auto block = prepareBlockForAddingItem(); block->messages.push_back(item->createView( HistoryInner::ElementDelegate())); const auto view = block->messages.back().get(); view->attachToBlock(block, block->messages.size() - 1); if (isBuildingFrontBlock() && _buildingFrontBlock->expectedItemsCount > 0) { --_buildingFrontBlock->expectedItemsCount; } } void History::addEdgesToSharedMedia() { auto from = loadedAtTop() ? 0 : minMsgId(); auto till = loadedAtBottom() ? ServerMaxMsgId : maxMsgId(); for (auto i = 0; i != Storage::kSharedMediaTypeCount; ++i) { const auto type = static_cast(i); Auth().storage().add(Storage::SharedMediaAddSlice( peer->id, type, {}, { from, till })); } } void History::addOlderSlice(const QVector &slice) { if (slice.isEmpty()) { _loadedAtTop = true; checkJoinedMessage(); return; } if (const auto added = createItems(slice); !added.empty()) { startBuildingFrontBlock(added.size()); for (const auto item : added) { addItemToBlock(item); } finishBuildingFrontBlock(); if (loadedAtBottom()) { // Add photos to overview and authors to lastAuthors. addItemsToLists(added); } addToSharedMedia(added); } else { // If no items were added it means we've loaded everything old. _loadedAtTop = true; addEdgesToSharedMedia(); } checkJoinedMessage(); checkLastMessage(); } void History::addNewerSlice(const QVector &slice) { bool wasEmpty = isEmpty(), wasLoadedAtBottom = loadedAtBottom(); if (slice.isEmpty()) { _loadedAtBottom = true; if (!lastMessage()) { setLastMessage(lastAvailableMessage()); } } if (const auto added = createItems(slice); !added.empty()) { Assert(!isBuildingFrontBlock()); for (const auto item : added) { addItemToBlock(item); } addToSharedMedia(added); } else { _loadedAtBottom = true; setLastMessage(lastAvailableMessage()); addEdgesToSharedMedia(); } if (!wasLoadedAtBottom) { checkAddAllToUnreadMentions(); } checkJoinedMessage(); checkLastMessage(); } void History::checkLastMessage() { if (const auto last = lastMessage()) { if (!_loadedAtBottom && last->mainView()) { _loadedAtBottom = true; checkAddAllToUnreadMentions(); } } else if (_loadedAtBottom) { setLastMessage(lastAvailableMessage()); } } void History::addItemsToLists( const std::vector> &items) { std::deque> *lastAuthors = nullptr; base::flat_set> *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 (const auto item : base::reversed(items)) { item->addToUnreadMentions(UnreadMentionType::Existing); if (item->from()->id) { if (lastAuthors) { // chats if (auto user = item->from()->asUser()) { if (!base::contains(*lastAuthors, user)) { lastAuthors->push_back(user); } } } } 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.empty()) && 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; } } } } } } void History::checkAddAllToUnreadMentions() { if (!loadedAtBottom()) { return; } for (const auto &block : blocks) { for (const auto &message : block->messages) { const auto item = message->data(); item->addToUnreadMentions(UnreadMentionType::Existing); } } } void History::addToSharedMedia( const std::vector> &items) { std::vector medias[Storage::kSharedMediaTypeCount]; for (const auto item : items) { if (const auto types = item->sharedMediaTypes()) { for (auto i = 0; i != Storage::kSharedMediaTypeCount; ++i) { const auto type = static_cast(i); if (types.test(type)) { if (medias[i].empty()) { medias[i].reserve(items.size()); } medias[i].push_back(item->id); } } } } const auto from = loadedAtTop() ? 0 : minMsgId(); const auto till = loadedAtBottom() ? ServerMaxMsgId : maxMsgId(); for (auto i = 0; i != Storage::kSharedMediaTypeCount; ++i) { if (!medias[i].empty()) { const auto type = static_cast(i); Auth().storage().add(Storage::SharedMediaAddSlice( peer->id, type, std::move(medias[i]), { from, till })); } } } int History::countUnread(MsgId upTo) { int result = 0; for (auto i = blocks.cend(), e = blocks.cbegin(); i != e;) { --i; const auto &messages = (*i)->messages; for (auto j = messages.cend(), en = messages.cbegin(); j != en;) { --j; const auto item = (*j)->data(); if (item->id > 0 && item->id <= upTo) { break; } else if (!item->out() && item->unread() && item->id > upTo) { ++result; } } } return result; } void History::calculateFirstUnreadMessage() { if (_firstUnreadView || !_inboxReadBefore) { return; } for (auto i = blocks.cend(); i != blocks.cbegin();) { --i; const auto &messages = (*i)->messages; for (auto j = messages.cend(); j != messages.cbegin();) { --j; const auto view = j->get(); const auto item = view->data(); if (!IsServerMsgId(item->id)) { continue; } else if (!item->out() || !_firstUnreadView) { if (item->id >= *_inboxReadBefore) { _firstUnreadView = view; } else { return; } } } } } MsgId History::readInbox() { const auto upTo = msgIdForRead(); changeUnreadCount(-unreadCount()); if (upTo) { inboxRead(upTo); } return upTo; } void History::inboxRead(MsgId upTo) { if (const auto nowUnreadCount = unreadCount()) { if (loadedAtBottom()) { App::main()->historyToDown(this); } changeUnreadCount(countUnread(upTo) - nowUnreadCount); } setInboxReadTill(upTo); updateChatListEntry(); if (peer->migrateTo()) { if (auto migrateTo = App::historyLoaded(peer->migrateTo()->id)) { migrateTo->updateChatListEntry(); } } _firstUnreadView = nullptr; Auth().notifications().clearFromHistory(this); } void History::inboxRead(not_null wasRead) { if (IsServerMsgId(wasRead->id)) { inboxRead(wasRead->id); } } void History::outboxRead(MsgId upTo) { setOutboxReadTill(upTo); if (const auto last = lastMessage()) { if (last->out() && IsServerMsgId(last->id) && last->id <= upTo) { if (const auto main = App::main()) { main->repaintDialogRow(this, last->id); } } } updateChatListEntry(); } void History::outboxRead(not_null wasRead) { if (IsServerMsgId(wasRead->id)) { outboxRead(wasRead->id); } } MsgId History::loadAroundId() const { if (_unreadCount && *_unreadCount > 0 && _inboxReadBefore) { return *_inboxReadBefore; } return MsgId(0); } HistoryItem *History::lastAvailableMessage() const { return isEmpty() ? nullptr : blocks.back()->messages.back()->data().get(); } int History::unreadCount() const { return _unreadCount ? *_unreadCount : 0; } bool History::unreadCountKnown() const { return !!_unreadCount; } void History::setUnreadCount(int newUnreadCount) { if (!_unreadCount || *_unreadCount != newUnreadCount) { const auto unreadCountDelta = _unreadCount | [&](int count) { return newUnreadCount - count; }; if (newUnreadCount == 1) { if (loadedAtBottom()) { _firstUnreadView = !isEmpty() ? blocks.back()->messages.back().get() : nullptr; } if (const auto last = msgIdForRead()) { setInboxReadTill(last - 1); } } else if (!newUnreadCount) { _firstUnreadView = nullptr; if (const auto last = msgIdForRead()) { setInboxReadTill(last); } } else { if (!_firstUnreadView && !_unreadBarView && loadedAtBottom()) { calculateFirstUnreadMessage(); } } _unreadCount = newUnreadCount; if (_unreadBarView) { const auto count = chatListUnreadCount(); if (count > 0) { _unreadBarView->setUnreadBarCount(count); } else { _unreadBarView->setUnreadBarFreezed(); } } if (inChatList(Dialogs::Mode::All)) { App::histories().unreadIncrement( unreadCountDelta ? *unreadCountDelta : newUnreadCount, mute()); } if (const auto main = App::main()) { main->unreadCountChanged(this); } } } void History::changeUnreadCount(int delta) { if (_unreadCount) { setUnreadCount(std::max(*_unreadCount + delta, 0)); } if (const auto channel = peer->asChannel()) { if (const auto feed = channel->feed()) { const auto mutedCountDelta = mute() ? delta : 0; feed->unreadCountChanged(delta, mutedCountDelta); } } } bool History::mute() const { return _mute; } bool History::changeMute(bool newMute) { if (_mute == newMute) { return false; } _mute = newMute; const auto feed = peer->isChannel() ? peer->asChannel()->feed() : nullptr; if (feed) { if (_unreadCount) { if (*_unreadCount) { const auto unreadCountDelta = 0; const auto mutedCountDelta = _mute ? *_unreadCount : -*_unreadCount; feed->unreadCountChanged(unreadCountDelta, mutedCountDelta); } } else { Auth().api().requestDialogEntry(this); Auth().api().requestDialogEntry(feed); } } if (inChatList(Dialogs::Mode::All)) { if (_unreadCount && *_unreadCount) { App::histories().unreadMuteChanged(*_unreadCount, _mute); Notify::unreadCounterUpdated(); } Notify::historyMuteUpdated(this); } updateChatListEntry(); Notify::peerUpdatedDelayed( peer, Notify::PeerUpdate::Flag::NotificationsEnabled); return true; } void History::getNextFirstUnreadMessage() { Expects(_firstUnreadView != nullptr); const auto block = _firstUnreadView->block(); const auto index = _firstUnreadView->indexInBlock(); const auto setFromMessage = [&](const auto &view) { if (IsServerMsgId(view->data()->id)) { _firstUnreadView = view.get(); return true; } return false; }; if (index >= 0) { const auto count = int(block->messages.size()); for (auto i = index + 1; i != count; ++i) { const auto &message = block->messages[i]; if (setFromMessage(message)) { return; } } } const auto count = int(blocks.size()); for (auto j = block->indexInHistory() + 1; j != count; ++j) { for (const auto &message : blocks[j]->messages) { if (setFromMessage(message)) { return; } } } _firstUnreadView = nullptr; } std::shared_ptr History::adminLogIdManager() { if (const auto strong = _adminLogIdManager.lock()) { return strong; } auto result = std::make_shared(); _adminLogIdManager = result; return result; } QDateTime History::adjustChatListDate() const { const auto result = chatsListDate(); if (const auto draft = cloudDraft()) { if (!Data::draftIsNull(draft)) { const auto draftResult = ParseDateTime(draft->date); if (draftResult > result) { return draftResult; } } } return result; } 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; } auto itemIndex = 0; auto blockIndex = 0; auto itemTop = 0; if (scrollTopItem) { itemIndex = scrollTopItem->indexInBlock(); blockIndex = scrollTopItem->block()->indexInHistory(); itemTop = blocks[blockIndex]->y() + scrollTopItem->y(); } if (itemTop > top) { // go backward through history while we don't find an item that starts above do { const auto &block = blocks[blockIndex]; for (--itemIndex; itemIndex >= 0; --itemIndex) { const auto view = block->messages[itemIndex].get(); itemTop = block->y() + view->y(); if (itemTop <= top) { scrollTopItem = view; return; } } if (--blockIndex >= 0) { itemIndex = blocks[blockIndex]->messages.size(); } else { break; } } while (true); scrollTopItem = blocks.front()->messages.front().get(); } else { // go forward through history while we don't find the last item that starts above for (auto blocksCount = int(blocks.size()); blockIndex < blocksCount; ++blockIndex) { const auto &block = blocks[blockIndex]; for (auto itemsCount = int(block->messages.size()); itemIndex < itemsCount; ++itemIndex) { itemTop = block->y() + block->messages[itemIndex]->y(); if (itemTop > top) { Assert(itemIndex > 0 || blockIndex > 0); if (itemIndex > 0) { scrollTopItem = block->messages[itemIndex - 1].get(); } else { scrollTopItem = blocks[blockIndex - 1]->messages.back().get(); } return; } } itemIndex = 0; } scrollTopItem = blocks.back()->messages.back().get(); } } void History::getNextScrollTopItem(HistoryBlock *block, int32 i) { ++i; if (i > 0 && i < block->messages.size()) { scrollTopItem = block->messages[i].get(); return; } int j = block->indexInHistory() + 1; if (j > 0 && j < blocks.size()) { scrollTopItem = blocks[j]->messages.front().get(); return; } scrollTopItem = nullptr; } void History::addUnreadBar() { if (_unreadBarView || !_firstUnreadView || !unreadCount()) { return; } if (const auto count = chatListUnreadCount()) { _unreadBarView = _firstUnreadView; _unreadBarView->setUnreadBarCount(count); } } void History::destroyUnreadBar() { if (const auto view = base::take(_unreadBarView)) { view->destroyUnreadBar(); } } bool History::hasNotFreezedUnreadBar() const { if (_firstUnreadView) { if (const auto view = _unreadBarView) { if (const auto bar = view->Get()) { return !bar->freezed; } } } return false; } void History::unsetFirstUnreadMessage() { _firstUnreadView = nullptr; } HistoryView::Element *History::unreadBar() const { return _unreadBarView; } HistoryView::Element *History::firstUnreadMessage() const { return _firstUnreadView; } not_null History::addNewInTheMiddle( not_null item, int blockIndex, int itemIndex) { Expects(blockIndex >= 0); Expects(blockIndex < blocks.size()); Expects(itemIndex >= 0); Expects(itemIndex <= blocks[blockIndex]->messages.size()); const auto &block = blocks[blockIndex]; const auto it = block->messages.insert( block->messages.begin() + itemIndex, item->createView( HistoryInner::ElementDelegate())); (*it)->attachToBlock(block.get(), itemIndex); if (itemIndex + 1 < block->messages.size()) { for (auto i = itemIndex + 1, l = int(block->messages.size()); i != l; ++i) { block->messages[i]->setIndexInBlock(i); } block->messages[itemIndex + 1]->previousInBlocksChanged(); } else if (blockIndex + 1 < blocks.size() && !blocks[blockIndex + 1]->messages.empty()) { blocks[blockIndex + 1]->messages.front()->previousInBlocksChanged(); } else { (*it)->nextInBlocksRemoved(); } return item; } int History::chatListUnreadCount() const { const auto result = unreadCount(); const auto addFromId = [&] { if (const auto from = peer->migrateFrom()) { return from->id; } else if (const auto to = peer->migrateTo()) { return to->id; } return PeerId(0); }(); if (const auto migrated = App::historyLoaded(addFromId)) { return result + migrated->unreadCount(); } return result; } bool History::chatListMutedBadge() const { return mute(); } HistoryItem *History::chatsListItem() const { return lastMessage(); } const QString &History::chatsListName() const { return peer->name; } const base::flat_set &History::chatsListNameWords() const { return peer->nameWords(); } const base::flat_set &History::chatsListFirstLetters() const { return peer->nameFirstLetters(); } void History::loadUserpic() { peer->loadUserpic(); } void History::paintUserpic( Painter &p, int x, int y, int size) const { peer->paintUserpic(p, x, y, size); } void History::startBuildingFrontBlock(int expectedItemsCount) { Assert(!isBuildingFrontBlock()); Assert(expectedItemsCount > 0); _buildingFrontBlock.reset(new BuildingBlock()); _buildingFrontBlock->expectedItemsCount = expectedItemsCount; } void History::finishBuildingFrontBlock() { Expects(isBuildingFrontBlock()); // Some checks if there was some message history already if (const auto block = base::take(_buildingFrontBlock)->block) { if (blocks.size() > 1) { // ... item, item, item, last ], [ first, item, item ... const auto last = block->messages.back().get(); const auto first = blocks[1]->messages.front().get(); // we've added a new front block, so previous item for // the old first item of a first block was changed first->previousInBlocksChanged(); } else { block->messages.back()->nextInBlocksRemoved(); } } } void History::clearNotifications() { notifies.clear(); } bool History::loadedAtBottom() const { return _loadedAtBottom; } bool History::loadedAtTop() const { return _loadedAtTop; } 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 (const auto migratePeer = peer->migrateFrom()) { if (const auto migrated = App::historyLoaded(migratePeer)) { if (migrated->unreadCount()) { return migrated->isReadyFor(msgId); } } } if (unreadCount() && _inboxReadBefore) { 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->mainView(); } void History::getReadyFor(MsgId msgId) { if (msgId < 0 && -msgId < ServerMaxMsgId && peer->migrateFrom()) { const auto migrated = App::history(peer->migrateFrom()->id); migrated->getReadyFor(-msgId); if (migrated->isEmpty()) { unloadBlocks(); } return; } if (msgId == ShowAtUnreadMsgId) { if (const auto migratePeer = peer->migrateFrom()) { if (const auto migrated = App::historyLoaded(migratePeer)) { if (migrated->unreadCount()) { unloadBlocks(); migrated->getReadyFor(msgId); return; } } } } if (!isReadyFor(msgId)) { unloadBlocks(); if (msgId == ShowAtTheEndMsgId) { _loadedAtBottom = true; } } } void History::setNotLoadedAtBottom() { _loadedAtBottom = false; Auth().storage().invalidate( Storage::SharedMediaInvalidateBottom(peer->id)); if (const auto channel = peer->asChannel()) { if (const auto feed = channel->feed()) { Auth().storage().invalidate( Storage::FeedMessagesInvalidateBottom( feed->id())); } } } void History::markFullyLoaded() { _loadedAtTop = _loadedAtBottom = true; if (isEmpty()) { Auth().storage().remove(Storage::SharedMediaRemoveAll(peer->id)); if (const auto channel = peer->asChannel()) { if (const auto feed = channel->feed()) { Auth().storage().remove(Storage::FeedMessagesRemoveAll( feed->id(), channel->bareId())); } } } } void History::setLastMessage(HistoryItem *item) { if (item) { if (_lastMessage && !*_lastMessage) { Local::removeSavedPeer(peer); } _lastMessage = item; if (const auto feed = peer->feed()) { feed->updateLastMessage(item); } setChatsListDate(ItemDateTime(item)); } else if (!_lastMessage || *_lastMessage) { _lastMessage = nullptr; updateChatListEntry(); } } HistoryItem *History::lastMessage() const { return _lastMessage ? (*_lastMessage) : nullptr; } bool History::lastMessageKnown() const { return !!_lastMessage; } void History::updateChatListExistence() { Entry::updateChatListExistence(); if (!lastMessageKnown() || !unreadCountKnown()) { if (const auto channel = peer->asChannel()) { if (!channel->feed()) { // After ungrouping from a feed we need to load dialog. Auth().api().requestDialogEntry(this); } } } } bool History::useProxyPromotion() const { if (!isProxyPromoted()) { return false; } else if (const auto channel = peer->asChannel()) { return !isPinnedDialog() && !channel->amIn(); } return false; } bool History::shouldBeInChatList() const { if (peer->migrateTo()) { return false; } else if (isPinnedDialog()) { return true; } else if (const auto channel = peer->asChannel()) { if (!channel->amIn()) { return isProxyPromoted(); } else if (const auto feed = channel->feed()) { return !feed->needUpdateInChatList(); } } return true; } void History::unknownMessageDeleted(MsgId messageId) { if (_inboxReadBefore && messageId >= *_inboxReadBefore) { changeUnreadCount(-1); } } bool History::isServerSideUnread(not_null item) const { Expects(IsServerMsgId(item->id)); return item->out() ? (!_outboxReadBefore || (item->id >= *_outboxReadBefore)) : (!_inboxReadBefore || (item->id >= *_inboxReadBefore)); } void History::applyDialog(const MTPDdialog &data) { applyDialogFields( data.vunread_count.v, data.vread_inbox_max_id.v, data.vread_outbox_max_id.v); applyDialogTopMessage(data.vtop_message.v); setUnreadMentionsCount(data.vunread_mentions_count.v); if (const auto channel = peer->asChannel()) { if (data.has_pts()) { channel->ptsReceived(data.vpts.v); } if (!channel->amCreator()) { const auto topMessageId = FullMsgId( peerToChannel(channel->id), data.vtop_message.v); if (const auto item = App::histItemById(topMessageId)) { if (item->date() <= channel->date) { Auth().api().requestSelfParticipant(channel); } } } } Auth().data().applyNotifySetting( MTP_notifyPeer(data.vpeer), data.vnotify_settings); if (data.has_draft() && data.vdraft.type() == mtpc_draftMessage) { Data::applyPeerCloudDraft(peer->id, data.vdraft.c_draftMessage()); } } void History::applyDialogFields( int unreadCount, MsgId maxInboxRead, MsgId maxOutboxRead) { setUnreadCount(unreadCount); setInboxReadTill(maxInboxRead); setOutboxReadTill(maxOutboxRead); } void History::applyDialogTopMessage(MsgId topMessageId) { if (topMessageId) { const auto itemId = FullMsgId( channelId(), topMessageId); if (const auto item = App::histItemById(itemId)) { setLastMessage(item); } else { setLastMessage(nullptr); } } else { setLastMessage(nullptr); } } void History::setInboxReadTill(MsgId upTo) { if (_inboxReadBefore) { accumulate_max(*_inboxReadBefore, upTo + 1); } else { _inboxReadBefore = upTo + 1; } } void History::setOutboxReadTill(MsgId upTo) { if (_outboxReadBefore) { accumulate_max(*_outboxReadBefore, upTo + 1); } else { _outboxReadBefore = upTo + 1; } } MsgId History::minMsgId() const { for (const auto &block : blocks) { for (const auto &message : block->messages) { const auto item = message->data(); if (IsServerMsgId(item->id)) { return item->id; } } } return 0; } MsgId History::maxMsgId() const { for (const auto &block : base::reversed(blocks)) { for (const auto &message : base::reversed(block->messages)) { const auto item = message->data(); if (IsServerMsgId(item->id)) { return item->id; } } } return 0; } MsgId History::msgIdForRead() const { const auto last = lastMessage(); const auto result = (last && IsServerMsgId(last->id)) ? last->id : MsgId(0); return loadedAtBottom() ? std::max(result, maxMsgId()) : result; } HistoryItem *History::lastSentMessage() const { if (!loadedAtBottom()) { return nullptr; } for (const auto &block : base::reversed(blocks)) { for (const auto &message : base::reversed(block->messages)) { const auto item = message->data(); if (IsServerMsgId(item->id) && (item->out() || peer->isSelf())) { return item; } } } return nullptr; } void History::resizeToWidth(int newWidth) { const auto resizeAllItems = (_width != newWidth); if (!resizeAllItems && !hasPendingResizedItems()) { return; } _flags &= ~(Flag::f_has_pending_resized_items); _width = newWidth; int y = 0; for (const auto &block : blocks) { block->setY(y); y += block->resizeGetHeight(newWidth, resizeAllItems); } _height = y; } ChannelId History::channelId() const { return peerToChannel(peer->id); } bool History::isChannel() const { return peerIsChannel(peer->id); } bool History::isMegagroup() const { return peer->isMegagroup(); } not_null History::migrateToOrMe() const { if (auto to = peer->migrateTo()) { return App::history(to); } // We could get it by App::history(peer), but we optimize. return const_cast(this); } History *History::migrateFrom() const { if (auto from = peer->migrateFrom()) { return App::history(from); } return nullptr; } MsgRange History::rangeForDifferenceRequest() const { auto fromId = MsgId(0); auto toId = MsgId(0); for (auto blockIndex = 0, blocksCount = int(blocks.size()); blockIndex < blocksCount; ++blockIndex) { const auto &block = blocks[blockIndex]; for (auto itemIndex = 0, itemsCount = int(block->messages.size()); itemIndex < itemsCount; ++itemIndex) { const auto id = block->messages[itemIndex]->data()->id; if (id > 0) { fromId = id; break; } } if (fromId) break; } if (fromId) { for (auto blockIndex = blocks.size(); blockIndex > 0;) { const auto &block = blocks[--blockIndex]; for (auto itemIndex = block->messages.size(); itemIndex > 0;) { const auto id = block->messages[--itemIndex]->data()->id; if (id > 0) { toId = id; break; } } if (toId) break; } return { fromId, toId + 1 }; } return MsgRange(); } HistoryService *History::insertJoinedMessage(bool unread) { if (!isChannel() || _joinedMessage || !peer->asChannel()->amIn() || (peer->isMegagroup() && peer->asChannel()->mgInfo->joinedMessageFound)) { return _joinedMessage; } const auto inviter = (peer->asChannel()->inviter > 0) ? App::userLoaded(peer->asChannel()->inviter) : nullptr; if (!inviter) { return nullptr; } MTPDmessage::Flags flags = 0; if (inviter->id == Auth().userPeerId()) { unread = false; //} else if (unread) { // flags |= MTPDmessage::Flag::f_unread; } const auto inviteDate = peer->asChannel()->inviteDate; if (isEmpty()) { _joinedMessage = GenerateJoinedMessage( this, inviteDate, inviter, flags); addNewItem(_joinedMessage, unread); return _joinedMessage; } for (auto blockIndex = blocks.size(); blockIndex > 0;) { const auto &block = blocks[--blockIndex]; for (auto itemIndex = block->messages.size(); itemIndex > 0;) { const auto item = block->messages[--itemIndex]->data(); // 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 = GenerateJoinedMessage( this, inviteDate, inviter, flags); addNewInTheMiddle(_joinedMessage, blockIndex, itemIndex); const auto lastDate = chatsListDate(); if (lastDate.isNull() || ParseDateTime(inviteDate) >= lastDate) { setLastMessage(_joinedMessage); if (unread) { newItemAdded(_joinedMessage); } } return _joinedMessage; } } } startBuildingFrontBlock(); _joinedMessage = GenerateJoinedMessage( this, inviteDate, inviter, flags); addItemToBlock(_joinedMessage); finishBuildingFrontBlock(); return _joinedMessage; } void History::checkJoinedMessage(bool createUnread) { if (!isChannel() || _joinedMessage || peer->asChannel()->inviter <= 0) { return; } if (isEmpty()) { if (loadedAtTop() && loadedAtBottom()) { if (insertJoinedMessage(createUnread)) { if (_joinedMessage->mainView()) { setLastMessage(_joinedMessage); } } return; } } const auto inviteDate = peer->asChannel()->inviteDate; auto firstDate = TimeId(0); auto lastDate = TimeId(0); if (!blocks.empty()) { firstDate = blocks.front()->messages.front()->data()->date(); lastDate = blocks.back()->messages.back()->data()->date(); } if (firstDate && lastDate && (firstDate <= inviteDate || loadedAtTop()) && (lastDate > inviteDate || loadedAtBottom())) { const auto willBeLastMsg = (inviteDate >= lastDate); if (insertJoinedMessage(createUnread && willBeLastMsg) && willBeLastMsg) { if (_joinedMessage->mainView()) { setLastMessage(_joinedMessage); } } } } void History::removeJoinedMessage() { if (_joinedMessage) { base::take(_joinedMessage)->destroy(); } } bool History::isEmpty() const { return blocks.empty(); } bool History::isDisplayedEmpty() const { return isEmpty() || ((blocks.size() == 1) && blocks.front()->messages.size() == 1 && blocks.front()->messages.front()->data()->isEmpty()); } bool History::hasOrphanMediaGroupPart() const { if (loadedAtTop() || !loadedAtBottom()) { return false; } else if (blocks.size() != 1) { return false; } else if (blocks.front()->messages.size() != 1) { return false; } const auto last = blocks.front()->messages.front()->data(); return last->groupId() != MessageGroupId(); } bool History::removeOrphanMediaGroupPart() { if (hasOrphanMediaGroupPart()) { unloadBlocks(); return true; } return false; } QVector History::collectMessagesFromUserToDelete( not_null user) const { auto result = QVector(); for (const auto &block : blocks) { for (const auto &message : block->messages) { const auto item = message->data(); if (item->from() == user && item->canDelete()) { result.push_back(item->id); } } } return result; } void History::clear() { clearBlocks(false); } void History::unloadBlocks() { clearBlocks(true); } void History::clearBlocks(bool leaveItems) { _unreadBarView = nullptr; _firstUnreadView = nullptr; _joinedMessage = nullptr; if (scrollTopItem) { forgetScrollState(); } if (leaveItems) { Auth().data().notifyHistoryUnloaded(this); } else { setLastMessage(nullptr); notifies.clear(); Auth().data().notifyHistoryCleared(this); } blocks.clear(); if (leaveItems) { lastKeyboardInited = false; } else { changeUnreadCount(-unreadCount()); if (auto channel = peer->asChannel()) { channel->clearPinnedMessage(); if (const auto feed = channel->feed()) { // Should be after setLastMessage(nullptr); feed->historyCleared(this); } } clearLastKeyboard(); } Auth().data().notifyHistoryChangeDelayed(this); _loadedAtTop = false; _loadedAtBottom = !leaveItems; forgetScrollState(); if (const auto chat = peer->asChat()) { chat->lastAuthors.clear(); chat->markupSenders.clear(); } else if (const auto channel = peer->asMegagroup()) { channel->mgInfo->markupSenders.clear(); } } void History::clearUpTill(MsgId availableMinId) { auto minId = minMsgId(); if (!minId || minId > availableMinId) { return; } do { const auto item = blocks.front()->messages.front()->data(); const auto itemId = item->id; if (IsServerMsgId(itemId) && itemId >= availableMinId) { if (itemId == availableMinId) { auto fromId = 0; auto replyToId = 0; item->applyEdition(MTP_messageService( MTP_flags(0), MTP_int(itemId), MTP_int(fromId), peerToMTP(peer->id), MTP_int(replyToId), MTP_int(item->date()), MTP_messageActionHistoryClear() ).c_messageService()); } break; } item->destroy(); } while (!isEmpty()); if (!lastMessageKnown()) { Auth().api().requestDialogEntry(this); } Auth().data().sendHistoryChangeNotifications(); } void History::applyGroupAdminChanges( const base::flat_map &changes) { for (const auto &block : blocks) { for (const auto &message : block->messages) { message->data()->applyGroupAdminChanges(changes); } } } void History::changedInChatListHook(Dialogs::Mode list, bool added) { if (list == Dialogs::Mode::All && unreadCount()) { const auto delta = added ? unreadCount() : -unreadCount(); App::histories().unreadIncrement(delta, mute()); } } void History::changedChatListPinHook() { Notify::peerUpdatedDelayed( peer, Notify::PeerUpdate::Flag::PinnedChanged); } void History::removeBlock(not_null block) { Expects(block->messages.empty()); if (_buildingFrontBlock && block == _buildingFrontBlock->block) { _buildingFrontBlock->block = nullptr; } int index = block->indexInHistory(); blocks.erase(blocks.begin() + index); if (index < blocks.size()) { for (int i = index, l = blocks.size(); i < l; ++i) { blocks[i]->setIndexInHistory(i); } blocks[index]->messages.front()->previousInBlocksChanged(); } else if (!blocks.empty() && !blocks.back()->messages.empty()) { blocks.back()->messages.back()->nextInBlocksRemoved(); } } History::~History() = default; HistoryBlock::HistoryBlock(not_null history) : _history(history) { } int HistoryBlock::resizeGetHeight(int newWidth, bool resizeAllItems) { auto y = 0; for (const auto &message : messages) { message->setY(y); if (resizeAllItems || message->pendingResize()) { y += message->resizeGetHeight(newWidth); } else { y += message->height(); } } _height = y; return _height; } void HistoryBlock::remove(not_null view) { Expects(view->block() == this); _history->mainViewRemoved(this, view); const auto blockIndex = indexInHistory(); const auto itemIndex = view->indexInBlock(); const auto item = view->data(); item->clearMainView(); messages.erase(messages.begin() + itemIndex); for (auto i = itemIndex, l = int(messages.size()); i < l; ++i) { messages[i]->setIndexInBlock(i); } if (messages.empty()) { // Deletes this. _history->removeBlock(this); } else if (itemIndex < messages.size()) { messages[itemIndex]->previousInBlocksChanged(); } else if (blockIndex + 1 < _history->blocks.size()) { _history->blocks[blockIndex + 1]->messages.front()->previousInBlocksChanged(); } else if (!_history->blocks.empty() && !_history->blocks.back()->messages.empty()) { _history->blocks.back()->messages.back()->nextInBlocksRemoved(); } } void HistoryBlock::refreshView(not_null view) { Expects(view->block() == this); const auto item = view->data(); auto refreshed = item->createView(HistoryInner::ElementDelegate()); auto blockIndex = indexInHistory(); auto itemIndex = view->indexInBlock(); _history->viewReplaced(view, refreshed.get()); messages[itemIndex] = std::move(refreshed); messages[itemIndex]->attachToBlock(this, itemIndex); if (itemIndex + 1 < messages.size()) { messages[itemIndex + 1]->previousInBlocksChanged(); } else if (blockIndex + 1 < _history->blocks.size()) { _history->blocks[blockIndex + 1]->messages.front()->previousInBlocksChanged(); } else if (!_history->blocks.empty() && !_history->blocks.back()->messages.empty()) { _history->blocks.back()->messages.back()->nextInBlocksRemoved(); } } HistoryBlock::~HistoryBlock() = default;