mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2026-07-25 06:54:43 +00:00
Better post views tracking.
This commit is contained in:
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user