diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 559738afd..fee34723f 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -147,6 +147,8 @@ PRIVATE api/api_text_entities.h api/api_toggling_media.cpp api/api_toggling_media.h + api/api_unread_things.cpp + api/api_unread_things.h api/api_updates.cpp api/api_updates.h api/api_user_privacy.cpp @@ -686,6 +688,8 @@ PRIVATE history/history_message.h history/history_service.cpp history/history_service.h + history/history_unread_things.cpp + history/history_unread_things.h history/history_widget.cpp history/history_widget.h info/info_content_widget.cpp diff --git a/Telegram/Resources/icons/dialogs/dialogs_reaction.png b/Telegram/Resources/icons/dialogs/dialogs_reaction.png new file mode 100644 index 000000000..4ae8b8f16 Binary files /dev/null and b/Telegram/Resources/icons/dialogs/dialogs_reaction.png differ diff --git a/Telegram/Resources/icons/dialogs/dialogs_reaction@2x.png b/Telegram/Resources/icons/dialogs/dialogs_reaction@2x.png new file mode 100644 index 000000000..47c7e152f Binary files /dev/null and b/Telegram/Resources/icons/dialogs/dialogs_reaction@2x.png differ diff --git a/Telegram/Resources/icons/dialogs/dialogs_reaction@3x.png b/Telegram/Resources/icons/dialogs/dialogs_reaction@3x.png new file mode 100644 index 000000000..bb5012550 Binary files /dev/null and b/Telegram/Resources/icons/dialogs/dialogs_reaction@3x.png differ diff --git a/Telegram/Resources/icons/history_unread_reaction.png b/Telegram/Resources/icons/history_unread_reaction.png new file mode 100644 index 000000000..3a6386dd2 Binary files /dev/null and b/Telegram/Resources/icons/history_unread_reaction.png differ diff --git a/Telegram/Resources/icons/history_unread_reaction@2x.png b/Telegram/Resources/icons/history_unread_reaction@2x.png new file mode 100644 index 000000000..b4581adb3 Binary files /dev/null and b/Telegram/Resources/icons/history_unread_reaction@2x.png differ diff --git a/Telegram/Resources/icons/history_unread_reaction@3x.png b/Telegram/Resources/icons/history_unread_reaction@3x.png new file mode 100644 index 000000000..c1f26c076 Binary files /dev/null and b/Telegram/Resources/icons/history_unread_reaction@3x.png differ diff --git a/Telegram/SourceFiles/api/api_unread_things.cpp b/Telegram/SourceFiles/api/api_unread_things.cpp new file mode 100644 index 000000000..195cb35bc --- /dev/null +++ b/Telegram/SourceFiles/api/api_unread_things.cpp @@ -0,0 +1,138 @@ +/* +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 "api/api_unread_things.h" + +#include "data/data_peer.h" +#include "data/data_channel.h" +#include "data/data_session.h" +#include "main/main_session.h" +#include "history/history.h" +#include "history/history_item.h" +#include "history/history_unread_things.h" +#include "apiwrap.h" + +namespace Api { +namespace { + +constexpr auto kPreloadIfLess = 5; +constexpr auto kFirstRequestLimit = 10; +constexpr auto kNextRequestLimit = 100; + +} // namespace + +UnreadThings::UnreadThings(not_null api) : _api(api) { +} + +bool UnreadThings::trackMentions(PeerData *peer) const { + return peer && (peer->isChat() || peer->isMegagroup()); +} + +bool UnreadThings::trackReactions(PeerData *peer) const { + return trackMentions(peer) || (peer && peer->isUser()); +} + +void UnreadThings::preloadEnough(History *history) { + if (!history) { + return; + } + if (trackMentions(history->peer)) { + preloadEnoughMentions(history); + } + if (trackReactions(history->peer)) { + preloadEnoughReactions(history); + } +} + +void UnreadThings::mediaAndMentionsRead( + const base::flat_set &readIds, + ChannelData *channel) { + for (const auto &msgId : readIds) { + _api->requestMessageData(channel, msgId, [=] { + const auto item = channel + ? _api->session().data().message(channel->id, msgId) + : _api->session().data().nonChannelMessage(msgId); + if (item && item->mentionsMe()) { + item->markMediaAndMentionRead(); + } + }); + } +} + +void UnreadThings::preloadEnoughMentions(not_null history) { + const auto fullCount = history->unreadMentions().count(); + const auto loadedCount = history->unreadMentions().loadedCount(); + const auto allLoaded = (fullCount >= 0) && (loadedCount >= fullCount); + if (fullCount >= 0 && loadedCount < kPreloadIfLess && !allLoaded) { + requestMentions(history, loadedCount); + } +} + +void UnreadThings::preloadEnoughReactions(not_null history) { + const auto fullCount = history->unreadReactions().count(); + const auto loadedCount = history->unreadReactions().loadedCount(); + const auto allLoaded = (fullCount >= 0) && (loadedCount >= fullCount); + if (fullCount >= 0 && loadedCount < kPreloadIfLess && !allLoaded) { + requestReactions(history, loadedCount); + } +} + +void UnreadThings::requestMentions(not_null history, int loaded) { + if (_mentionsRequests.contains(history)) { + return; + } + const auto offsetId = std::max( + history->unreadMentions().maxLoaded(), + MsgId(1)); + const auto limit = loaded ? kNextRequestLimit : kFirstRequestLimit; + const auto addOffset = loaded ? -(limit + 1) : -limit; + const auto maxId = 0; + const auto minId = 0; + const auto requestId = _api->request(MTPmessages_GetUnreadMentions( + history->peer->input, + MTP_int(offsetId), + MTP_int(addOffset), + MTP_int(limit), + MTP_int(maxId), + MTP_int(minId) + )).done([=](const MTPmessages_Messages &result) { + _mentionsRequests.remove(history); + history->unreadMentions().addSlice(result); + }).fail([=] { + _mentionsRequests.remove(history); + }).send(); + _mentionsRequests.emplace(history, requestId); +} + +void UnreadThings::requestReactions(not_null history, int loaded) { + if (_reactionsRequests.contains(history)) { + return; + } + const auto offsetId = std::max( + history->unreadMentions().maxLoaded(), + MsgId(1)); + const auto limit = loaded ? kNextRequestLimit : kFirstRequestLimit; + const auto addOffset = loaded ? -(limit + 1) : -limit; + const auto maxId = 0; + const auto minId = 0; + const auto requestId = _api->request(MTPmessages_GetUnreadReactions( + history->peer->input, + MTP_int(offsetId), + MTP_int(addOffset), + MTP_int(limit), + MTP_int(maxId), + MTP_int(minId) + )).done([=](const MTPmessages_Messages &result) { + _reactionsRequests.remove(history); + history->unreadReactions().addSlice(result); + }).fail([this, history] { + _reactionsRequests.remove(history); + }).send(); + _reactionsRequests.emplace(history, requestId); +} + +} // namespace UnreadThings diff --git a/Telegram/SourceFiles/api/api_unread_things.h b/Telegram/SourceFiles/api/api_unread_things.h new file mode 100644 index 000000000..4f6f42364 --- /dev/null +++ b/Telegram/SourceFiles/api/api_unread_things.h @@ -0,0 +1,44 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +class History; +class ApiWrap; +class PeerData; +class ChannelData; + +namespace Api { + +class UnreadThings final { +public: + explicit UnreadThings(not_null api); + + [[nodiscard]] bool trackMentions(PeerData *peer) const; + [[nodiscard]] bool trackReactions(PeerData *peer) const; + + void preloadEnough(History *history); + + void mediaAndMentionsRead( + const base::flat_set &readIds, + ChannelData *channel = nullptr); + +private: + void preloadEnoughMentions(not_null history); + void preloadEnoughReactions(not_null history); + + void requestMentions(not_null history, int loaded); + void requestReactions(not_null history, int loaded); + + const not_null _api; + + base::flat_map, mtpRequestId> _mentionsRequests; + base::flat_map, mtpRequestId> _reactionsRequests; + +}; + +} // namespace Api diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index 4f138a510..9359a0d73 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_chat_participants.h" #include "api/api_text_entities.h" #include "api/api_user_privacy.h" +#include "api/api_unread_things.h" #include "main/main_session.h" #include "main/main_account.h" #include "mtproto/mtp_instance.h" @@ -1178,25 +1179,29 @@ void Updates::applyUpdateNoPtsCheck(const MTPUpdate &update) { case mtpc_updateReadMessagesContents: { const auto &d = update.c_updateReadMessagesContents(); - auto possiblyReadMentions = base::flat_set(); + auto unknownReadIds = base::flat_set(); for (const auto &msgId : d.vmessages().v) { if (const auto item = _session->data().nonChannelMessage(msgId.v)) { + const auto unreadForPeer = item->isUnreadMedia() + || item->isUnreadMention(); + const auto unreadForMe = item->hasUnreadReaction(); if (item->isUnreadMedia() || item->isUnreadMention()) { - item->markMediaRead(); + item->markMediaAndMentionRead(); _session->data().requestItemRepaint(item); if (item->out() && item->history()->peer->isUser() && !requestingDifference()) { - item->history()->peer->asUser()->madeAction(base::unixtime::now()); + item->history()->peer->asUser()->madeAction( + base::unixtime::now()); } } } else { // Perhaps it was an unread mention! - possiblyReadMentions.insert(msgId.v); + unknownReadIds.insert(msgId.v); } } - session().api().checkForUnreadMentions(possiblyReadMentions); + session().api().unreadThings().mediaAndMentionsRead(unknownReadIds); } break; case mtpc_updateReadHistoryInbox: { @@ -1565,19 +1570,21 @@ void Updates::feedUpdate(const MTPUpdate &update) { } return; } - auto possiblyReadMentions = base::flat_set(); + auto unknownReadIds = base::flat_set(); for (const auto &msgId : d.vmessages().v) { if (auto item = session().data().message(channel->id, msgId.v)) { if (item->isUnreadMedia() || item->isUnreadMention()) { - item->markMediaRead(); + item->markMediaAndMentionRead(); session().data().requestItemRepaint(item); } } else { // Perhaps it was an unread mention! - possiblyReadMentions.insert(msgId.v); + unknownReadIds.insert(msgId.v); } } - session().api().checkForUnreadMentions(possiblyReadMentions, channel); + session().api().unreadThings().mediaAndMentionsRead( + unknownReadIds, + channel); } break; // Edited messages. diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index b9f47daf3..da47b0efd 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -26,6 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_user_privacy.h" #include "api/api_views.h" #include "api/api_confirm_phone.h" +#include "api/api_unread_things.h" #include "data/stickers/data_stickers.h" #include "data/data_drafts.h" #include "data/data_changes.h" @@ -96,9 +97,6 @@ constexpr auto kSaveCloudDraftTimeout = 1000; constexpr auto kTopPromotionInterval = TimeId(60 * 60); constexpr auto kTopPromotionMinDelay = TimeId(10); constexpr auto kSmallDelayMs = 5; -constexpr auto kUnreadMentionsPreloadIfLess = 5; -constexpr auto kUnreadMentionsFirstRequestLimit = 10; -constexpr auto kUnreadMentionsNextRequestLimit = 100; constexpr auto kSharedMediaLimit = 100; constexpr auto kReadFeaturedSetsTimeout = crl::time(1000); constexpr auto kFileLoaderQueueStopTimeout = crl::time(5000); @@ -142,7 +140,8 @@ ApiWrap::ApiWrap(not_null session) , _confirmPhone(std::make_unique(this)) , _peerPhoto(std::make_unique(this)) , _polls(std::make_unique(this)) -, _chatParticipants(std::make_unique(this)) { +, _chatParticipants(std::make_unique(this)) +, _unreadThings(std::make_unique(this)) { crl::on_main(session, [=] { // You can't use _session->lifetime() in the constructor, // only queued, because it is not constructed yet. @@ -1287,7 +1286,7 @@ void ApiWrap::migrateFail(not_null peer, const QString &error) { } } -void ApiWrap::markMediaRead( +void ApiWrap::markContentsRead( const base::flat_set> &items) { auto markedIds = QVector(); auto channelMarkedIds = base::flat_map< @@ -1295,12 +1294,7 @@ void ApiWrap::markMediaRead( QVector>(); markedIds.reserve(items.size()); for (const auto &item : items) { - if ((!item->isUnreadMedia() || item->out()) - && !item->isUnreadMention()) { - continue; - } - item->markMediaRead(); - if (!item->isRegular()) { + if (!item->markContentsRead() || !item->isRegular()) { continue; } if (const auto channel = item->history()->peer->asChannel()) { @@ -1324,13 +1318,8 @@ void ApiWrap::markMediaRead( } } -void ApiWrap::markMediaRead(not_null item) { - if ((!item->isUnreadMedia() || item->out()) - && !item->isUnreadMention()) { - return; - } - item->markMediaRead(); - if (!item->isRegular()) { +void ApiWrap::markContentsRead(not_null item) { + if (!item->markContentsRead() || !item->isRegular()) { return; } const auto ids = MTP_vector(1, MTP_int(item->id)); @@ -2910,45 +2899,6 @@ void ApiWrap::jumpToHistoryDate(not_null peer, const QDate &date) { } } -void ApiWrap::preloadEnoughUnreadMentions(not_null history) { - auto fullCount = history->getUnreadMentionsCount(); - auto loadedCount = history->getUnreadMentionsLoadedCount(); - auto allLoaded = (fullCount >= 0) ? (loadedCount >= fullCount) : false; - if (fullCount < 0 || loadedCount >= kUnreadMentionsPreloadIfLess || allLoaded) { - return; - } - if (_unreadMentionsRequests.contains(history)) { - return; - } - auto offsetId = loadedCount ? history->getMaxLoadedUnreadMention() : 1; - auto limit = loadedCount ? kUnreadMentionsNextRequestLimit : kUnreadMentionsFirstRequestLimit; - auto addOffset = loadedCount ? -(limit + 1) : -limit; - auto maxId = 0; - auto minId = 0; - auto requestId = request(MTPmessages_GetUnreadMentions(history->peer->input, MTP_int(offsetId), MTP_int(addOffset), MTP_int(limit), MTP_int(maxId), MTP_int(minId))).done([this, history](const MTPmessages_Messages &result) { - _unreadMentionsRequests.remove(history); - history->addUnreadMentionsSlice(result); - }).fail([this, history] { - _unreadMentionsRequests.remove(history); - }).send(); - _unreadMentionsRequests.emplace(history, requestId); -} - -void ApiWrap::checkForUnreadMentions( - const base::flat_set &possiblyReadMentions, - ChannelData *channel) { - for (const auto &msgId : possiblyReadMentions) { - requestMessageData(channel, msgId, [=] { - const auto item = channel - ? _session->data().message(channel->id, msgId) - : _session->data().nonChannelMessage(msgId); - if (item && item->mentionsMe()) { - item->markMediaRead(); - } - }); - } -} - void ApiWrap::requestSharedMediaCount( not_null peer, Storage::SharedMediaType type) { @@ -4146,3 +4096,7 @@ Api::Polls &ApiWrap::polls() { Api::ChatParticipants &ApiWrap::chatParticipants() { return *_chatParticipants; } + +Api::UnreadThings &ApiWrap::unreadThings() { + return *_unreadThings; +} diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index 737672918..f26ea4dd5 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -67,6 +67,7 @@ class ConfirmPhone; class PeerPhoto; class Polls; class ChatParticipants; +class UnreadThings; namespace details { @@ -206,8 +207,9 @@ public: FnMut)> done, Fn fail = nullptr); - void markMediaRead(const base::flat_set> &items); - void markMediaRead(not_null item); + void markContentsRead( + const base::flat_set> &items); + void markContentsRead(not_null item); void deleteAllFromParticipant( not_null channel, @@ -250,11 +252,6 @@ public: void jumpToDate(Dialogs::Key chat, const QDate &date); - void preloadEnoughUnreadMentions(not_null history); - void checkForUnreadMentions( - const base::flat_set &possiblyReadMentions, - ChannelData *channel = nullptr); - using SliceType = Data::LoadDirection; void requestSharedMedia( not_null peer, @@ -356,6 +353,7 @@ public: [[nodiscard]] Api::PeerPhoto &peerPhoto(); [[nodiscard]] Api::Polls &polls(); [[nodiscard]] Api::ChatParticipants &chatParticipants(); + [[nodiscard]] Api::UnreadThings &unreadThings(); void updatePrivacyLastSeens(); @@ -562,8 +560,6 @@ private: mtpRequestId _contactsRequestId = 0; mtpRequestId _contactsStatusesRequestId = 0; - base::flat_map, mtpRequestId> _unreadMentionsRequests; - base::flat_set, SharedMediaType, @@ -636,6 +632,7 @@ private: const std::unique_ptr _peerPhoto; const std::unique_ptr _polls; const std::unique_ptr _chatParticipants; + const std::unique_ptr _unreadThings; mtpRequestId _wallPaperRequestId = 0; QString _wallPaperSlug; diff --git a/Telegram/SourceFiles/chat_helpers/send_context_menu.cpp b/Telegram/SourceFiles/chat_helpers/send_context_menu.cpp index 16ac9378f..93ae5f1e1 100644 --- a/Telegram/SourceFiles/chat_helpers/send_context_menu.cpp +++ b/Telegram/SourceFiles/chat_helpers/send_context_menu.cpp @@ -183,7 +183,11 @@ void SetupUnreadMentionsMenu( } return base::EventFilterResult::Continue; }); +} +void SetupUnreadReactionsMenu( + not_null button, + Fn currentPeer) { } } // namespace SendMenu diff --git a/Telegram/SourceFiles/chat_helpers/send_context_menu.h b/Telegram/SourceFiles/chat_helpers/send_context_menu.h index 1b79feffb..3cddc9a11 100644 --- a/Telegram/SourceFiles/chat_helpers/send_context_menu.h +++ b/Telegram/SourceFiles/chat_helpers/send_context_menu.h @@ -54,4 +54,8 @@ void SetupUnreadMentionsMenu( not_null button, Fn currentPeer); +void SetupUnreadReactionsMenu( + not_null button, + Fn currentPeer); + } // namespace SendMenu diff --git a/Telegram/SourceFiles/data/data_changes.h b/Telegram/SourceFiles/data/data_changes.h index 9b64579ad..ee9e58d44 100644 --- a/Telegram/SourceFiles/data/data_changes.h +++ b/Telegram/SourceFiles/data/data_changes.h @@ -116,18 +116,19 @@ struct HistoryUpdate { TopPromoted = (1U << 2), Folder = (1U << 3), UnreadMentions = (1U << 4), - ClientSideMessages = (1U << 5), - ChatOccupied = (1U << 6), - MessageSent = (1U << 7), - ScheduledSent = (1U << 8), - ForwardDraft = (1U << 9), - OutboxRead = (1U << 10), - BotKeyboard = (1U << 11), - CloudDraft = (1U << 12), - LocalDraftSet = (1U << 13), - PinnedMessages = (1U << 14), + UnreadReactions = (1U << 5), + ClientSideMessages = (1U << 6), + ChatOccupied = (1U << 7), + MessageSent = (1U << 8), + ScheduledSent = (1U << 9), + ForwardDraft = (1U << 10), + OutboxRead = (1U << 11), + BotKeyboard = (1U << 12), + CloudDraft = (1U << 13), + LocalDraftSet = (1U << 14), + PinnedMessages = (1U << 15), - LastUsedBit = (1U << 14), + LastUsedBit = (1U << 15), }; using Flags = base::flags; friend inline constexpr auto is_flag_type(Flag) { return true; } diff --git a/Telegram/SourceFiles/data/data_chat_filters.cpp b/Telegram/SourceFiles/data/data_chat_filters.cpp index 93628a42c..68812aaa8 100644 --- a/Telegram/SourceFiles/data/data_chat_filters.cpp +++ b/Telegram/SourceFiles/data/data_chat_filters.cpp @@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_folder.h" #include "data/data_histories.h" #include "dialogs/dialogs_main_list.h" +#include "history/history_unread_things.h" #include "ui/ui_utility.h" #include "main/main_session.h" #include "apiwrap.h" @@ -202,13 +203,13 @@ bool ChatFilter::contains(not_null history) const { || ((_flags & flag) && (!(_flags & Flag::NoMuted) || !history->mute() - || (history->hasUnreadMentions() + || (history->unreadMentions().has() && history->folderKnown() && !history->folder())) && (!(_flags & Flag::NoRead) || history->unreadCount() || history->unreadMark() - || history->hasUnreadMentions() + || history->unreadMentions().has() || history->fakeUnreadWhileOpened()) && (!(_flags & Flag::NoArchived) || (history->folderKnown() && !history->folder()))) diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 9cae7df26..9ed93566c 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -1303,7 +1303,14 @@ void Session::photoLoadFail( void Session::markMediaRead(not_null document) { const auto i = _documentItems.find(document); if (i != end(_documentItems)) { - _session->api().markMediaRead({ begin(i->second), end(i->second) }); + auto items = base::flat_set>(); + items.reserve(i->second.size()); + for (const auto &item : i->second) { + if (item->isUnreadMention() || item->isIncomingUnreadMedia()) { + items.emplace(item); + } + } + _session->api().markContentsRead(items); } } diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp index 7eaf3aa08..8fa508f5d 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp @@ -24,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "history/view/history_view_send_action.h" #include "history/view/history_view_item_preview.h" +#include "history/history_unread_things.h" #include "history/history_item_components.h" #include "history/history_item.h" #include "history/history.h" @@ -778,7 +779,7 @@ void RowPainter::paint( : QDateTime(); }(); const auto displayMentionBadge = history - ? history->hasUnreadMentions() + ? history->unreadMentions().has() : false; const auto displayUnreadCounter = [&] { if (displayMentionBadge @@ -941,7 +942,7 @@ void RowPainter::paint( const auto unreadMuted = history->chatListMutedBadge(); const auto mentionMuted = (history->folder() != nullptr); const auto displayMentionBadge = displayUnreadInfo - && history->hasUnreadMentions(); + && history->unreadMentions().has(); const auto displayUnreadCounter = (unreadCount > 0); const auto displayUnreadMark = !displayUnreadCounter && !displayMentionBadge diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index cba789212..e3b4af378 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_service.h" #include "history/history_item_components.h" #include "history/history_inner_widget.h" +#include "history/history_unread_things.h" #include "dialogs/dialogs_indexed_list.h" #include "data/stickers/data_stickers.h" #include "data/data_drafts.h" @@ -128,11 +129,11 @@ void History::popNotification(ItemNotification notification) { } bool History::hasPendingResizedItems() const { - return _flags & Flag::f_has_pending_resized_items; + return _flags & Flag::HasPendingResizedItems; } void History::setHasPendingResizedItems() { - _flags |= Flag::f_has_pending_resized_items; + _flags |= Flag::HasPendingResizedItems; } void History::itemRemoved(not_null item) { @@ -691,106 +692,40 @@ not_null History::addNewLocalMessage( true); } -void History::setUnreadMentionsCount(int count) { - const auto had = _unreadMentionsCount && (*_unreadMentionsCount > 0); - if (_unreadMentions.size() > count) { - LOG(("API Warning: real mentions count is greater than received mentions count")); - count = _unreadMentions.size(); - } - _unreadMentionsCount = count; - const auto has = (count > 0); - if (has != had) { - owner().chatsFilters().refreshHistory(this); - updateChatListEntry(); - } +void History::setUnreadThingsKnown() { + _flags &= ~Flag::UnreadThingsKnown; } -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) { - _unreadMentions.insert(msgId); - setUnreadMentionsCount(*_unreadMentionsCount + 1); - 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 && *_unreadMentionsCount > 0) { - setUnreadMentionsCount(*_unreadMentionsCount - 1); - } - session().changes().historyUpdated(this, UpdateFlag::UnreadMentions); -} - -void History::addUnreadMentionsSlice(const MTPmessages_Messages &result) { - auto count = 0; - auto messages = (const QVector*)nullptr; - auto getMessages = [&](auto &list) { - owner().processUsers(list.vusers()); - owner().processChats(list.vchats()); - return &list.vmessages().v; +HistoryUnreadThings::Proxy History::unreadMentions() { + return { + this, + _unreadThings, + HistoryUnreadThings::Type::Mentions, + !!(_flags & Flag::UnreadThingsKnown), }; - 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; +HistoryUnreadThings::ConstProxy History::unreadMentions() const { + return { + _unreadThings ? &_unreadThings->mentions : nullptr, + !!(_flags & Flag::UnreadThingsKnown), + }; +} - 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; +HistoryUnreadThings::Proxy History::unreadReactions() { + return { + this, + _unreadThings, + HistoryUnreadThings::Type::Reactions, + !!(_flags & Flag::UnreadThingsKnown), + }; +} - case mtpc_messages_messagesNotModified: { - LOG(("API Error: received messages.messagesNotModified! (History::addUnreadMentionsSlice)")); - } break; - - default: Unexpected("type in History::addUnreadMentionsSlice"); - } - - auto added = false; - if (messages) { - const auto localFlags = MessageFlags(); - const auto type = NewMessageType::Existing; - for (const auto &message : *messages) { - const auto item = addNewMessage( - IdFromMessage(message), - message, - localFlags, - type); - if (item && item->isUnreadMention()) { - _unreadMentions.insert(item->id); - added = true; - } - } - } - if (!added) { - count = _unreadMentions.size(); - } - setUnreadMentionsCount(count); - session().changes().historyUpdated(this, UpdateFlag::UnreadMentions); +HistoryUnreadThings::ConstProxy History::unreadReactions() const { + return { + _unreadThings ? &_unreadThings->reactions : nullptr, + !!(_flags & Flag::UnreadThingsKnown), + }; } not_null History::addNewToBack( @@ -1368,7 +1303,7 @@ void History::addItemsToLists( markupSenders = &peer->asChannel()->mgInfo->markupSenders; } for (const auto &item : ranges::views::reverse(items)) { - item->addToUnreadMentions(UnreadMentionType::Existing); + item->addToUnreadThings(HistoryUnreadThings::AddType::Existing); if (item->from()->id) { if (lastAuthors) { // chats if (auto user = item->from()->asUser()) { @@ -1433,7 +1368,7 @@ void History::checkAddAllToUnreadMentions() { for (const auto &block : blocks) { for (const auto &message : block->messages) { const auto item = message->data(); - item->addToUnreadMentions(UnreadMentionType::Existing); + item->addToUnreadThings(HistoryUnreadThings::AddType::Existing); } } } @@ -1754,7 +1689,7 @@ void History::setFakeUnreadWhileOpened(bool enabled) { && (!inChatList() || (!unreadCount() && !unreadMark() - && !hasUnreadMentions())))) { + && !unreadMentions().has())))) { return; } _fakeUnreadWhileOpened = enabled; @@ -2601,7 +2536,8 @@ void History::applyDialog( data.vread_outbox_max_id().v); applyDialogTopMessage(data.vtop_message().v); setUnreadMark(data.is_unread_mark()); - setUnreadMentionsCount(data.vunread_mentions_count().v); + unreadMentions().setCount(data.vunread_mentions_count().v); + unreadReactions().setCount(data.vunread_reactions_count().v); if (const auto channel = peer->asChannel()) { if (const auto pts = data.vpts()) { channel->ptsReceived(pts->v); @@ -2832,7 +2768,7 @@ void History::resizeToWidth(int newWidth) { if (!resizeAllItems && !hasPendingResizedItems()) { return; } - _flags &= ~(Flag::f_has_pending_resized_items); + _flags &= ~(Flag::HasPendingResizedItems); _width = newWidth; int y = 0; @@ -2845,7 +2781,7 @@ void History::resizeToWidth(int newWidth) { void History::forceFullResize() { _width = 0; - _flags |= Flag::f_has_pending_resized_items; + _flags |= Flag::HasPendingResizedItems; } not_null History::migrateToOrMe() const { diff --git a/Telegram/SourceFiles/history/history.h b/Telegram/SourceFiles/history/history.h index 122b22e46..3f8398a37 100644 --- a/Telegram/SourceFiles/history/history.h +++ b/Telegram/SourceFiles/history/history.h @@ -27,6 +27,13 @@ class HistoryService; struct HistoryMessageMarkupData; class HistoryMainElementDelegateMixin; +namespace HistoryUnreadThings { +enum class AddType; +struct All; +class Proxy; +class ConstProxy; +} // namespace HistoryUnreadThings + namespace Main { class Session; } // namespace Main @@ -71,11 +78,6 @@ enum class NewMessageType { Existing, }; -enum class UnreadMentionType { - New, // when new message is added to history - Existing, // when some messages slice was received -}; - enum class ItemNotificationType { Message, Reaction, @@ -333,25 +335,11 @@ public: void clearLastKeyboard(); - int getUnreadMentionsLoadedCount() const { - return _unreadMentions.size(); - } - MsgId getMinLoadedUnreadMention() const { - return _unreadMentions.empty() ? 0 : _unreadMentions.front(); - } - MsgId getMaxLoadedUnreadMention() const { - return _unreadMentions.empty() ? 0 : _unreadMentions.back(); - } - int getUnreadMentionsCount(int notLoadedValue = -1) const { - return _unreadMentionsCount ? *_unreadMentionsCount : notLoadedValue; - } - bool hasUnreadMentions() const { - return (getUnreadMentionsCount() > 0); - } - void setUnreadMentionsCount(int count); - bool addToUnreadMentions(MsgId msgId, UnreadMentionType type); - void eraseFromUnreadMentions(MsgId msgId); - void addUnreadMentionsSlice(const MTPmessages_Messages &result); + void setUnreadThingsKnown(); + [[nodiscard]] HistoryUnreadThings::Proxy unreadMentions(); + [[nodiscard]] HistoryUnreadThings::ConstProxy unreadMentions() const; + [[nodiscard]] HistoryUnreadThings::Proxy unreadReactions(); + [[nodiscard]] HistoryUnreadThings::ConstProxy unreadReactions() const; Data::Draft *draft(Data::DraftKey key) const; void setDraft(Data::DraftKey key, std::unique_ptr &&draft); @@ -493,7 +481,8 @@ private: friend class HistoryBlock; enum class Flag { - f_has_pending_resized_items = (1 << 0), + HasPendingResizedItems = (1 << 0), + UnreadThingsKnown = (1 << 1), }; using Flags = base::flags; friend inline constexpr auto is_flag_type(Flag) { @@ -622,12 +611,11 @@ private: std::optional _inboxReadBefore; std::optional _outboxReadBefore; std::optional _unreadCount; - std::optional _unreadMentionsCount; - base::flat_set _unreadMentions; std::optional _lastMessage; std::optional _lastServerMessage; base::flat_set> _clientSideMessages; std::unordered_set> _messages; + std::unique_ptr _unreadThings; // This almost always is equal to _lastMessage. The only difference is // for a group that migrated to a supergroup. Then _lastMessage can diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 53687c464..9eae92ee3 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -1019,7 +1019,8 @@ void HistoryInner::paintEvent(QPaintEvent *e) { if (item->hasViews()) { session().api().views().scheduleIncrement(item); } - if (item->isUnreadMention() && !item->isUnreadMedia()) { + if (item->isUnreadMention() + && !item->isUnreadMedia()) { readMentions.insert(item); _widget->enqueueMessageHighlight(view); } @@ -1051,7 +1052,7 @@ void HistoryInner::paintEvent(QPaintEvent *e) { } if (!readMentions.empty() && _widget->doWeReadMentions()) { - session().api().markMediaRead(readMentions); + session().api().markContentsRead(readMentions); } if (mtop >= 0 || htop >= 0) { diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 48e51581d..40a694c46 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -12,10 +12,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_element.h" #include "history/view/history_view_item_preview.h" #include "history/view/history_view_service_message.h" -#include "history/history_item_components.h" #include "history/view/media/history_view_media_grouped.h" +#include "history/history_item_components.h" #include "history/history_service.h" #include "history/history_message.h" +#include "history/history_unread_things.h" #include "history/history.h" #include "mtproto/mtproto_config.h" #include "media/clip/media_clip_reader.h" @@ -332,7 +333,11 @@ bool HistoryItem::hasUnreadMediaFlag() const { } bool HistoryItem::isUnreadMention() const { - return mentionsMe() && (_flags & MessageFlag::MediaIsUnread); + return !out() && mentionsMe() && (_flags & MessageFlag::MediaIsUnread); +} + +bool HistoryItem::hasUnreadReaction() const { + return false; } bool HistoryItem::mentionsMe() const { @@ -356,15 +361,34 @@ bool HistoryItem::isUnreadMedia() const { return false; } -void HistoryItem::markMediaRead() { +bool HistoryItem::isIncomingUnreadMedia() const { + return !out() && isUnreadMedia(); +} + +void HistoryItem::markMediaAndMentionRead() { _flags &= ~MessageFlag::MediaIsUnread; if (mentionsMe()) { history()->updateChatListEntry(); - history()->eraseFromUnreadMentions(id); + history()->unreadMentions().erase(id); } } +void HistoryItem::markReactionsRead() { + +} + +bool HistoryItem::markContentsRead() { + if (hasUnreadReaction()) { + markReactionsRead(); + return true; + } else if (isUnreadMention() || isIncomingUnreadMedia()) { + markMediaAndMentionRead(); + return true; + } + return false; +} + void HistoryItem::setIsPinned(bool pinned) { const auto changed = (isPinned() != pinned); if (pinned) { @@ -526,7 +550,7 @@ void HistoryItem::clearMainView() { _mainView = nullptr; } -void HistoryItem::addToUnreadMentions(UnreadMentionType type) { +void HistoryItem::addToUnreadThings(HistoryUnreadThings::AddType type) { } void HistoryItem::applyEditionToHistoryCleared() { @@ -592,7 +616,7 @@ void HistoryItem::applySentMessage( void HistoryItem::indexAsNewItem() { if (isRegular()) { - addToUnreadMentions(UnreadMentionType::New); + addToUnreadThings(HistoryUnreadThings::AddType::New); if (const auto types = sharedMediaTypes()) { _history->session().storage().add(Storage::SharedMediaAddNew( _history->peer->id, diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index b446071b5..83f13960c 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -16,7 +16,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include -enum class UnreadMentionType; struct HistoryMessageReplyMarkup; class ReplyKeyboard; class HistoryMessage; @@ -50,6 +49,10 @@ namespace Window { class SessionController; } // namespace Window +namespace HistoryUnreadThings { +enum class AddType; +} // namespace HistoryUnreadThings + namespace HistoryView { struct TextState; struct StateRequest; @@ -140,9 +143,13 @@ public: void markClientSideAsRead(); [[nodiscard]] bool mentionsMe() const; [[nodiscard]] bool isUnreadMention() const; + [[nodiscard]] bool hasUnreadReaction() const; [[nodiscard]] bool isUnreadMedia() const; + [[nodiscard]] bool isIncomingUnreadMedia() const; [[nodiscard]] bool hasUnreadMediaFlag() const; - void markMediaRead(); + void markReactionsRead(); + void markMediaAndMentionRead(); + bool markContentsRead(); void setIsPinned(bool isPinned); // For edit media in history_message. @@ -274,7 +281,7 @@ public: virtual void contributeToSlowmode(TimeId realDate = 0) { } - virtual void addToUnreadMentions(UnreadMentionType type); + virtual void addToUnreadThings(HistoryUnreadThings::AddType type); virtual void destroyHistoryEntry() { } [[nodiscard]] virtual Storage::SharedMediaTypesMask sharedMediaTypes() const = 0; diff --git a/Telegram/SourceFiles/history/history_message.cpp b/Telegram/SourceFiles/history/history_message.cpp index ab004089e..edb10a470 100644 --- a/Telegram/SourceFiles/history/history_message.cpp +++ b/Telegram/SourceFiles/history/history_message.cpp @@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item_components.h" #include "history/history_location_manager.h" #include "history/history_service.h" +#include "history/history_unread_things.h" #include "history/view/history_view_service_message.h" #include "history/view/history_view_context_menu.h" // CopyPostLink. #include "history/view/history_view_spoiler_click_handler.h" @@ -1529,19 +1530,32 @@ void HistoryMessage::contributeToSlowmode(TimeId realDate) { } } -void HistoryMessage::addToUnreadMentions(UnreadMentionType type) { - if (isRegular() && isUnreadMention()) { - if (history()->addToUnreadMentions(id, type)) { +void HistoryMessage::addToUnreadThings(HistoryUnreadThings::AddType type) { + if (!isRegular()) { + return; + } + if (isUnreadMention()) { + if (history()->unreadMentions().add(id, type)) { history()->session().changes().historyUpdated( history(), Data::HistoryUpdate::Flag::UnreadMentions); } } + if (hasUnreadReaction()) { + if (history()->unreadReactions().add(id, type)) { + history()->session().changes().historyUpdated( + history(), + Data::HistoryUpdate::Flag::UnreadReactions); + } + } } void HistoryMessage::destroyHistoryEntry() { if (isUnreadMention()) { - history()->eraseFromUnreadMentions(id); + history()->unreadMentions().erase(id); + } + if (hasUnreadReaction()) { + history()->unreadReactions().erase(id); } if (const auto reply = Get()) { changeReplyToTopCounter(reply, -1); diff --git a/Telegram/SourceFiles/history/history_message.h b/Telegram/SourceFiles/history/history_message.h index eb02f0906..ca31472e5 100644 --- a/Telegram/SourceFiles/history/history_message.h +++ b/Telegram/SourceFiles/history/history_message.h @@ -175,7 +175,7 @@ public: void updateForwardedInfo(const MTPMessageFwdHeader *fwd) override; void contributeToSlowmode(TimeId realDate = 0) override; - void addToUnreadMentions(UnreadMentionType type) override; + void addToUnreadThings(HistoryUnreadThings::AddType type) override; void destroyHistoryEntry() override; [[nodiscard]] Storage::SharedMediaTypesMask sharedMediaTypes() const override; diff --git a/Telegram/SourceFiles/history/history_unread_things.cpp b/Telegram/SourceFiles/history/history_unread_things.cpp new file mode 100644 index 000000000..663ce371d --- /dev/null +++ b/Telegram/SourceFiles/history/history_unread_things.cpp @@ -0,0 +1,190 @@ +/* +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_unread_things.h" + +#include "data/data_session.h" +#include "data/data_changes.h" +#include "data/data_channel.h" +#include "data/data_chat_filters.h" +#include "history/history.h" +#include "history/history_item.h" +#include "main/main_session.h" + +namespace HistoryUnreadThings { + +void Proxy::setCount(int count) { + if (!_known) { + _history->setUnreadThingsKnown(); + } + if (!_data) { + if (!count) { + return; + } + createData(); + } + auto &list = resolveList(); + const auto loaded = list.loadedCount(); + if (loaded > count) { + LOG(("API Warning: " + "real count is greater than received unread count")); + count = loaded; + } + if (!count) { + const auto &other = (_type == Type::Mentions) + ? _data->reactions + : _data->mentions; + if (other.count(-1) == 0) { + _data = nullptr; + return; + } + } + + const auto had = (list.count() > 0); + list.setCount(count); + const auto has = (count > 0); + if (has != had) { + _history->owner().chatsFilters().refreshHistory(_history); + _history->updateChatListEntry(); + } +} + +bool Proxy::add(MsgId msgId, AddType type) { + const auto peer = _history->peer; + if (peer->isChannel() && !peer->isMegagroup()) { + return false; + } + + if (!_data) { + createData(); + } + auto &list = resolveList(); + const auto count = list.count(); + const auto loaded = list.loadedCount(); + const auto allLoaded = (count >= 0) && (loaded >= count); + if (allLoaded) { + if (type == AddType::New) { + list.insert(msgId); + setCount(count + 1); + return true; + } + } else if (loaded > 0 && type != AddType::New) { + list.insert(msgId); + return true; + } + return false; + +} + +void Proxy::erase(MsgId msgId) { + if (!_data) { + return; + } + auto &list = resolveList(); + list.erase(msgId); + if (const auto count = list.count(); count > 0) { + setCount(count - 1); + } + _history->session().changes().historyUpdated( + _history, + Data::HistoryUpdate::Flag::UnreadMentions); +} + +void Proxy::addSlice(const MTPmessages_Messages &slice) { + auto fullCount = slice.match([&]( + const MTPDmessages_messagesNotModified &) { + LOG(("API Error: received messages.messagesNotModified! " + "(Proxy::addSlice)")); + return 0; + }, [&](const MTPDmessages_messages &data) { + return int(data.vmessages().v.size()); + }, [&](const MTPDmessages_messagesSlice &data) { + return data.vcount().v; + }, [&](const MTPDmessages_channelMessages &data) { + if (_history->peer->isChannel()) { + _history->peer->asChannel()->ptsReceived(data.vpts().v); + } else { + LOG(("API Error: received messages.channelMessages when " + "no channel was passed! (Proxy::addSlice)")); + } + return data.vcount().v; + }); + + auto &owner = _history->owner(); + const auto messages = slice.match([&]( + const MTPDmessages_messagesNotModified &) { + LOG(("API Error: received messages.messagesNotModified! " + "(Proxy::addSlice)")); + return QVector(); + }, [&](const auto &data) { + owner.processUsers(data.vusers()); + owner.processChats(data.vchats()); + return data.vmessages().v; + }); + if (messages.isEmpty()) { + return; + } + + if (!_data) { + createData(); + } + auto added = false; + auto &list = resolveList(); + const auto localFlags = MessageFlags(); + const auto type = NewMessageType::Existing; + for (const auto &message : messages) { + const auto item = _history->addNewMessage( + IdFromMessage(message), + message, + localFlags, + type); + const auto is = [&] { + switch (_type) { + case Type::Mentions: return item->isUnreadMention(); + case Type::Reactions: return item->hasUnreadReaction(); + } + Unexpected("Type in Proxy::addSlice."); + }(); + if (is) { + list.insert(item->id); + added = true; + } + } + if (!added) { + fullCount = list.loadedCount(); + } + setCount(fullCount); + const auto flag = [&] { + using Flag = Data::HistoryUpdate::Flag; + switch (_type) { + case Type::Mentions: return Flag::UnreadMentions; + case Type::Reactions: return Flag::UnreadReactions; + } + Unexpected("Type in Proxy::addSlice."); + }(); + _history->session().changes().historyUpdated(_history, flag); +} + +void Proxy::createData() { + _data = std::make_unique(); + if (_known) { + _data->mentions.setCount(0); + _data->reactions.setCount(0); + } +} + +[[nodiscard]] List &Proxy::resolveList() { + Expects(_data != nullptr); + + switch (_type) { + case Type::Mentions: return _data->mentions; + case Type::Reactions: return _data->reactions; + } + Unexpected("Unread things type in Proxy::resolveList."); +} + +} // namespace HistoryUnreadThings diff --git a/Telegram/SourceFiles/history/history_unread_things.h b/Telegram/SourceFiles/history/history_unread_things.h new file mode 100644 index 000000000..3c0d0ac05 --- /dev/null +++ b/Telegram/SourceFiles/history/history_unread_things.h @@ -0,0 +1,131 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +class History; + +namespace HistoryUnreadThings { + +enum class AddType { + New, + Existing, +}; + +enum class Type { + Mentions, + Reactions, +}; + +class List final { +public: + [[nodiscard]] int loadedCount() const { + return _messages.size(); + } + [[nodiscard]] MsgId minLoaded() const { + return _messages.empty() ? 0 : _messages.front(); + } + [[nodiscard]] MsgId maxLoaded() const { + return _messages.empty() ? 0 : _messages.back(); + } + [[nodiscard]] int count(int notKnownValue = -1) const { + return _count.value_or(notKnownValue); + } + [[nodiscard]] bool has() const { + return (count() > 0); + } + void setCount(int count) { + _count = count; + } + void insert(MsgId msgId) { + _messages.insert(msgId); + } + void erase(MsgId msgId) { + _messages.remove(msgId); + } + +private: + std::optional _count; + base::flat_set _messages; + +}; + +struct All { + List mentions; + List reactions; +}; + +class ConstProxy { +public: + ConstProxy(const List *list, bool known) : _list(list), _known(known) { + } + ConstProxy(const ConstProxy &) = delete; + ConstProxy &operator=(const ConstProxy &) = delete; + + [[nodiscard]] int loadedCount() const { + return _list ? _list->loadedCount() : 0; + } + [[nodiscard]] MsgId minLoaded() const { + return _list ? _list->minLoaded() : 0; + } + [[nodiscard]] MsgId maxLoaded() const { + return _list ? _list->maxLoaded() : 0; + } + [[nodiscard]] int count(int notKnownValue = -1) const { + return _list + ? _list->count(notKnownValue) + : _known + ? 0 + : notKnownValue; + } + [[nodiscard]] bool has() const { + return _list && _list->has(); + } + +private: + const List *_list = nullptr; + const bool _known = false; + +}; + +class Proxy final : public ConstProxy { +public: + Proxy( + not_null history, + std::unique_ptr &data, + Type type, + bool known) + : ConstProxy( + (!data + ? nullptr + : (type == Type::Mentions) + ? &data->mentions + : &data->reactions), + known) + , _history(history) + , _data(data) + , _type(type) { + } + + void setCount(int count); + bool add(MsgId msgId, AddType type); + void erase(MsgId msgId); + + void addSlice(const MTPmessages_Messages &slice); + +private: + void createData(); + [[nodiscard]] List &resolveList(); + + const not_null _history; + std::unique_ptr &_data; + Type _type = Type::Mentions; + bool _known = false; + +}; + +} // namespace HistoryUnreadThings diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 1dde33297..1f075ddcd 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_sending.h" #include "api/api_text_entities.h" #include "api/api_send_progress.h" +#include "api/api_unread_things.h" #include "ui/boxes/confirm_box.h" #include "boxes/delete_messages_box.h" #include "boxes/send_files_box.h" @@ -73,6 +74,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_drag_area.h" #include "history/history_inner_widget.h" #include "history/history_item_components.h" +#include "history/history_unread_things.h" #include "history/view/controls/history_view_voice_record_bar.h" #include "history/view/controls/history_view_ttl_button.h" #include "history/view/history_view_service_message.h" @@ -215,6 +217,9 @@ HistoryWidget::HistoryWidget( , _unreadMentions( _scroll, controller->chatStyle()->value(lifetime(), st::historyUnreadMentions)) +, _unreadReactions( + _scroll, + controller->chatStyle()->value(lifetime(), st::historyUnreadReactions)) , _fieldAutocomplete(this, controller) , _supportAutocomplete(session().supportMode() ? object_ptr(this, &session()) @@ -278,8 +283,13 @@ HistoryWidget::HistoryWidget( } }, lifetime()); - _historyDown->addClickHandler([=] { historyDownClicked(); }); - _unreadMentions->addClickHandler([=] { showNextUnreadMention(); }); + _historyDown.widget->addClickHandler([=] { historyDownClicked(); }); + _unreadMentions.widget->addClickHandler([=] { + showNextUnreadMention(); + }); + _unreadReactions.widget->addClickHandler([=] { + showNextUnreadReaction(); + }); _fieldBarCancel->addClickHandler([=] { cancelFieldAreaState(); }); _send->addClickHandler([=] { sendButtonClicked(); }); @@ -353,9 +363,13 @@ HistoryWidget::HistoryWidget( _scroll->updateBars(); }, lifetime()); - _historyDown->installEventFilter(this); - _unreadMentions->installEventFilter(this); - SendMenu::SetupUnreadMentionsMenu(_unreadMentions.data(), [=] { + _historyDown.widget->installEventFilter(this); + _unreadMentions.widget->installEventFilter(this); + _unreadReactions.widget->installEventFilter(this); + SendMenu::SetupUnreadMentionsMenu(_unreadMentions.widget.data(), [=] { + return _history ? _history->peer.get() : nullptr; + }); + SendMenu::SetupUnreadReactionsMenu(_unreadReactions.widget.data(), [=] { return _history ? _history->peer.get() : nullptr; }); @@ -562,6 +576,7 @@ HistoryWidget::HistoryWidget( | HistoryUpdateFlag::BotKeyboard | HistoryUpdateFlag::CloudDraft | HistoryUpdateFlag::UnreadMentions + | HistoryUpdateFlag::UnreadReactions | HistoryUpdateFlag::UnreadView | HistoryUpdateFlag::TopPromoted | HistoryUpdateFlag::ClientSideMessages @@ -591,8 +606,9 @@ HistoryWidget::HistoryWidget( if (flags & HistoryUpdateFlag::ClientSideMessages) { updateSendButtonType(); } - if (flags & HistoryUpdateFlag::UnreadMentions) { - updateUnreadMentionsVisibility(); + if ((flags & HistoryUpdateFlag::UnreadMentions) + || (flags & HistoryUpdateFlag::UnreadReactions)) { + updateUnreadThingsVisibility(); } if (flags & HistoryUpdateFlag::UnreadView) { unreadCountUpdated(); @@ -907,7 +923,7 @@ void HistoryWidget::initVoiceRecordBar() { _voiceRecordBar->lockShowStarts( ) | rpl::start_with_next([=] { updateHistoryDownVisibility(); - updateUnreadMentionsVisibility(); + updateUnreadThingsVisibility(); }, lifetime()); _voiceRecordBar->updateSendButtonTypeRequests( @@ -2459,7 +2475,7 @@ void HistoryWidget::updateControlsVisibility() { _topBar->setVisible(_peer != nullptr); } updateHistoryDownVisibility(); - updateUnreadMentionsVisibility(); + updateUnreadThingsVisibility(); if (!_history || _a_show.animating()) { hideChildWidgets(); return; @@ -2744,7 +2760,7 @@ void HistoryWidget::newItemAdded(not_null item) { destroyUnreadBar(); if (doWeReadServerHistory()) { if (item->isUnreadMention() && !item->isUnreadMedia()) { - session().api().markMediaRead(item); + session().api().markContentsRead(item); } session().data().histories().readInboxOnNewMessage(item); @@ -2769,7 +2785,7 @@ void HistoryWidget::unreadCountUpdated() { }); } else { updateHistoryDownVisibility(); - _historyDown->setUnreadCount(_history->chatListUnreadCount()); + _historyDown.widget->setUnreadCount(_history->chatListUnreadCount()); } } @@ -3243,7 +3259,7 @@ void HistoryWidget::preloadHistoryIfNeeded() { } updateHistoryDownVisibility(); - updateUnreadMentionsVisibility(); + updateUnreadThingsVisibility(); if (!_scrollToAnimation.animating()) { preloadHistoryByScroll(); checkReplyReturns(); @@ -3332,7 +3348,7 @@ void HistoryWidget::historyDownClicked() { } void HistoryWidget::showNextUnreadMention() { - const auto msgId = _history->getMinLoadedUnreadMention(); + const auto msgId = _history->unreadMentions().minLoaded(); const auto already = (_showAtMsgId == msgId); // Mark mention voice/video message as read. @@ -3354,6 +3370,12 @@ void HistoryWidget::showNextUnreadMention() { showHistory(_peer->id, msgId); } +void HistoryWidget::showNextUnreadReaction() { + const auto msgId = _history->unreadReactions().minLoaded(); + const auto already = (_showAtMsgId == msgId); + showHistory(_peer->id, msgId); +} + void HistoryWidget::saveEditMsg() { Expects(_history != nullptr); @@ -3698,8 +3720,7 @@ void HistoryWidget::showAnimated( _preserveScrollTop = true; show(); _topBar->finishAnimating(); - historyDownAnimationFinish(); - unreadMentionsAnimationFinish(); + cornerButtonsAnimationFinish(); if (_pinnedBar) { _pinnedBar->finishAnimating(); } @@ -3732,8 +3753,7 @@ void HistoryWidget::showAnimated( void HistoryWidget::animationCallback() { update(); if (!_a_show.animating()) { - historyDownAnimationFinish(); - unreadMentionsAnimationFinish(); + cornerButtonsAnimationFinish(); if (_pinnedBar) { _pinnedBar->finishAnimating(); } @@ -3808,22 +3828,20 @@ void HistoryWidget::checkSuggestToGigagroup() { } void HistoryWidget::finishAnimating() { - if (!_a_show.animating()) return; + if (!_a_show.animating()) { + return; + } _a_show.stop(); _topShadow->setVisible(_peer != nullptr); _topBar->setVisible(_peer != nullptr); - historyDownAnimationFinish(); - unreadMentionsAnimationFinish(); + cornerButtonsAnimationFinish(); } -void HistoryWidget::historyDownAnimationFinish() { - _historyDownShown.stop(); - updateHistoryDownPosition(); -} - -void HistoryWidget::unreadMentionsAnimationFinish() { - _unreadMentionsShown.stop(); - updateUnreadMentionsPosition(); +void HistoryWidget::cornerButtonsAnimationFinish() { + _historyDown.animation.stop(); + _unreadMentions.animation.stop(); + _unreadReactions.animation.stop(); + updateCornerButtonsPositions(); } void HistoryWidget::chooseAttach() { @@ -4047,7 +4065,10 @@ bool HistoryWidget::eventFilter(QObject *obj, QEvent *e) { } } } - if ((obj == _historyDown || obj == _unreadMentions) && e->type() == QEvent::Wheel) { + if (e->type() == QEvent::Wheel + && (obj == _historyDown.widget + || obj == _unreadMentions.widget + || obj == _unreadReactions.widget)) { return _scroll->viewportEvent(e); } return TWidget::eventFilter(obj, e); @@ -4946,7 +4967,7 @@ void HistoryWidget::updateControlsGeometry() { updateFieldSize(); - updateHistoryDownPosition(); + updateCornerButtonsPositions(); if (_membersDropdown) { _membersDropdown->setMaxHeight(countMembersDropdownHeightMax()); @@ -5156,16 +5177,7 @@ void HistoryWidget::updateHistoryGeometry( if (_supportAutocomplete) { _supportAutocomplete->setBoundings(_scroll->geometry()); } - if (!_historyDownShown.animating()) { - // _historyDown is a child widget of _scroll, not me. - _historyDown->moveToRight(st::historyToDownPosition.x(), _scroll->height() - _historyDown->height() - st::historyToDownPosition.y()); - if (!_unreadMentionsShown.animating()) { - // _unreadMentions is a child widget of _scroll, not me. - auto additionalSkip = _historyDownIsShown ? (_historyDown->height() + st::historyUnreadMentionsSkip) : 0; - _unreadMentions->moveToRight(st::historyToDownPosition.x(), _scroll->height() - _unreadMentions->height() - additionalSkip - st::historyToDownPosition.y()); - } - } - + updateCornerButtonsPositions(); controller()->floatPlayerAreaUpdated(); } @@ -5470,15 +5482,71 @@ int HistoryWidget::computeMaxFieldHeight() const { return std::min(st::historyComposeFieldMaxHeight, available); } -void HistoryWidget::updateHistoryDownPosition() { - // _historyDown is a child widget of _scroll, not me. - auto top = anim::interpolate(0, _historyDown->height() + st::historyToDownPosition.y(), _historyDownShown.value(_historyDownIsShown ? 1. : 0.)); - _historyDown->moveToRight(st::historyToDownPosition.x(), _scroll->height() - top); - auto shouldBeHidden = !_historyDownIsShown && !_historyDownShown.animating(); - if (shouldBeHidden != _historyDown->isHidden()) { - _historyDown->setVisible(!shouldBeHidden); +void HistoryWidget::updateCornerButtonsPositions() { + const auto checkVisibility = [](CornerButton &button) { + const auto shouldBeHidden = !button.shown + && !button.animation.animating(); + if (shouldBeHidden != button.widget->isHidden()) { + button.widget->setVisible(!shouldBeHidden); + } + }; + const auto shown = [](CornerButton &button) { + return button.animation.value(button.shown ? 1. : 0.); + }; + + // All corner buttons is a child widgets of _scroll, not me. + + const auto historyDownShown = shown(_historyDown); + const auto unreadMentionsShown = shown(_unreadMentions); + const auto unreadReactionsShown = shown(_unreadReactions); + const auto skip = st::historyUnreadThingsSkip; + { + const auto top = anim::interpolate( + 0, + _historyDown.widget->height() + st::historyToDownPosition.y(), + historyDownShown); + _historyDown.widget->moveToRight( + st::historyToDownPosition.x(), + _scroll->height() - top); } - updateUnreadMentionsPosition(); + { + const auto right = anim::interpolate( + -_unreadMentions.widget->width(), + st::historyToDownPosition.x(), + unreadMentionsShown); + const auto shift = anim::interpolate( + 0, + _historyDown.widget->height() + skip, + historyDownShown); + const auto top = _scroll->height() + - _unreadMentions.widget->height() + - st::historyToDownPosition.y() + - shift; + _unreadMentions.widget->moveToRight(right, top); + } + { + const auto right = anim::interpolate( + -_unreadReactions.widget->width(), + st::historyToDownPosition.x(), + unreadReactionsShown); + const auto shift = anim::interpolate( + 0, + _historyDown.widget->height() + skip, + historyDownShown + ) + anim::interpolate( + 0, + _unreadMentions.widget->height() + skip, + unreadMentionsShown); + const auto top = _scroll->height() + - _unreadReactions.widget->height() + - st::historyToDownPosition.y() + - shift; + _unreadReactions.widget->moveToRight(right, top); + } + + checkVisibility(_historyDown); + checkVisibility(_unreadMentions); + checkVisibility(_unreadReactions); } void HistoryWidget::updateHistoryDownVisibility() { @@ -5495,7 +5563,7 @@ void HistoryWidget::updateHistoryDownVisibility() { const auto top = _list->itemTop(unread); return (top >= _scroll->scrollTop() + _scroll->height()); }; - const auto historyDownIsVisible = [&] { + updateCornerButtonVisibility(_historyDown, [&] { if (!_list || _firstLoadRequest) { return false; } @@ -5514,60 +5582,69 @@ void HistoryWidget::updateHistoryDownVisibility() { return true; } return false; - }; - auto historyDownIsShown = historyDownIsVisible(); - if (_historyDownIsShown != historyDownIsShown) { - _historyDownIsShown = historyDownIsShown; - _historyDownShown.start([=] { updateHistoryDownPosition(); }, _historyDownIsShown ? 0. : 1., _historyDownIsShown ? 1. : 0., st::historyToDownDuration); + }()); +} + +void HistoryWidget::updateCornerButtonVisibility( + CornerButton &button, + bool shown) { + if (button.shown != shown) { + button.shown = shown; + button.animation.start( + [=] { updateCornerButtonsPositions(); }, + shown ? 0. : 1., + shown ? 1. : 0., + st::historyToDownDuration); } } -void HistoryWidget::updateUnreadMentionsPosition() { - // _unreadMentions is a child widget of _scroll, not me. - auto right = anim::interpolate(-_unreadMentions->width(), st::historyToDownPosition.x(), _unreadMentionsShown.value(_unreadMentionsIsShown ? 1. : 0.)); - auto shift = anim::interpolate(0, _historyDown->height() + st::historyUnreadMentionsSkip, _historyDownShown.value(_historyDownIsShown ? 1. : 0.)); - auto top = _scroll->height() - _unreadMentions->height() - st::historyToDownPosition.y() - shift; - _unreadMentions->moveToRight(right, top); - auto shouldBeHidden = !_unreadMentionsIsShown && !_unreadMentionsShown.animating(); - if (shouldBeHidden != _unreadMentions->isHidden()) { - _unreadMentions->setVisible(!shouldBeHidden); +void HistoryWidget::updateUnreadThingsVisibility() { + if (_a_show.animating()) { + return; } -} -void HistoryWidget::updateUnreadMentionsVisibility() { - if (_a_show.animating()) return; + auto &unreadThings = session().api().unreadThings(); + unreadThings.preloadEnough(_history); - auto showUnreadMentions = _peer && (_peer->isChat() || _peer->isMegagroup()); - if (showUnreadMentions) { - session().api().preloadEnoughUnreadMentions(_history); - } - const auto unreadMentionsIsShown = [&] { - if (!showUnreadMentions || _firstLoadRequest) { - return false; - } - if (_voiceRecordBar->isLockPresent()) { - return false; - } - if (!_history->getUnreadMentionsLoadedCount()) { - return false; - } - // If we have an unheard voice message with the mention - // and our message is the last one, we can't see the status - // (delivered/read) of this message. - // (Except for MacBooks with the TouchPad.) - if (_scroll->scrollTop() == _scroll->scrollTopMax()) { - if (const auto lastMessage = _history->lastMessage()) { - return !lastMessage->from()->isSelf(); + const auto updateWithLoadedCount = [&](CornerButton &button, int count) { + updateCornerButtonVisibility(button, [&] { + if (!count + || _firstLoadRequest + || _voiceRecordBar->isLockPresent()) { + return false; } + // If we have an unheard voice message with the mention + // and our message is the last one, we can't see the status + // (delivered/read) of this message. + // (Except for MacBooks with the TouchPad.) + if (_scroll->scrollTop() == _scroll->scrollTopMax()) { + if (const auto lastMessage = _history->lastMessage()) { + return !lastMessage->from()->isSelf(); + } + } + return true; + }()); + }; + if (unreadThings.trackMentions(_peer)) { + if (const auto count = _history->unreadMentions().count(0)) { + _unreadMentions.widget->setUnreadCount(count); } - return true; - }(); - if (unreadMentionsIsShown) { - _unreadMentions->setUnreadCount(_history->getUnreadMentionsCount()); + updateWithLoadedCount( + _unreadMentions, + _history->unreadMentions().loadedCount()); + } else { + updateCornerButtonVisibility(_unreadMentions, false); } - if (_unreadMentionsIsShown != unreadMentionsIsShown) { - _unreadMentionsIsShown = unreadMentionsIsShown; - _unreadMentionsShown.start([=] { updateUnreadMentionsPosition(); }, _unreadMentionsIsShown ? 0. : 1., _unreadMentionsIsShown ? 1. : 0., st::historyToDownDuration); + + if (unreadThings.trackReactions(_peer)) { + if (const auto count = _history->unreadReactions().count(0)) { + _unreadReactions.widget->setUnreadCount(count); + } + updateWithLoadedCount( + _unreadReactions, + _history->unreadReactions().loadedCount()); + } else { + updateCornerButtonVisibility(_unreadReactions, false); } } diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index d5da88bbc..a11749841 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -242,11 +242,6 @@ public: void applyCloudDraft(History *history); - void updateHistoryDownPosition(); - void updateHistoryDownVisibility(); - void updateUnreadMentionsPosition(); - void updateUnreadMentionsVisibility(); - void updateFieldSubmitSettings(); void activate(); @@ -332,7 +327,15 @@ private: }; using TextUpdateEvents = base::flags; friend inline constexpr bool is_flag_type(TextUpdateEvent) { return true; }; + struct CornerButton { + template + CornerButton(Args &&...args) : widget(std::forward(args)...) { + } + Ui::Animations::Simple animation; + bool shown = false; + object_ptr widget; + }; void checkSuggestToGigagroup(); void initTabbedSelector(); @@ -384,6 +387,7 @@ private: void recountChatWidth(); void historyDownClicked(); void showNextUnreadMention(); + void showNextUnreadReaction(); void handlePeerUpdate(); void setMembersShowAreaActive(bool active); void handleHistoryChange(not_null history); @@ -416,11 +420,15 @@ private: void animationCallback(); void updateOverStates(QPoint pos); void chooseAttach(); - void historyDownAnimationFinish(); - void unreadMentionsAnimationFinish(); + void cornerButtonsAnimationFinish(); void sendButtonClicked(); void newItemAdded(not_null item); + void updateCornerButtonsPositions(); + void updateHistoryDownVisibility(); + void updateUnreadThingsVisibility(); + void updateCornerButtonVisibility(CornerButton &button, bool shown); + bool canSendFiles(not_null data) const; bool confirmSendingFiles( const QStringList &files, @@ -694,13 +702,9 @@ private: bool _synteticScrollEvent = false; Ui::Animations::Simple _scrollToAnimation; - Ui::Animations::Simple _historyDownShown; - bool _historyDownIsShown = false; - object_ptr _historyDown; - - Ui::Animations::Simple _unreadMentionsShown; - bool _unreadMentionsIsShown = false; - object_ptr _unreadMentions; + CornerButton _historyDown; + CornerButton _unreadMentions; + CornerButton _unreadReactions; const object_ptr _fieldAutocomplete; object_ptr _supportAutocomplete; diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 3af40d1ad..b3f71d6de 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -121,7 +121,11 @@ historyUnreadMentions: TwoIconButton(historyToDown) { iconAbove: icon {{ "history_unread_mention", historyToDownFg, point(16px, 16px) }}; iconAboveOver: icon {{ "history_unread_mention", historyToDownFgOver, point(16px, 16px) }}; } -historyUnreadMentionsSkip: 4px; +historyUnreadReactions: TwoIconButton(historyToDown) { + iconAbove: icon {{ "history_unread_reaction", historyToDownFg, point(16px, 16px) }}; + iconAboveOver: icon {{ "history_unread_reaction", historyToDownFgOver, point(16px, 16px) }}; +} +historyUnreadThingsSkip: 4px; membersInnerWidth: 310px; membersInnerHeightMax: 360px; diff --git a/Telegram/SourceFiles/window/notifications_manager.cpp b/Telegram/SourceFiles/window/notifications_manager.cpp index e985be534..630bd1f22 100644 --- a/Telegram/SourceFiles/window/notifications_manager.cpp +++ b/Telegram/SourceFiles/window/notifications_manager.cpp @@ -918,8 +918,8 @@ void Manager::notificationReplied( history->session().api().sendMessage(std::move(message)); const auto item = history->owner().message(history->peer, id.msgId); - if (item && item->isUnreadMention() && !item->isUnreadMedia()) { - history->session().api().markMediaRead(item); + if (item && item->isUnreadMention() && !item->isIncomingUnreadMedia()) { + history->session().api().markContentsRead(item); } }