Better post views tracking.

This commit is contained in:
John Preston
2026-03-13 14:14:49 +04:00
parent 4fcfbed7d2
commit 5a39484407
13 changed files with 692 additions and 33 deletions
+4
View File
@@ -163,6 +163,8 @@ PRIVATE
api/api_premium.h
api/api_premium_option.cpp
api/api_premium_option.h
api/api_read_metrics.cpp
api/api_read_metrics.h
api/api_report.cpp
api/api_report.h
api/api_ringtones.cpp
@@ -965,6 +967,8 @@ PRIVATE
history/view/history_view_quick_action.h
history/view/history_view_reaction_preview.cpp
history/view/history_view_reaction_preview.h
history/view/history_view_read_metrics_tracker.cpp
history/view/history_view_read_metrics_tracker.h
history/view/history_view_reply.cpp
history/view/history_view_reply.h
history/view/history_view_reply_button.cpp
@@ -0,0 +1,73 @@
/*
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_read_metrics.h"
#include "apiwrap.h"
#include "data/data_peer.h"
namespace Api {
namespace {
constexpr auto kSendTimeout = crl::time(5000);
} // namespace
ReadMetrics::ReadMetrics(not_null<ApiWrap*> api)
: _api(&api->instance())
, _timer([=] { send(); }) {
}
void ReadMetrics::add(
not_null<PeerData*> peer,
FinalizedReadMetric metric) {
_pending[peer].push_back(metric);
if (!_timer.isActive()) {
_timer.callOnce(kSendTimeout);
}
}
void ReadMetrics::send() {
for (auto i = _pending.begin(); i != _pending.end();) {
if (_requests.contains(i->first)) {
++i;
continue;
}
auto metrics = QVector<MTPInputMessageReadMetric>();
metrics.reserve(i->second.size());
for (const auto &m : i->second) {
metrics.push_back(MTP_inputMessageReadMetric(
MTP_int(m.msgId.bare),
MTP_long(m.viewId),
MTP_int(m.timeInViewMs),
MTP_int(m.activeTimeInViewMs),
MTP_int(m.heightToViewportRatioPermille),
MTP_int(m.seenRangeRatioPermille)));
}
const auto peer = i->first;
const auto finish = [=] {
_requests.erase(peer);
if (!_pending.empty() && !_timer.isActive()) {
_timer.callOnce(kSendTimeout);
}
};
const auto requestId = _api.request(MTPmessages_ReportReadMetrics(
peer->input(),
MTP_vector<MTPInputMessageReadMetric>(std::move(metrics))
)).done([=](const MTPBool &) {
finish();
}).fail([=](const MTP::Error &) {
finish();
}).send();
_requests.emplace(peer, requestId);
i = _pending.erase(i);
}
}
} // namespace Api
@@ -0,0 +1,45 @@
/*
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
#include "mtproto/sender.h"
#include "base/timer.h"
class ApiWrap;
class PeerData;
namespace Api {
struct FinalizedReadMetric {
MsgId msgId = 0;
uint64 viewId = 0;
int timeInViewMs = 0;
int activeTimeInViewMs = 0;
int heightToViewportRatioPermille = 0;
int seenRangeRatioPermille = 0;
};
class ReadMetrics final {
public:
explicit ReadMetrics(not_null<ApiWrap*> api);
void add(not_null<PeerData*> peer, FinalizedReadMetric metric);
private:
void send();
MTP::Sender _api;
base::flat_map<
not_null<PeerData*>,
std::vector<FinalizedReadMetric>> _pending;
base::flat_map<not_null<PeerData*>, mtpRequestId> _requests;
base::Timer _timer;
};
} // namespace Api
+6
View File
@@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_global_privacy.h"
#include "api/api_updates.h"
#include "api/api_user_privacy.h"
#include "api/api_read_metrics.h"
#include "api/api_views.h"
#include "api/api_confirm_phone.h"
#include "api/api_unread_things.h"
@@ -181,6 +182,7 @@ ApiWrap::ApiWrap(not_null<Main::Session*> session)
, _inviteLinks(std::make_unique<Api::InviteLinks>(this))
, _chatLinks(std::make_unique<Api::ChatLinks>(this))
, _views(std::make_unique<Api::ViewsManager>(this))
, _readMetrics(std::make_unique<Api::ReadMetrics>(this))
, _confirmPhone(std::make_unique<Api::ConfirmPhone>(this))
, _peerPhoto(std::make_unique<Api::PeerPhoto>(this))
, _polls(std::make_unique<Api::Polls>(this))
@@ -4976,6 +4978,10 @@ Api::ViewsManager &ApiWrap::views() {
return *_views;
}
Api::ReadMetrics &ApiWrap::readMetrics() {
return *_readMetrics;
}
Api::ConfirmPhone &ApiWrap::confirmPhone() {
return *_confirmPhone;
}
+3
View File
@@ -83,6 +83,7 @@ class UnreadThings;
class Ringtones;
class Transcribes;
class Premium;
class ReadMetrics;
class Usernames;
class Websites;
@@ -414,6 +415,7 @@ public:
[[nodiscard]] Api::InviteLinks &inviteLinks();
[[nodiscard]] Api::ChatLinks &chatLinks();
[[nodiscard]] Api::ViewsManager &views();
[[nodiscard]] Api::ReadMetrics &readMetrics();
[[nodiscard]] Api::ConfirmPhone &confirmPhone();
[[nodiscard]] Api::PeerPhoto &peerPhoto();
[[nodiscard]] Api::Polls &polls();
@@ -772,6 +774,7 @@ private:
const std::unique_ptr<Api::InviteLinks> _inviteLinks;
const std::unique_ptr<Api::ChatLinks> _chatLinks;
const std::unique_ptr<Api::ViewsManager> _views;
const std::unique_ptr<Api::ReadMetrics> _readMetrics;
const std::unique_ptr<Api::ConfirmPhone> _confirmPhone;
const std::unique_ptr<Api::PeerPhoto> _peerPhoto;
const std::unique_ptr<Api::Polls> _polls;
@@ -645,6 +645,7 @@ bool Application::eventFilter(QObject *object, QEvent *e) {
switch (e->type()) {
case QEvent::KeyPress: {
updateNonIdle();
_inAppKeyPressed.fire({});
const auto event = static_cast<QKeyEvent*>(e);
if (base::Platform::GlobalShortcuts::IsToggleFullScreenKey(event)
&& toggleActiveWindowFullScreen()) {
@@ -1224,6 +1225,10 @@ crl::time Application::lastNonIdleTime() const {
_lastNonIdleTime);
}
rpl::producer<> Application::inAppKeyPressed() const {
return _inAppKeyPressed.events();
}
rpl::producer<bool> Application::passcodeLockChanges() const {
return _passcodeLock.changes();
}
+2
View File
@@ -329,6 +329,7 @@ public:
void handleAppActivated();
void handleAppDeactivated();
[[nodiscard]] rpl::producer<bool> appDeactivatedValue() const;
[[nodiscard]] rpl::producer<> inAppKeyPressed() const;
void materializeLocalDrafts();
[[nodiscard]] rpl::producer<> materializeLocalDraftsRequests() const;
@@ -464,6 +465,7 @@ private:
base::flat_map<not_null<QWidget*>, LeaveFilter> _leaveFilters;
rpl::event_stream<Media::View::OpenRequest> _openInMediaViewRequests;
rpl::event_stream<> _inAppKeyPressed;
rpl::event_stream<> _materializeLocalDraftsRequests;
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history_inner_widget.h"
#include "chat_helpers/stickers_emoji_pack.h"
#include "core/application.h"
#include "core/file_utilities.h"
#include "core/click_handler_types.h"
#include "core/phone_click_handler.h"
@@ -69,6 +70,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "chat_helpers/emoji_interactions.h"
#include "history/history_widget.h"
#include "history/view/history_view_translate_tracker.h"
#include "history/view/history_view_read_metrics_tracker.h"
#include "base/platform/base_platform_info.h"
#include "base/qt/qt_common_adapters.h"
#include "base/qt/qt_key_modifiers.h"
@@ -111,6 +113,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <QtGui/QClipboard>
#include <QtWidgets/QApplication>
#include <QtCore/QCoreApplication>
#include <QtCore/QMimeData>
namespace {
@@ -325,6 +328,8 @@ HistoryInner::HistoryInner(
[=](not_null<const Element*> view) { return itemTop(view); }))
, _migrated(history->migrateFrom())
, _translateTracker(std::make_unique<HistoryView::TranslateTracker>(history))
, _readMetricsTracker(std::make_unique<HistoryView::ReadMetricsTracker>(
_peer))
, _pathGradient(
HistoryView::MakePathShiftGradient(
controller->chatStyle(),
@@ -365,6 +370,10 @@ HistoryInner::HistoryInner(
refreshAboutView();
setMouseTracking(true);
Core::App().inAppKeyPressed(
) | rpl::on_next([=] {
registerReadMetricsActivity();
}, lifetime());
_controller->gifPauseLevelChanged(
) | rpl::on_next([=] {
if (!elementAnimationsPaused()) {
@@ -419,9 +428,12 @@ HistoryInner::HistoryInner(
}, lifetime());
session().data().viewLayoutChanged(
) | rpl::filter([=](not_null<const Element*> view) {
return (view == viewByItem(view->data())) && view->isUnderCursor();
return (view == viewByItem(view->data()));
}) | rpl::on_next([=](not_null<const Element*> view) {
mouseActionUpdate();
markReadMetricsStale();
if (view->isUnderCursor()) {
mouseActionUpdate();
}
}, lifetime());
session().data().itemDataChanges(
@@ -1134,8 +1146,12 @@ void HistoryInner::startEffectOnRead(not_null<HistoryItem*> item) {
}
void HistoryInner::paintEvent(QPaintEvent *e) {
if (_controller->contentOverlapped(this, e)
|| hasPendingResizedItems()) {
const auto overlapped = _controller->contentOverlapped(this, e);
const auto pendingResized = hasPendingResizedItems();
_readMetricsTracker->setScreenActive(!overlapped
&& !pendingResized
&& _widget->markingContentsRead());
if (overlapped || pendingResized) {
return;
} else if (_recountedAfterPendingResizedItems) {
_recountedAfterPendingResizedItems = false;
@@ -1179,6 +1195,10 @@ void HistoryInner::paintEvent(QPaintEvent *e) {
}
_translateTracker->startBunch();
const auto metricsStale = base::take(_readMetricsStale);
if (metricsStale) {
_readMetricsTracker->startBatch(_visibleAreaTop, _visibleAreaBottom);
}
auto readTill = (HistoryItem*)nullptr;
auto readContents = base::flat_set<not_null<HistoryItem*>>();
auto startEffects = base::flat_set<not_null<const Element*>>();
@@ -1187,6 +1207,9 @@ void HistoryInner::paintEvent(QPaintEvent *e) {
if (_pinnedItem) {
_translateTracker->add(_pinnedItem);
}
if (metricsStale) {
_readMetricsTracker->endBatch();
}
_translateTracker->finishBunch();
if (!startEffects.empty()) {
for (const auto &view : startEffects) {
@@ -1208,6 +1231,9 @@ void HistoryInner::paintEvent(QPaintEvent *e) {
int height) {
_translateTracker->add(view);
const auto item = view->data();
if (metricsStale && height > 0) {
_readMetricsTracker->push(item, top, height);
}
const auto isSponsored = item->isSponsored();
const auto isUnread = !item->out()
&& item->unread(_history)
@@ -1670,6 +1696,7 @@ void HistoryInner::touchEvent(QTouchEvent *e) {
_touchPrevPos = _touchPos;
_touchPos = e->touchPoints().cbegin()->screenPos().toPoint();
}
registerReadMetricsActivity();
switch (e->type()) {
case QEvent::TouchBegin: {
@@ -1792,6 +1819,7 @@ void HistoryInner::mouseMoveEvent(QMouseEvent *e) {
}
if (reallyMoved) {
_mouseActive = true;
registerReadMetricsActivity();
lastGlobalPosition = e->globalPos();
if (!buttonsPressed || (_scrollDateLink && ClickHandler::getPressed() == _scrollDateLink)) {
keepScrollDateForNow();
@@ -1851,6 +1879,7 @@ void HistoryInner::mousePressEvent(QMouseEvent *e) {
return;
}
_mouseActive = true;
registerReadMetricsActivity();
mouseActionStart(e->globalPos(), e->button());
}
@@ -2279,6 +2308,7 @@ void HistoryInner::mouseReleaseEvent(QMouseEvent *e) {
e->accept();
return;
}
registerReadMetricsActivity();
mouseActionFinish(e->globalPos(), e->button());
if (!rect().contains(e->pos())) {
leaveEvent(e);
@@ -2286,6 +2316,7 @@ void HistoryInner::mouseReleaseEvent(QMouseEvent *e) {
}
void HistoryInner::mouseDoubleClickEvent(QMouseEvent *e) {
registerReadMetricsActivity();
mouseActionStart(e->globalPos(), e->button());
const auto mouseActionView = viewByItem(_mouseActionItem);
@@ -3641,6 +3672,17 @@ void HistoryInner::keyPressEvent(QKeyEvent *e) {
}
}
void HistoryInner::markReadMetricsStale() {
_readMetricsStale = true;
update();
}
void HistoryInner::registerReadMetricsActivity() {
if (_widget->markingContentsRead()) {
_readMetricsTracker->registerActivity();
}
}
void HistoryInner::checkActivation() {
if (!_widget->markingMessagesRead()) {
return;
@@ -3771,6 +3813,8 @@ void HistoryInner::visibleAreaUpdated(int top, int bottom) {
auto scrolledUp = (top < _visibleAreaTop);
_visibleAreaTop = top;
_visibleAreaBottom = bottom;
markReadMetricsStale();
registerReadMetricsActivity();
const auto visibleAreaHeight = bottom - top;
// if history has pending resize events we should not update scrollTopItem
@@ -40,6 +40,7 @@ enum class ElementChatMode : char;
class EmptyPainter;
class Element;
class TranslateTracker;
class ReadMetricsTracker;
struct PinnedId;
struct SelectedQuote;
class AboutView;
@@ -256,6 +257,8 @@ protected:
private:
void onTouchSelect();
void onTouchScrollTimer();
void markReadMetricsStale();
void registerReadMetricsActivity();
[[nodiscard]] static int SelectionViewOffset(
not_null<const HistoryInner*> inner,
@@ -497,6 +500,8 @@ private:
std::unique_ptr<HistoryView::AboutView> _aboutView;
std::unique_ptr<HistoryView::EmptyPainter> _emptyPainter;
std::unique_ptr<HistoryView::TranslateTracker> _translateTracker;
std::unique_ptr<HistoryView::ReadMetricsTracker> _readMetricsTracker;
bool _readMetricsStale = false;
rpl::event_stream<not_null<DocumentData*>> _sendIntroSticker;
mutable History *_curHistory = nullptr;
@@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/unixtime.h"
#include "base/qt/qt_key_modifiers.h"
#include "base/qt/qt_common_adapters.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_components.h"
#include "history/history_item_helpers.h"
@@ -27,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_service_message.h"
#include "history/view/history_view_cursor_state.h"
#include "history/view/history_view_translate_tracker.h"
#include "history/view/history_view_read_metrics_tracker.h"
#include "history/view/history_view_top_peers_selector.h"
#include "history/view/history_view_quick_action.h"
#include "chat_helpers/message_field.h"
@@ -99,6 +101,16 @@ constexpr auto kClearUserpicsAfter = 50;
return history ? std::make_unique<TranslateTracker>(history) : nullptr;
}
[[nodiscard]] std::unique_ptr<ReadMetricsTracker> MaybeReadMetricsTracker(
Context context,
History *history) {
if (!history
|| (context != Context::History && context != Context::Replies)) {
return nullptr;
}
return std::make_unique<ReadMetricsTracker>(history->peer);
}
} // namespace
WindowListDelegate::WindowListDelegate(
@@ -409,6 +421,9 @@ ListWidget::ListWidget(
, _replyButtonManager(std::make_unique<ReplyButton::Manager>(
[=](QRect updated) { update(updated); }))
, _translateTracker(MaybeTranslateTracker(_delegate->listTranslateHistory()))
, _readMetricsTracker(MaybeReadMetricsTracker(
_delegate->listContext(),
_delegate->listTranslateHistory()))
, _scrollDateCheck([this] { scrollDateCheck(); })
, _applyUpdatedScrollState([this] { applyUpdatedScrollState(); })
, _selectEnabled(_delegate->listAllowsMultiSelect())
@@ -419,12 +434,19 @@ ListWidget::ListWidget(
, _touchSelectTimer([=] { onTouchSelect(); })
, _touchScrollTimer([=] { onTouchScrollTimer(); })
, _middleClickAutoscroll(
[=](int d) { _delegate->listScrollTo(_visibleTop + d, false); },
[=](const QCursor &cursor) { setCursor(cursor); },
[=] { mouseActionUpdate(QCursor::pos()); setCursor(_cursor); },
[=] { return window()->isActiveWindow(); }) {
[=](int d) { _delegate->listScrollTo(_visibleTop + d, false); },
[=](const QCursor &cursor) { setCursor(cursor); },
[=] { mouseActionUpdate(QCursor::pos()); setCursor(_cursor); },
[=] { return window()->isActiveWindow(); }) {
setAttribute(Qt::WA_AcceptTouchEvents);
setMouseTracking(true);
if (_readMetricsTracker) {
Core::App().inAppKeyPressed(
) | rpl::on_next([=] {
registerReadMetricsActivity();
}, lifetime());
}
_scrollDateHideTimer.setCallback([this] { scrollDateHideByTimer(); });
_session->data().viewRepaintRequest(
) | rpl::on_next([this](Data::RequestViewRepaint data) {
@@ -451,6 +473,7 @@ ListWidget::ListWidget(
_session->data().viewLayoutChanged(
) | rpl::on_next([this](auto view) {
if (view->delegate() == this) {
markReadMetricsStale();
if (view->isUnderCursor()) {
mouseActionUpdate();
}
@@ -1112,29 +1135,6 @@ int ListWidget::findNearestItem(Data::MessagePosition position) const {
: int(after - begin(_items));
}
HistoryItemsList ListWidget::collectVisibleItems() const {
auto result = HistoryItemsList();
const auto from = std::lower_bound(
begin(_items),
end(_items),
_visibleTop,
[this](auto &elem, int top) {
return this->itemTop(elem) + elem->height() <= top;
});
const auto to = std::lower_bound(
begin(_items),
end(_items),
_visibleBottom,
[this](auto &elem, int bottom) {
return this->itemTop(elem) < bottom;
});
result.reserve(to - from);
for (auto i = from; i != to; ++i) {
result.push_back((*i)->data());
}
return result;
}
void ListWidget::visibleTopBottomUpdated(
int visibleTop,
int visibleBottom) {
@@ -1146,6 +1146,8 @@ void ListWidget::visibleTopBottomUpdated(
const auto scrolledUp = (visibleTop < _visibleTop);
_visibleTop = visibleTop;
_visibleBottom = visibleBottom;
markReadMetricsStale();
registerReadMetricsActivity();
// Unload userpics.
if (_userpics.size() > kClearUserpicsAfter) {
@@ -2274,11 +2276,21 @@ void ListWidget::checkActivation() {
}
void ListWidget::paintEvent(QPaintEvent *e) {
if (_delegate->listIgnorePaintEvent(this, e)) {
const auto overlapped = _delegate->listIgnorePaintEvent(this, e);
if (_readMetricsTracker) {
_readMetricsTracker->setScreenActive(
!overlapped && markingContentsRead());
}
if (overlapped) {
return;
} else if (_translateTracker) {
_translateTracker->startBunch();
}
const auto metricsStale = _readMetricsTracker
&& base::take(_readMetricsStale);
if (metricsStale) {
_readMetricsTracker->startBatch(_visibleTop, _visibleBottom);
}
auto readTill = (HistoryItem*)nullptr;
auto readContents = base::flat_set<not_null<HistoryItem*>>();
const auto markingAsViewed = markingMessagesRead();
@@ -2287,6 +2299,9 @@ void ListWidget::paintEvent(QPaintEvent *e) {
_delegate->listAddTranslatedItems(_translateTracker.get());
_translateTracker->finishBunch();
}
if (metricsStale) {
_readMetricsTracker->endBatch();
}
if (markingAsViewed && readTill) {
_delegate->listMarkReadTill(readTill);
}
@@ -2345,6 +2360,9 @@ void ListWidget::paintEvent(QPaintEvent *e) {
if (_translateTracker) {
_translateTracker->add(view);
}
if (metricsStale && height > 0) {
_readMetricsTracker->push(item, top, height);
}
const auto isSponsored = item->isSponsored();
const auto isUnread = _delegate->listElementShownUnread(view)
&& item->isRegular();
@@ -2770,6 +2788,7 @@ auto ListWidget::scrollKeyEvents() const
}
void ListWidget::mouseDoubleClickEvent(QMouseEvent *e) {
registerReadMetricsActivity();
mouseActionStart(e->globalPos(), e->button());
trySwitchToWordSelection();
if (!ClickHandler::getActive()
@@ -3057,6 +3076,7 @@ void ListWidget::mousePressEvent(QMouseEvent *e) {
return;
}
_mouseActive = true;
registerReadMetricsActivity();
mouseActionStart(e->globalPos(), e->button());
}
@@ -3160,6 +3180,7 @@ void ListWidget::touchEvent(QTouchEvent *e) {
_touchPrevPos = _touchPos;
_touchPos = e->touchPoints().cbegin()->screenPos().toPoint();
}
registerReadMetricsActivity();
switch (e->type()) {
case QEvent::TouchBegin: {
@@ -3273,6 +3294,7 @@ void ListWidget::mouseMoveEvent(QMouseEvent *e) {
}
if (reallyMoved) {
_mouseActive = true;
registerReadMetricsActivity();
lastGlobalPosition = e->globalPos();
if (!buttonsPressed
|| (_scrollDateLink
@@ -3292,12 +3314,24 @@ void ListWidget::mouseReleaseEvent(QMouseEvent *e) {
e->accept();
return;
}
registerReadMetricsActivity();
mouseActionFinish(e->globalPos(), e->button());
if (!rect().contains(e->pos())) {
leaveEvent(e);
}
}
void ListWidget::markReadMetricsStale() {
_readMetricsStale = true;
update();
}
void ListWidget::registerReadMetricsActivity() {
if (_readMetricsTracker && markingContentsRead()) {
_readMetricsTracker->registerActivity();
}
}
void ListWidget::touchScrollUpdated(const QPoint &screenPos) {
_touchPos = screenPos;
_delegate->listScrollTo(
@@ -68,6 +68,7 @@ struct TextState;
struct StateRequest;
class EmojiInteractions;
class TranslateTracker;
class ReadMetricsTracker;
enum class CursorState : char;
enum class PointState : char;
enum class Context : char;
@@ -541,6 +542,8 @@ private:
void onTouchSelect();
void onTouchScrollTimer();
void markReadMetricsStale();
void registerReadMetricsActivity();
void updateAroundPositionFromNearest(int nearestIndex);
void refreshRows(const Data::MessagesSlice &old);
@@ -599,7 +602,6 @@ private:
[[nodiscard]] Element *strictFindItemByY(int y) const;
[[nodiscard]] int findNearestItem(Data::MessagePosition position) const;
void viewReplaced(not_null<const Element*> was, Element *now);
[[nodiscard]] HistoryItemsList collectVisibleItems() const;
void checkMoveToOtherViewer();
void updateVisibleTopItem();
@@ -794,6 +796,8 @@ private:
std::unique_ptr<ReplyButton::Manager> _replyButtonManager;
std::unique_ptr<TranslateTracker> _translateTracker;
std::unique_ptr<ReadMetricsTracker> _readMetricsTracker;
bool _readMetricsStale = false;
int _minHeight = 0;
int _visibleTop = 0;
@@ -0,0 +1,353 @@
/*
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/view/history_view_read_metrics_tracker.h"
#include "api/api_read_metrics.h"
#include "apiwrap.h"
#include "base/random.h"
#include "core/application.h"
#include "data/data_peer.h"
#include "history/history_item.h"
#include "main/main_session.h"
namespace HistoryView {
namespace {
constexpr auto kGracePeriod = crl::time(300);
constexpr auto kActivityTimeout = 15 * crl::time(1000);
constexpr auto kMaxTrackingDuration = 5 * 60 * crl::time(1000);
constexpr auto kMinReportThreshold = crl::time(300);
} // namespace
ReadMetricsTracker::ReadMetricsTracker(not_null<PeerData*> peer)
: _peer(peer)
, _timer([=] { onTimeout(); }) {
Core::App().appDeactivatedValue(
) | rpl::on_next([=](bool deactivated) {
const auto appActive = !deactivated;
if (_appActive == appActive) {
return;
}
_appActive = appActive;
refreshPaused(crl::now());
}, _lifetime);
}
ReadMetricsTracker::~ReadMetricsTracker() {
finalizeAll();
}
void ReadMetricsTracker::startBatch(int visibleTop, int visibleBottom) {
_batchNow = crl::now();
sync(_batchNow);
_batchViewportHeight = visibleBottom - visibleTop;
_batchVisibleTop = visibleTop;
_batchVisibleBottom = visibleBottom;
_batchVisible.clear();
}
void ReadMetricsTracker::push(
not_null<HistoryItem*> item,
int itemTop,
int itemHeight) {
if (!ShouldTrack(item)) {
return;
}
const auto msgId = item->id;
_batchVisible.emplace(msgId);
const auto clippedTop = std::max(itemTop, _batchVisibleTop) - itemTop;
const auto clippedBottom = std::min(
itemTop + itemHeight,
_batchVisibleBottom) - itemTop;
const auto addTracked = [&] {
auto tracked = TrackedItem();
tracked.entryGracePending = true;
tracked.entryGraceStart = _batchNow;
tracked.maxItemHeight = itemHeight;
tracked.maxViewportHeight = _batchViewportHeight;
tracked.seenTop = clippedTop;
tracked.seenBottom = clippedBottom;
_tracked.emplace(msgId, tracked);
};
auto it = _tracked.find(msgId);
if (it == end(_tracked)) {
addTracked();
return;
}
auto &tracked = it->second;
if (tracked.exitGracePending) {
if (_batchNow - tracked.exitGraceStart >= kGracePeriod) {
finalize(msgId, tracked);
_tracked.erase(it);
addTracked();
return;
}
tracked.exitGracePending = false;
tracked.exitGraceStart = 0;
tracked.lastUpdate = _batchNow;
}
tracked.seenTop = std::min(tracked.seenTop, clippedTop);
tracked.seenBottom = std::max(tracked.seenBottom, clippedBottom);
accumulate_max(tracked.maxItemHeight, itemHeight);
accumulate_max(tracked.maxViewportHeight, _batchViewportHeight);
}
void ReadMetricsTracker::endBatch() {
for (auto it = _tracked.begin(); it != _tracked.end();) {
if (_batchVisible.contains(it->first)) {
++it;
continue;
}
auto &tracked = it->second;
if (tracked.entryGracePending) {
it = _tracked.erase(it);
continue;
}
if (!tracked.exitGracePending) {
tracked.exitGracePending = true;
tracked.exitGraceStart = _batchNow;
}
++it;
}
_currentlyVisible = std::move(_batchVisible);
restartTimer();
}
void ReadMetricsTracker::registerActivity() {
if (!_appActive || !_screenActive) {
return;
}
const auto now = crl::now();
const auto activityDeadline = activeUntil();
if (activityDeadline && activityDeadline < now) {
sync(now);
}
_lastActivity = now;
restartTimer();
}
void ReadMetricsTracker::setScreenActive(bool active) {
if (_screenActive == active) {
return;
}
_screenActive = active;
refreshPaused(crl::now());
}
void ReadMetricsTracker::pauseTracking() {
setScreenActive(false);
}
void ReadMetricsTracker::resumeTracking() {
setScreenActive(true);
}
void ReadMetricsTracker::onTimeout() {
sync(crl::now());
}
void ReadMetricsTracker::sync(crl::time now) {
const auto activeUntil = this->activeUntil();
processTransitions(now, activeUntil);
accumulate(now, activeUntil);
restartTimer();
}
void ReadMetricsTracker::processTransitions(
crl::time now,
crl::time activeUntil) {
for (auto it = _tracked.begin(); it != _tracked.end();) {
auto &tracked = it->second;
if (tracked.entryGracePending
&& !_paused
&& now - tracked.entryGraceStart >= kGracePeriod) {
tracked.entryGracePending = false;
do {
tracked.viewId = base::RandomValue<uint64>();
} while (!tracked.viewId);
tracked.trackingStarted = tracked.entryGraceStart;
tracked.lastUpdate = tracked.trackingStarted;
}
if (tracked.exitGracePending
&& !_paused
&& now - tracked.exitGraceStart >= kGracePeriod) {
finalize(it->first, tracked);
it = _tracked.erase(it);
continue;
}
if (tracked.viewId && !tracked.entryGracePending && !tracked.exitGracePending) {
const auto deadline = tracked.trackingStarted + kMaxTrackingDuration;
if (now >= deadline) {
if (!_paused
&& _currentlyVisible.contains(it->first)
&& tracked.lastUpdate < deadline) {
addElapsed(tracked, tracked.lastUpdate, deadline, activeUntil);
tracked.lastUpdate = deadline;
}
finalize(it->first, tracked);
it = _tracked.erase(it);
continue;
}
}
++it;
}
}
void ReadMetricsTracker::accumulate(crl::time now, crl::time activeUntil) {
for (auto &[msgId, tracked] : _tracked) {
if (!tracked.viewId
|| tracked.entryGracePending
|| tracked.exitGracePending
|| tracked.lastUpdate <= 0) {
continue;
}
if (_paused || !_currentlyVisible.contains(msgId)) {
tracked.lastUpdate = now;
continue;
}
addElapsed(tracked, tracked.lastUpdate, now, activeUntil);
tracked.lastUpdate = now;
}
}
void ReadMetricsTracker::refreshPaused(crl::time now) {
const auto paused = !_appActive || !_screenActive;
if (_paused == paused) {
return;
} else if (paused) {
const auto activeUntil = this->activeUntil();
processTransitions(now, activeUntil);
accumulate(now, activeUntil);
_pausedSince = now;
for (auto &[msgId, tracked] : _tracked) {
if (tracked.entryGracePending || tracked.exitGracePending) {
continue;
}
tracked.lastUpdate = now;
}
} else {
const auto delta = now - _pausedSince;
for (auto &[msgId, tracked] : _tracked) {
if (tracked.entryGracePending) {
tracked.entryGraceStart = (tracked.entryGraceStart < _pausedSince)
? (tracked.entryGraceStart + delta)
: now;
} else if (tracked.exitGracePending) {
tracked.exitGraceStart = (tracked.exitGraceStart < _pausedSince)
? (tracked.exitGraceStart + delta)
: now;
} else if (tracked.viewId) {
tracked.lastUpdate = now;
}
}
_pausedSince = 0;
}
_paused = paused;
restartTimer();
}
void ReadMetricsTracker::restartTimer() {
if (_tracked.empty()) {
_timer.cancel();
return;
}
const auto now = crl::now();
auto nearest = crl::time(0);
const auto updateNearest = [&](crl::time deadline) {
if (!deadline) {
return;
}
if (!nearest || deadline < nearest) {
nearest = deadline;
}
};
const auto activityDeadline = activeUntil();
for (const auto &[msgId, tracked] : _tracked) {
if (tracked.entryGracePending && !_paused) {
updateNearest(tracked.entryGraceStart + kGracePeriod);
} else if (tracked.exitGracePending && !_paused) {
updateNearest(tracked.exitGraceStart + kGracePeriod);
} else if (tracked.viewId) {
updateNearest(tracked.trackingStarted + kMaxTrackingDuration);
if (!_paused && _currentlyVisible.contains(msgId) && activityDeadline > now) {
updateNearest(activityDeadline);
}
}
}
if (!nearest) {
_timer.cancel();
return;
}
_timer.callOnce(std::max(nearest - now, crl::time(0)));
}
crl::time ReadMetricsTracker::activeUntil() const {
return _lastActivity ? (_lastActivity + kActivityTimeout) : 0;
}
void ReadMetricsTracker::addElapsed(
TrackedItem &tracked,
crl::time from,
crl::time till,
crl::time activeUntil) const {
if (till <= from) {
return;
}
tracked.totalInView += (till - from);
if (activeUntil > from) {
tracked.activeInView += std::max(std::min(till, activeUntil) - from, crl::time(0));
tracked.activeInView = std::min(tracked.activeInView, tracked.totalInView);
}
}
void ReadMetricsTracker::finalize(
MsgId msgId,
const TrackedItem &tracked) {
if (tracked.viewId == 0 || tracked.totalInView < kMinReportThreshold) {
return;
}
const auto heightRatio = (tracked.maxViewportHeight > 0)
? qRound((tracked.maxItemHeight * 1000.0) / tracked.maxViewportHeight)
: 0;
const auto seenRange = (tracked.maxItemHeight > 0)
? std::clamp(
qRound(((tracked.seenBottom - tracked.seenTop) * 1000.0)
/ tracked.maxItemHeight),
0,
1000)
: 0;
_peer->session().api().readMetrics().add(_peer, {
.msgId = msgId,
.viewId = tracked.viewId,
.timeInViewMs = int(tracked.totalInView),
.activeTimeInViewMs = int(tracked.activeInView),
.heightToViewportRatioPermille = heightRatio,
.seenRangeRatioPermille = seenRange,
});
}
void ReadMetricsTracker::finalizeAll() {
sync(crl::now());
for (const auto &[msgId, tracked] : _tracked) {
finalize(msgId, tracked);
}
_tracked.clear();
_timer.cancel();
}
bool ReadMetricsTracker::ShouldTrack(not_null<HistoryItem*> item) {
return item->isRegular() && item->hasViews();
}
} // namespace HistoryView
@@ -0,0 +1,81 @@
/*
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
#include "base/timer.h"
class PeerData;
class HistoryItem;
namespace HistoryView {
class ReadMetricsTracker final {
public:
explicit ReadMetricsTracker(not_null<PeerData*> peer);
~ReadMetricsTracker();
void startBatch(int visibleTop, int visibleBottom);
void push(not_null<HistoryItem*> item, int itemTop, int itemHeight);
void endBatch();
void registerActivity();
void setScreenActive(bool active);
void pauseTracking();
void resumeTracking();
private:
struct TrackedItem {
uint64 viewId = 0;
crl::time entryGraceStart = 0;
crl::time trackingStarted = 0;
crl::time lastUpdate = 0;
crl::time totalInView = 0;
crl::time activeInView = 0;
int seenTop = std::numeric_limits<int>::max();
int seenBottom = 0;
int maxItemHeight = 0;
int maxViewportHeight = 0;
bool entryGracePending = false;
bool exitGracePending = false;
crl::time exitGraceStart = 0;
};
void onTimeout();
void sync(crl::time now);
void processTransitions(crl::time now, crl::time activeUntil);
void accumulate(crl::time now, crl::time activeUntil);
void refreshPaused(crl::time now);
void restartTimer();
[[nodiscard]] crl::time activeUntil() const;
void addElapsed(
TrackedItem &tracked,
crl::time from,
crl::time till,
crl::time activeUntil) const;
void finalize(MsgId msgId, const TrackedItem &tracked);
void finalizeAll();
[[nodiscard]] static bool ShouldTrack(not_null<HistoryItem*> item);
const not_null<PeerData*> _peer;
base::flat_map<MsgId, TrackedItem> _tracked;
base::flat_set<MsgId> _currentlyVisible;
crl::time _batchNow = 0;
int _batchViewportHeight = 0;
int _batchVisibleTop = 0;
int _batchVisibleBottom = 0;
base::flat_set<MsgId> _batchVisible;
crl::time _lastActivity = 0;
bool _appActive = true;
bool _screenActive = true;
bool _paused = false;
crl::time _pausedSince = 0;
base::Timer _timer;
rpl::lifetime _lifetime;
};
} // namespace HistoryView