diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index edd4ab362..e889a9b67 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -2094,17 +2094,18 @@ void Updates::feedUpdate(const MTPUpdate &update) { const auto msgId = d.vtop_msg_id().v; const auto readTillId = d.vread_max_id().v; const auto item = session().data().message(channelId, msgId); + const auto unreadCount = std::nullopt; if (item) { - item->setRepliesInboxReadTill(readTillId); + item->setRepliesInboxReadTill(readTillId, unreadCount); if (const auto post = item->lookupDiscussionPostOriginal()) { - post->setRepliesInboxReadTill(readTillId); + post->setRepliesInboxReadTill(readTillId, unreadCount); } } if (const auto broadcastId = d.vbroadcast_id()) { if (const auto post = session().data().message( broadcastId->v, d.vbroadcast_post()->v)) { - post->setRepliesInboxReadTill(readTillId); + post->setRepliesInboxReadTill(readTillId, unreadCount); } } } break; diff --git a/Telegram/SourceFiles/data/data_changes.h b/Telegram/SourceFiles/data/data_changes.h index b94f7f7f5..105c82858 100644 --- a/Telegram/SourceFiles/data/data_changes.h +++ b/Telegram/SourceFiles/data/data_changes.h @@ -134,16 +134,17 @@ struct MessageUpdate { enum class Flag : uint32 { None = 0, - Edited = (1U << 0), - Destroyed = (1U << 1), - DialogRowRepaint = (1U << 2), - DialogRowRefresh = (1U << 3), - NewAdded = (1U << 4), - ReplyMarkup = (1U << 5), - BotCallbackSent = (1U << 6), - NewMaybeAdded = (1U << 7), + Edited = (1U << 0), + Destroyed = (1U << 1), + DialogRowRepaint = (1U << 2), + DialogRowRefresh = (1U << 3), + NewAdded = (1U << 4), + ReplyMarkup = (1U << 5), + BotCallbackSent = (1U << 6), + NewMaybeAdded = (1U << 7), + RepliesUnreadCount = (1U << 8), - LastUsedBit = (1U << 7), + LastUsedBit = (1U << 7), }; using Flags = base::flags; friend inline constexpr auto is_flag_type(Flag) { return true; } diff --git a/Telegram/SourceFiles/data/data_replies_list.cpp b/Telegram/SourceFiles/data/data_replies_list.cpp index 051f4d771..7dc921522 100644 --- a/Telegram/SourceFiles/data/data_replies_list.cpp +++ b/Telegram/SourceFiles/data/data_replies_list.cpp @@ -101,6 +101,15 @@ rpl::producer RepliesList::source( _partLoaded.events( ) | rpl::start_with_next(pushDelayed, lifetime); + _history->session().data().channelDifferenceTooLong( + ) | rpl::filter([=](not_null channel) { + if (_history->peer != channel || !_skippedAfter.has_value()) { + return false; + } + _skippedAfter = std::nullopt; + return true; + }) | rpl::start_with_next(pushDelayed, lifetime); + push(); return lifetime; }; @@ -169,6 +178,64 @@ rpl::producer RepliesList::fullCount() const { return _fullCount.value() | rpl::filter_optional(); } +std::optional RepliesList::fullUnreadCountAfter( + MsgId readTillId, + MsgId wasReadTillId, + std::optional wasUnreadCountAfter) const { + Expects(readTillId >= wasReadTillId); + + readTillId = std::max(readTillId, _rootId); + wasReadTillId = std::max(wasReadTillId, _rootId); + const auto backLoaded = (_skippedBefore == 0); + const auto frontLoaded = (_skippedAfter == 0); + const auto fullLoaded = backLoaded && frontLoaded; + const auto allUnread = (readTillId == _rootId) + || (fullLoaded && _list.empty()); + const auto countIncoming = [&](auto from, auto till) { + auto &owner = _history->owner(); + const auto channelId = _history->channelId(); + auto count = 0; + for (auto i = from; i != till; ++i) { + if (!owner.message(channelId, *i)->out()) { + ++count; + } + } + return count; + }; + if (allUnread && fullLoaded) { + // Should not happen too often unless the list is empty. + return countIncoming(begin(_list), end(_list)); + } else if (frontLoaded && !_list.empty() && readTillId >= _list.front()) { + // Always "count by local data" if read till the end. + return 0; + } else if (wasReadTillId == readTillId) { + // Otherwise don't recount the same value over and over. + return wasUnreadCountAfter; + } else if (frontLoaded && !_list.empty() && readTillId >= _list.back()) { + // And count by local data if it is available and read-till changed. + return countIncoming( + begin(_list), + ranges::lower_bound(_list, readTillId, std::greater<>())); + } else if (_list.empty()) { + return std::nullopt; + } else if (wasUnreadCountAfter.has_value() + && (frontLoaded || readTillId <= _list.front()) + && (backLoaded || wasReadTillId >= _list.back())) { + // Count how many were read since previous value. + const auto from = ranges::lower_bound( + _list, + readTillId, + std::greater<>()); + const auto till = ranges::lower_bound( + from, + end(_list), + wasReadTillId, + std::greater<>()); + return std::max(*wasUnreadCountAfter - countIncoming(from, till), 0); + } + return std::nullopt; +} + void RepliesList::injectRootMessageAndReverse(not_null viewer) { injectRootMessage(viewer); ranges::reverse(viewer->slice.ids); diff --git a/Telegram/SourceFiles/data/data_replies_list.h b/Telegram/SourceFiles/data/data_replies_list.h index 36932b82b..71c8d32b3 100644 --- a/Telegram/SourceFiles/data/data_replies_list.h +++ b/Telegram/SourceFiles/data/data_replies_list.h @@ -31,6 +31,11 @@ public: [[nodiscard]] rpl::producer fullCount() const; + [[nodiscard]] std::optional fullUnreadCountAfter( + MsgId readTillId, + MsgId wasReadTillId, + std::optional wasUnreadCountAfter) const; + private: struct Viewer; diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index 7bd594be4..66fa8b370 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -223,7 +223,9 @@ public: [[nodiscard]] virtual MsgId repliesInboxReadTill() const { return MsgId(0); } - virtual void setRepliesInboxReadTill(MsgId readTillId) { + virtual void setRepliesInboxReadTill( + MsgId readTillId, + std::optional unreadCount) { } [[nodiscard]] virtual MsgId computeRepliesInboxReadTillFull() const { return MsgId(0); @@ -316,7 +318,10 @@ public: } virtual void clearReplies() { } - virtual void changeRepliesCount(int delta, PeerId replier) { + virtual void changeRepliesCount( + int delta, + PeerId replier, + std::optional unread) { } virtual void setReplyToTop(MsgId replyToTop) { } diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index 2173a1afe..57f5edce6 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -49,6 +49,7 @@ struct HistoryMessageViews : public RuntimeComponent unreadCount) { if (const auto views = Get()) { const auto newReadTillId = std::max(readTillId, 1); - if (newReadTillId > views->repliesInboxReadTillId) { + const auto ignore = (newReadTillId < views->repliesInboxReadTillId); + if (ignore) { + return; + } + const auto changed = (newReadTillId > views->repliesInboxReadTillId); + if (changed) { const auto wasUnread = repliesAreComments() && areRepliesUnread(); views->repliesInboxReadTillId = newReadTillId; if (wasUnread && !areRepliesUnread()) { history()->owner().requestItemRepaint(this); } } + const auto wasUnreadCount = (views->repliesUnreadCount >= 0) + ? std::make_optional(views->repliesUnreadCount) + : std::nullopt; + if (unreadCount != wasUnreadCount + && (changed || unreadCount.has_value())) { + setUnreadRepliesCount(views, unreadCount.value_or(-1)); + } } } @@ -1808,10 +1822,27 @@ void HistoryMessage::refreshRepliesText( } } -void HistoryMessage::changeRepliesCount(int delta, PeerId replier) { +void HistoryMessage::changeRepliesCount( + int delta, + PeerId replier, + std::optional unread) { const auto views = Get(); const auto limit = HistoryMessageViews::kMaxRecentRepliers; - if (!views || views->replies.count < 0) { + if (!views) { + return; + } + + // Update unread count. + if (!unread) { + setUnreadRepliesCount(views, -1); + } else if (views->repliesUnreadCount >= 0 && *unread) { + setUnreadRepliesCount( + views, + std::max(views->repliesUnreadCount + delta, 0)); + } + + // Update full count. + if (views->replies.count < 0) { return; } views->replies.count = std::max(views->replies.count + delta, 0); @@ -1830,6 +1861,19 @@ void HistoryMessage::changeRepliesCount(int delta, PeerId replier) { refreshRepliesText(views); } +void HistoryMessage::setUnreadRepliesCount( + not_null views, + int count) { + // Track unread count in discussion forwards, not in the channel posts. + if (views->repliesUnreadCount == count || views->commentsMegagroupId) { + return; + } + views->repliesUnreadCount = count; + history()->session().changes().messageUpdated( + this, + Data::MessageUpdate::Flag::RepliesUnreadCount); +} + void HistoryMessage::setReplyToTop(MsgId replyToTop) { const auto reply = Get(); if (!reply @@ -1877,19 +1921,23 @@ void HistoryMessage::changeReplyToTopCounter( if (!top) { return; } - const auto changeFor = [&](not_null item) { - if (const auto from = displayFrom()) { - item->changeRepliesCount(delta, from->id); - return; - } - item->changeRepliesCount(delta, PeerId()); - }; + auto unread = out() ? std::make_optional(false) : std::nullopt; if (const auto views = top->Get()) { if (views->commentsMegagroupId) { // This is a post in channel, we don't track its replies. return; } + if (views->repliesInboxReadTillId > 0) { + unread = !out() && (id > views->repliesInboxReadTillId); + } } + const auto changeFor = [&](not_null item) { + if (const auto from = displayFrom()) { + item->changeRepliesCount(delta, from->id, unread); + } else { + item->changeRepliesCount(delta, PeerId(), unread); + } + }; changeFor(top); if (const auto original = top->lookupDiscussionPostOriginal()) { changeFor(original); diff --git a/Telegram/SourceFiles/history/history_message.h b/Telegram/SourceFiles/history/history_message.h index ecae4bd75..88440d555 100644 --- a/Telegram/SourceFiles/history/history_message.h +++ b/Telegram/SourceFiles/history/history_message.h @@ -133,7 +133,10 @@ public: void setForwardsCount(int count) override; void setReplies(const MTPMessageReplies &data) override; void clearReplies() override; - void changeRepliesCount(int delta, PeerId replier) override; + void changeRepliesCount( + int delta, + PeerId replier, + std::optional unread) override; void setReplyToTop(MsgId replyToTop) override; void setPostAuthor(const QString &author) override; void setRealId(MsgId newId) override; @@ -181,7 +184,9 @@ public: [[nodiscard]] bool externalReply() const override; [[nodiscard]] MsgId repliesInboxReadTill() const override; - void setRepliesInboxReadTill(MsgId readTillId) override; + void setRepliesInboxReadTill( + MsgId readTillId, + std::optional unreadCount) override; [[nodiscard]] MsgId computeRepliesInboxReadTillFull() const override; [[nodiscard]] MsgId repliesOutboxReadTill() const override; void setRepliesOutboxReadTill(MsgId readTillId) override; @@ -250,6 +255,9 @@ private: void refreshRepliesText( not_null views, bool forceResize = false); + void setUnreadRepliesCount( + not_null views, + int count); static void FillForwardedInfo( CreateConfig &config, diff --git a/Telegram/SourceFiles/history/view/history_view_pinned_section.h b/Telegram/SourceFiles/history/view/history_view_pinned_section.h index 9b8ce5fe9..6cff5bb06 100644 --- a/Telegram/SourceFiles/history/view/history_view_pinned_section.h +++ b/Telegram/SourceFiles/history/view/history_view_pinned_section.h @@ -160,7 +160,6 @@ private: bool _scrollDownIsShown = false; object_ptr _scrollDown; - Data::MessagesSlice _lastSlice; int _messagesCount = -1; }; diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp index 052e4f9c3..5d9e1a04c 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp @@ -247,16 +247,24 @@ RepliesWidget::RepliesWidget( data.progress); }, lifetime()); + using MessageUpdateFlag = Data::MessageUpdate::Flag; _history->session().changes().messageUpdates( - Data::MessageUpdate::Flag::Destroyed + MessageUpdateFlag::Destroyed + | MessageUpdateFlag::RepliesUnreadCount ) | rpl::start_with_next([=](const Data::MessageUpdate &update) { - if (update.item == _root) { - _root = nullptr; - updatePinnedVisibility(); - controller->showBackFromStack(); - } - while (update.item == _replyReturn) { - calculateNextReplyReturn(); + if (update.flags & MessageUpdateFlag::Destroyed) { + if (update.item == _root) { + _root = nullptr; + updatePinnedVisibility(); + controller->showBackFromStack(); + } + while (update.item == _replyReturn) { + calculateNextReplyReturn(); + } + return; + } else if ((update.item == _root) + && (update.flags & MessageUpdateFlag::RepliesUnreadCount)) { + refreshUnreadCountBadge(); } }, lifetime()); @@ -302,12 +310,15 @@ void RepliesWidget::sendReadTillRequest() { _readRequestPending = false; const auto api = &_history->session().api(); api->request(base::take(_readRequestId)).cancel(); + _readRequestId = api->request(MTPmessages_ReadDiscussion( _root->history()->peer->input, MTP_int(_root->id), MTP_int(_root->computeRepliesInboxReadTillFull()) - )).done([=](const MTPBool &) { - }).send(); + )).done(crl::guard(this, [=](const MTPBool &) { + _readRequestId = 0; + reloadUnreadCountIfNeeded(); + })).send(); } void RepliesWidget::setupRoot() { @@ -317,6 +328,7 @@ void RepliesWidget::setupRoot() { _root = lookupRoot(); if (_root) { _areComments = computeAreComments(); + refreshUnreadCountBadge(); if (_readRequestPending) { sendReadTillRequest(); } @@ -370,6 +382,19 @@ bool RepliesWidget::computeAreComments() const { return _root && _root->isDiscussionPost(); } +std::optional RepliesWidget::computeUnreadCount() const { + if (!_root) { + return std::nullopt; + } + const auto views = _root->Get(); + if (!views) { + return std::nullopt; + } + return (views->repliesUnreadCount >= 0) + ? std::make_optional(views->repliesUnreadCount) + : std::nullopt; +} + void RepliesWidget::setupComposeControls() { auto slowmodeSecondsLeft = session().changes().peerFlagsValue( _history->peer, @@ -1143,6 +1168,7 @@ void RepliesWidget::setupScrollDownButton() { _scrollDown->setClickedCallback([=] { scrollDownClicked(); }); + refreshUnreadCountBadge(); base::install_event_filter(_scrollDown, [=](not_null event) { if (event->type() != QEvent::Wheel) { return base::EventFilterResult::Continue; @@ -1154,6 +1180,56 @@ void RepliesWidget::setupScrollDownButton() { updateScrollDownVisibility(); } +void RepliesWidget::refreshUnreadCountBadge() { + if (!_root) { + return; + } else if (const auto count = computeUnreadCount()) { + _scrollDown->setUnreadCount(*count); + } else if (!_readRequestPending + && !_readRequestTimer.isActive() + && !_readRequestId) { + reloadUnreadCountIfNeeded(); + } +} + +void RepliesWidget::reloadUnreadCountIfNeeded() { + const auto views = _root ? _root->Get() : nullptr; + if (!views || views->repliesUnreadCount >= 0) { + return; + } else if (views->repliesInboxReadTillId + < _root->computeRepliesInboxReadTillFull()) { + _readRequestTimer.callOnce(0); + } else if (!_reloadUnreadCountRequestId) { + const auto session = &_history->session(); + const auto fullId = _root->fullId(); + const auto apply = [session, fullId](int readTill, int unreadCount) { + if (const auto root = session->data().message(fullId)) { + root->setRepliesInboxReadTill(readTill, unreadCount); + if (const auto post = root->lookupDiscussionPostOriginal()) { + post->setRepliesInboxReadTill(readTill, unreadCount); + } + } + }; + const auto weak = Ui::MakeWeak(this); + _reloadUnreadCountRequestId = session->api().request( + MTPmessages_GetDiscussionMessage( + _history->peer->input, + MTP_int(_rootId)) + ).done([=](const MTPmessages_DiscussionMessage &result) { + if (weak) { + _reloadUnreadCountRequestId = 0; + } + result.match([&](const MTPDmessages_discussionMessage &data) { + session->data().processUsers(data.vusers()); + session->data().processChats(data.vchats()); + apply( + data.vread_inbox_max_id().value_or_empty(), + data.vunread_count().v); + }); + }).send(); + } +} + void RepliesWidget::scrollDownClicked() { if (QGuiApplication::keyboardModifiers() == Qt::ControlModifier) { showAtEnd(); @@ -1692,11 +1768,21 @@ void RepliesWidget::readTill(not_null item) { } const auto was = _root->computeRepliesInboxReadTillFull(); const auto now = item->id; - const auto fast = item->out(); - if (was < now) { - _root->setRepliesInboxReadTill(now); + if (now < was) { + return; + } + const auto views = _root->Get(); + const auto wasReadTillId = views ? views->repliesInboxReadTillId : 0; + const auto wasUnreadCount = views ? views->repliesUnreadCount : -1; + const auto unreadCount = _replies->fullUnreadCountAfter( + now, + wasReadTillId, + wasUnreadCount); + const auto fast = item->out() || !unreadCount.has_value(); + if (was < now || (fast && now == was)) { + _root->setRepliesInboxReadTill(now, unreadCount); if (const auto post = _root->lookupDiscussionPostOriginal()) { - post->setRepliesInboxReadTill(now); + post->setRepliesInboxReadTill(now, unreadCount); } if (!_readRequestTimer.isActive()) { _readRequestTimer.callOnce(fast ? 0 : kReadRequestTimeout); diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.h b/Telegram/SourceFiles/history/view/history_view_replies_section.h index 0026c50c9..b317c0de5 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.h +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.h @@ -198,6 +198,7 @@ private: [[nodiscard]] MsgId replyToId() const; [[nodiscard]] HistoryItem *lookupRoot() const; [[nodiscard]] bool computeAreComments() const; + [[nodiscard]] std::optional computeUnreadCount() const; void orderWidgets(); void pushReplyReturn(not_null item); @@ -208,6 +209,8 @@ private: void recountChatWidth(); void replyToMessage(FullMsgId itemId); void refreshTopBarActiveChat(); + void refreshUnreadCountBadge(); + void reloadUnreadCountIfNeeded(); void uploadFile(const QByteArray &fileContent, SendMediaType type); bool confirmSendingFiles( @@ -276,13 +279,13 @@ private: bool _scrollDownIsShown = false; object_ptr _scrollDown; - Data::MessagesSlice _lastSlice; bool _choosingAttach = false; base::Timer _readRequestTimer; bool _readRequestPending = false; mtpRequestId _readRequestId = 0; + mtpRequestId _reloadUnreadCountRequestId = 0; bool _loaded = false; }; diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 5a193c5cc..f7a9b26a8 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -399,7 +399,8 @@ void SessionNavigation::showRepliesForMessage( item->setRepliesMaxId(maxId->v); } item->setRepliesInboxReadTill( - data.vread_inbox_max_id().value_or_empty()); + data.vread_inbox_max_id().value_or_empty(), + data.vunread_count().v); item->setRepliesOutboxReadTill( data.vread_outbox_max_id().value_or_empty()); const auto post = _session->data().message(channelId, rootId); @@ -409,7 +410,8 @@ void SessionNavigation::showRepliesForMessage( post->setRepliesMaxId(maxId->v); } post->setRepliesInboxReadTill( - data.vread_inbox_max_id().value_or_empty()); + data.vread_inbox_max_id().value_or_empty(), + data.vunread_count().v); post->setRepliesOutboxReadTill( data.vread_outbox_max_id().value_or_empty()); }