From 284cbda7c0dd04d31739a07346deabfce011cb12 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 8 Jul 2025 20:28:18 +0400 Subject: [PATCH] Track shown sponsored in video. --- Telegram/CMakeLists.txt | 2 + .../data/components/sponsored_messages.cpp | 74 ++++++-- .../data/components/sponsored_messages.h | 35 +++- .../media/view/media_view_overlay_widget.cpp | 15 ++ .../media/view/media_view_overlay_widget.h | 1 + .../media/view/media_view_playback_controls.h | 10 +- .../view/media_view_playback_sponsored.cpp | 178 ++++++++++++++++++ .../view/media_view_playback_sponsored.h | 62 ++++++ 8 files changed, 345 insertions(+), 32 deletions(-) create mode 100644 Telegram/SourceFiles/media/view/media_view_playback_sponsored.cpp create mode 100644 Telegram/SourceFiles/media/view/media_view_playback_sponsored.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 084411557c..c0d00a6dbd 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1257,6 +1257,8 @@ PRIVATE media/view/media_view_playback_controls.h media/view/media_view_playback_progress.cpp media/view/media_view_playback_progress.h + media/view/media_view_playback_sponsored.cpp + media/view/media_view_playback_sponsored.h media/system_media_controls_manager.h media/system_media_controls_manager.cpp menu/menu_antispam_validator.cpp diff --git a/Telegram/SourceFiles/data/components/sponsored_messages.cpp b/Telegram/SourceFiles/data/components/sponsored_messages.cpp index 8fca3c2de2..06cc56e191 100644 --- a/Telegram/SourceFiles/data/components/sponsored_messages.cpp +++ b/Telegram/SourceFiles/data/components/sponsored_messages.cpp @@ -28,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Data { namespace { +constexpr auto kMs = crl::time(1000); constexpr auto kRequestTimeLimit = 5 * 60 * crl::time(1000); const auto kFlaggedPreload = ((MediaPreload*)quintptr(0x01)); @@ -288,7 +289,7 @@ void SponsoredMessages::request(not_null history, Fn done) { }).send(); } -void SponsoredMessages::request( +void SponsoredMessages::requestForVideo( not_null item, Fn done) { Expects(done != nullptr); @@ -297,14 +298,23 @@ void SponsoredMessages::request( done({}); return; } - const auto id = item->fullId(); - auto &request = _requestsForVideo[id]; + const auto peer = item->history()->peer; + auto &request = _requestsForVideo[peer]; + if (TooEarlyForRequest(request.lastReceived)) { + auto prepared = prepareForVideo(peer); + if (prepared.list.empty() + || prepared.state.itemIndex < prepared.list.size() + || prepared.state.leftTillShow > 0) { + done(std::move(prepared)); + return; + } + } + request.callbacks.push_back(std::move(done)); if (request.requestId) { - done(prepareForVideo(id)); return; } { - const auto it = _dataForVideo.find(id); + const auto it = _dataForVideo.find(peer); if (it != end(_dataForVideo)) { auto &list = it->second; // Don't rebuild currently displayed messages. @@ -316,21 +326,41 @@ void SponsoredMessages::request( } } } + const auto finish = [=] { + const auto i = _requestsForVideo.find(peer); + if (i != end(_requestsForVideo)) { + for (const auto &callback : base::take(i->second.callbacks)) { + callback(prepareForVideo(peer)); + } + } + }; using Flag = MTPmessages_GetSponsoredMessages::Flag; request.requestId = _session->api().request( MTPmessages_GetSponsoredMessages( MTP_flags(Flag::f_msg_id), - item->history()->peer->input, + peer->input, MTP_int(item->id.bare)) ).done([=](const MTPmessages_sponsoredMessages &result) { - parse(id, result); - done(prepareForVideo(id)); + parseForVideo(peer, result); + finish(); }).fail([=] { - _requestsForVideo.remove(id); - done({}); + _requestsForVideo.remove(peer); + finish(); }).send(); } +void SponsoredMessages::updateForVideo( + FullMsgId itemId, + SponsoredForVideoState state) { + if (state.initial()) { + return; + } + const auto i = _dataForVideo.find(_session->data().peer(itemId.peer)); + if (i != end(_dataForVideo)) { + i->second.state = state; + } +} + void SponsoredMessages::parse( not_null history, const MTPmessages_sponsoredMessages &list) { @@ -366,10 +396,10 @@ void SponsoredMessages::parse( }); } -void SponsoredMessages::parse( - FullMsgId itemId, +void SponsoredMessages::parseForVideo( + not_null peer, const MTPmessages_sponsoredMessages &list) { - auto &request = _requestsForVideo[itemId]; + auto &request = _requestsForVideo[peer]; request.lastReceived = crl::now(); request.requestId = 0; if (!_clearTimer.isActive()) { @@ -380,24 +410,25 @@ void SponsoredMessages::parse( _session->data().processUsers(data.vusers()); _session->data().processChats(data.vchats()); - const auto history = _session->data().history(itemId.peer); + const auto history = _session->data().history(peer); const auto &messages = data.vmessages().v; - auto &list = _dataForVideo.emplace(itemId).first->second; + auto &list = _dataForVideo.emplace(peer).first->second; list.entries.clear(); list.received = crl::now(); - list.startDelay = data.vstart_delay().value_or_empty(); - list.betweenDelay = data.vbetween_delay().value_or_empty(); + list.startDelay = 2000;AssertIsDebug()// data.vstart_delay().value_or_empty() * kMs; + list.betweenDelay = 3000;//data.vbetween_delay().value_or_empty() * kMs; for (const auto &message : messages) { append([=] { - return &_dataForVideo[itemId].entries; + return &_dataForVideo[peer].entries; }, history, message); } }, [](const MTPDmessages_sponsoredMessagesEmpty &) { }); } -SponsoredForVideo SponsoredMessages::prepareForVideo(FullMsgId itemId) { - const auto i = _dataForVideo.find(itemId); +SponsoredForVideo SponsoredMessages::prepareForVideo( + not_null peer) { + const auto i = _dataForVideo.find(peer); if (i == end(_dataForVideo) || i->second.entries.empty()) { return {}; } @@ -407,6 +438,7 @@ SponsoredForVideo SponsoredMessages::prepareForVideo(FullMsgId itemId) { ) | ranges::to_vector, .startDelay = i->second.startDelay, .betweenDelay = i->second.betweenDelay, + .state = i->second.state, }; } @@ -535,6 +567,8 @@ void SponsoredMessages::append( .link = from.link, .sponsorInfo = std::move(sponsorInfo), .additionalInfo = std::move(additionalInfo), + .durationMin = 3000, AssertIsDebug()//data.vmin_display_duration().value_or_empty() * kMs, + .durationMax = 5000,//data.vmax_display_duration().value_or_empty() * kMs, }; const auto itemId = FullMsgId( history->peer->id, diff --git a/Telegram/SourceFiles/data/components/sponsored_messages.h b/Telegram/SourceFiles/data/components/sponsored_messages.h index 9aaa2a5f95..a01a8c76d0 100644 --- a/Telegram/SourceFiles/data/components/sponsored_messages.h +++ b/Telegram/SourceFiles/data/components/sponsored_messages.h @@ -71,6 +71,8 @@ struct SponsoredMessage { QString link; TextWithEntities sponsorInfo; TextWithEntities additionalInfo; + crl::time durationMin = 0; + crl::time durationMax = 0; }; struct SponsoredMessageDetails { @@ -92,10 +94,21 @@ struct SponsoredReportAction { Fn)> callback; }; +struct SponsoredForVideoState { + int itemIndex = 0; + crl::time leftTillShow = 0; + + [[nodiscard]] bool initial() const { + return !itemIndex && !leftTillShow; + } +}; + struct SponsoredForVideo { std::vector list; crl::time startDelay = 0; crl::time betweenDelay = 0; + + SponsoredForVideoState state; }; class SponsoredMessages final { @@ -120,9 +133,12 @@ public: [[nodiscard]] bool canHaveFor(not_null item) const; [[nodiscard]] bool isTopBarFor(not_null history) const; void request(not_null history, Fn done); - void request( + void requestForVideo( not_null item, Fn done); + void updateForVideo( + FullMsgId itemId, + SponsoredForVideoState state); void clearItems(not_null history); [[nodiscard]] Details lookupDetails(const FullMsgId &fullId) const; [[nodiscard]] Details lookupDetails( @@ -181,23 +197,30 @@ private: crl::time received = 0; crl::time startDelay = 0; crl::time betweenDelay = 0; + SponsoredForVideoState state; }; struct Request { mtpRequestId requestId = 0; crl::time lastReceived = 0; }; + struct RequestForVideo { + std::vector> callbacks; + mtpRequestId requestId = 0; + crl::time lastReceived = 0; + }; void parse( not_null history, const MTPmessages_sponsoredMessages &list); - void parse( - FullMsgId itemId, + void parseForVideo( + not_null peer, const MTPmessages_sponsoredMessages &list); void append( Fn*>()> entries, not_null history, const MTPSponsoredMessage &message); - [[nodiscard]] SponsoredForVideo prepareForVideo(FullMsgId itemId); + [[nodiscard]] SponsoredForVideo prepareForVideo( + not_null peer); void clearOldRequests(); const Entry *find(const FullMsgId &fullId) const; @@ -209,8 +232,8 @@ private: base::flat_map, Request> _requests; base::flat_map _viewRequests; - base::flat_map _dataForVideo; - base::flat_map _requestsForVideo; + base::flat_map, ListForVideo> _dataForVideo; + base::flat_map, RequestForVideo> _requestsForVideo; rpl::event_stream _itemRemoved; diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 99f7bc2825..0f34cd2d39 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -51,6 +51,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/view/media_view_pip.h" #include "media/view/media_view_overlay_raster.h" #include "media/view/media_view_overlay_opengl.h" +#include "media/view/media_view_playback_sponsored.h" #include "media/stories/media_stories_share.h" #include "media/stories/media_stories_view.h" #include "media/streaming/media_streaming_document.h" @@ -335,6 +336,7 @@ struct OverlayWidget::Streamed { Streaming::Instance instance; std::unique_ptr controls; + std::unique_ptr sponsored; std::unique_ptr powerSaveBlocker; bool ready = false; @@ -4069,6 +4071,9 @@ void OverlayWidget::initStreamingThumbnail() { void OverlayWidget::streamingReady(Streaming::Information &&info) { _streamed->ready = true; + if (const auto sponsored = _streamed->sponsored.get()) { + sponsored->start(); + } if (videoShown()) { applyVideoSize(); _streamedQualityChangeFrame = QImage(); @@ -4090,6 +4095,7 @@ void OverlayWidget::applyVideoSize() { bool OverlayWidget::createStreamingObjects() { Expects(_photo || _document); + Expects(!_streamed); const auto origin = fileOrigin(); const auto callback = [=] { waitingAnimationCallback(); }; @@ -4122,6 +4128,15 @@ bool OverlayWidget::createStreamingObjects() { _body, static_cast(this)); _streamed->controls->show(); + _streamed->sponsored = PlaybackSponsored::Has(_message) + ? std::make_unique(_body, _message) + : nullptr; + if (const auto sponsored = _streamed->sponsored.get()) { + _layerBg->layerShownValue( + ) | rpl::start_with_next([=](bool shown) { + sponsored->setPaused(shown); + }, sponsored->lifetime()); + } refreshClipControllerGeometry(); } return true; diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h index 8787be47e4..3784d54a1a 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.h +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.h @@ -78,6 +78,7 @@ struct ContentLayout; namespace Media::View { +class PlaybackSponsored; class GroupThumbs; class Pip; diff --git a/Telegram/SourceFiles/media/view/media_view_playback_controls.h b/Telegram/SourceFiles/media/view/media_view_playback_controls.h index a91b8ad2e9..be3e84c824 100644 --- a/Telegram/SourceFiles/media/view/media_view_playback_controls.h +++ b/Telegram/SourceFiles/media/view/media_view_playback_controls.h @@ -19,14 +19,13 @@ class MediaSlider; class PopupMenu; } // namespace Ui -namespace Media { -namespace Player { +namespace Media::Player { struct TrackState; class SettingsButton; class SpeedController; -} // namespace Player +} // namespace Media::Player -namespace View { +namespace Media::View { class PlaybackProgress; @@ -131,5 +130,4 @@ private: }; -} // namespace View -} // namespace Media +} // namespace Media::View diff --git a/Telegram/SourceFiles/media/view/media_view_playback_sponsored.cpp b/Telegram/SourceFiles/media/view/media_view_playback_sponsored.cpp new file mode 100644 index 0000000000..bbda921d8a --- /dev/null +++ b/Telegram/SourceFiles/media/view/media_view_playback_sponsored.cpp @@ -0,0 +1,178 @@ +/* +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 "media/view/media_view_playback_sponsored.h" + +#include "data/components/sponsored_messages.h" +#include "data/data_session.h" +#include "history/history.h" +#include "history/history_item.h" +#include "main/main_session.h" + +namespace Media::View { +namespace { + +constexpr auto kStartDelayMin = crl::time(1000); +constexpr auto kDurationMin = 5 * crl::time(1000); + +} // namespace + +PlaybackSponsored::PlaybackSponsored( + QWidget *parent, + not_null item) +: _parent(parent) +, _session(&item->history()->session()) +, _itemId(item->fullId()) +, _timer([=] { update(); }) { + _session->sponsoredMessages().requestForVideo(item, crl::guard(this, [=]( + Data::SponsoredForVideo data) { + if (data.list.empty()) { + return; + } + _data = std::move(data); + if (_data->state.initial() + || (_data->state.itemIndex > _data->list.size()) + || (_data->state.itemIndex == _data->list.size() + && _data->state.leftTillShow <= 0)) { + _data->state.itemIndex = 0; + _data->state.leftTillShow = std::max( + _data->startDelay, + kStartDelayMin); + } + update(); + })); +} + +PlaybackSponsored::~PlaybackSponsored() { + saveState(); +} + +void PlaybackSponsored::start() { + _started = true; + if (!_paused) { + _start = crl::now(); + update(); + } +} + +void PlaybackSponsored::setPaused(bool paused) { + if (_paused == paused) { + return; + } + _paused = paused; + if (!_started) { + return; + } else if (_paused) { + update(); + const auto state = computeState(); + _start = 0; + _timer.cancel(); + } else { + _start = crl::now(); + update(); + } +} + +void PlaybackSponsored::finish() { + _timer.cancel(); + if (_data) { + saveState(); + _data = std::nullopt; + } +} + +void PlaybackSponsored::update() { + if (!_data || !_start) { + return; + } + + const auto [now, state] = computeState(); + const auto message = (_data->state.itemIndex < _data->list.size()) + ? &_data->list[state.itemIndex] + : nullptr; + const auto duration = message + ? std::max( + message->durationMin + kDurationMin, + message->durationMax) + : crl::time(0); + if (_data->state.leftTillShow > 0 && state.leftTillShow <= 0) { + _data->state.leftTillShow = 0; + if (duration) { + show(*message); + + _start = now; + _timer.callOnce(duration); + saveState(); + } else { + finish(); + } + } else if (_data->state.leftTillShow <= 0 + && state.leftTillShow <= -duration) { + hide(); + + ++_data->state.itemIndex; + _data->state.leftTillShow = std::max( + _data->betweenDelay, + kStartDelayMin); + _start = now; + _timer.callOnce(_data->state.leftTillShow); + saveState(); + } else { + if (state.leftTillShow <= 0 && duration && !_widget) { + show(*message); + } + _data->state = state; + _timer.callOnce((state.leftTillShow > 0) + ? state.leftTillShow + : (state.leftTillShow + duration)); + } +} + +void PlaybackSponsored::show(const Data::SponsoredMessage &data) { + _widget = std::make_unique(_parent); + _widget->setGeometry(_parent->rect()); + _widget->paintRequest() | rpl::start_with_next([=] { + auto p = QPainter(_widget.get()); + p.fillRect(_widget->rect(), QColor(0, 128, 0, 128)); + }, _widget->lifetime()); + _widget->show(); +} + +void PlaybackSponsored::hide() { + _widget = nullptr; +} + +void PlaybackSponsored::saveState() { + _session->sponsoredMessages().updateForVideo( + _itemId, + computeState().data); +} + +PlaybackSponsored::State PlaybackSponsored::computeState() const { + auto result = State{ crl::now() }; + if (!_data) { + return result; + } + result.data = _data->state; + if (!_start) { + return result; + } + const auto elapsed = result.now - _start; + result.data.leftTillShow -= elapsed; + return result; +} + +rpl::lifetime &PlaybackSponsored::lifetime() { + return _lifetime; +} + +bool PlaybackSponsored::Has(HistoryItem *item) { + return item + && item->history()->session().sponsoredMessages().canHaveFor(item); +} + +} // namespace Media::View diff --git a/Telegram/SourceFiles/media/view/media_view_playback_sponsored.h b/Telegram/SourceFiles/media/view/media_view_playback_sponsored.h new file mode 100644 index 0000000000..94748a8e88 --- /dev/null +++ b/Telegram/SourceFiles/media/view/media_view_playback_sponsored.h @@ -0,0 +1,62 @@ +/* +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" +#include "base/weak_ptr.h" +#include "data/components/sponsored_messages.h" + +namespace Main { +class Session; +} // namespace Main + +namespace Media::View { + +class PlaybackSponsored final : public base::has_weak_ptr { +public: + PlaybackSponsored(QWidget *parent, not_null item); + ~PlaybackSponsored(); + + void start(); + void setPaused(bool paused); + + [[nodiscard]] rpl::lifetime &lifetime(); + + [[nodiscard]] static bool Has(HistoryItem *item); + +private: + struct State { + crl::time now = 0; + Data::SponsoredForVideoState data; + }; + + void update(); + void finish(); + void show(const Data::SponsoredMessage &data); + void hide(); + [[nodiscard]] State computeState() const; + void saveState(); + + const not_null _parent; + const not_null _session; + const FullMsgId _itemId; + + std::unique_ptr _widget; + + crl::time _start = 0; + bool _started = false; + bool _paused = false; + base::Timer _timer; + + std::optional _data; + + rpl::lifetime _lifetime; + +}; + +} // namespace Media::View