Track and display unread count in discussions.

This commit is contained in:
John Preston 2021-08-30 18:37:09 +03:00
parent 85e4c8527b
commit c39024c7fd
12 changed files with 271 additions and 45 deletions

View file

@ -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;

View file

@ -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<Flag>;
friend inline constexpr auto is_flag_type(Flag) { return true; }

View file

@ -101,6 +101,15 @@ rpl::producer<MessagesSlice> RepliesList::source(
_partLoaded.events(
) | rpl::start_with_next(pushDelayed, lifetime);
_history->session().data().channelDifferenceTooLong(
) | rpl::filter([=](not_null<ChannelData*> 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<int> RepliesList::fullCount() const {
return _fullCount.value() | rpl::filter_optional();
}
std::optional<int> RepliesList::fullUnreadCountAfter(
MsgId readTillId,
MsgId wasReadTillId,
std::optional<int> 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*> viewer) {
injectRootMessage(viewer);
ranges::reverse(viewer->slice.ids);

View file

@ -31,6 +31,11 @@ public:
[[nodiscard]] rpl::producer<int> fullCount() const;
[[nodiscard]] std::optional<int> fullUnreadCountAfter(
MsgId readTillId,
MsgId wasReadTillId,
std::optional<int> wasUnreadCountAfter) const;
private:
struct Viewer;

View file

@ -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<int> 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<bool> unread) {
}
virtual void setReplyToTop(MsgId replyToTop) {
}

View file

@ -49,6 +49,7 @@ struct HistoryMessageViews : public RuntimeComponent<HistoryMessageViews, Histor
MsgId repliesInboxReadTillId = 0;
MsgId repliesOutboxReadTillId = 0;
MsgId repliesMaxId = 0;
int repliesUnreadCount = -1; // unknown
ChannelId commentsMegagroupId = 0;
MsgId commentsRootId = 0;
};

View file

@ -816,16 +816,30 @@ MsgId HistoryMessage::repliesInboxReadTill() const {
return 0;
}
void HistoryMessage::setRepliesInboxReadTill(MsgId readTillId) {
void HistoryMessage::setRepliesInboxReadTill(
MsgId readTillId,
std::optional<int> unreadCount) {
if (const auto views = Get<HistoryMessageViews>()) {
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<bool> unread) {
const auto views = Get<HistoryMessageViews>();
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<HistoryMessageViews*> 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<HistoryMessageReply>();
if (!reply
@ -1877,19 +1921,23 @@ void HistoryMessage::changeReplyToTopCounter(
if (!top) {
return;
}
const auto changeFor = [&](not_null<HistoryItem*> 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<HistoryMessageViews>()) {
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<HistoryItem*> 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);

View file

@ -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<bool> 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<int> 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<HistoryMessageViews*> views,
bool forceResize = false);
void setUnreadRepliesCount(
not_null<HistoryMessageViews*> views,
int count);
static void FillForwardedInfo(
CreateConfig &config,

View file

@ -160,7 +160,6 @@ private:
bool _scrollDownIsShown = false;
object_ptr<Ui::HistoryDownButton> _scrollDown;
Data::MessagesSlice _lastSlice;
int _messagesCount = -1;
};

View file

@ -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<int> RepliesWidget::computeUnreadCount() const {
if (!_root) {
return std::nullopt;
}
const auto views = _root->Get<HistoryMessageViews>();
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<QEvent*> 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<HistoryMessageViews>() : 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<HistoryItem*> 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<HistoryMessageViews>();
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);

View file

@ -198,6 +198,7 @@ private:
[[nodiscard]] MsgId replyToId() const;
[[nodiscard]] HistoryItem *lookupRoot() const;
[[nodiscard]] bool computeAreComments() const;
[[nodiscard]] std::optional<int> computeUnreadCount() const;
void orderWidgets();
void pushReplyReturn(not_null<HistoryItem*> 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<Ui::HistoryDownButton> _scrollDown;
Data::MessagesSlice _lastSlice;
bool _choosingAttach = false;
base::Timer _readRequestTimer;
bool _readRequestPending = false;
mtpRequestId _readRequestId = 0;
mtpRequestId _reloadUnreadCountRequestId = 0;
bool _loaded = false;
};

View file

@ -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());
}