/* 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 "boxes/premium_preview_box.h" #include "data/components/sponsored_messages.h" #include "data/data_file_origin.h" #include "data/data_photo.h" #include "data/data_photo_media.h" #include "data/data_session.h" #include "history/history.h" #include "history/history_item.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "menu/menu_sponsored.h" #include "ui/effects/numbers_animation.h" #include "ui/effects/ripple_animation.h" #include "ui/widgets/menu/menu_add_action_callback.h" #include "ui/widgets/menu/menu_add_action_callback_factory.h" #include "ui/widgets/buttons.h" #include "ui/widgets/popup_menu.h" #include "ui/basic_click_handlers.h" #include "ui/painter.h" #include "ui/ui_utility.h" #include "ui/cached_round_corners.h" #include "styles/style_chat.h" #include "styles/style_media_view.h" namespace Media::View { namespace { constexpr auto kStartDelayMin = crl::time(1000); constexpr auto kDurationMin = 5 * crl::time(1000); enum class Action { Close, PromotePremium, Pause, Unpause, }; class Close final : public Ui::RippleButton { public: Close( not_null parent, const style::RippleAnimation &st, rpl::producer allowCloseAt); [[nodiscard]] rpl::producer actions() const; private: QPoint prepareRippleStartPosition() const override; QImage prepareRippleMask() const override; void paintEvent(QPaintEvent *e) override; void updateProgress(crl::time now); rpl::event_stream _actions; Ui::NumbersAnimation _countdown; Ui::Animations::Basic _progress; base::Timer _noAnimationTimer; crl::time _allowCloseAt = 0; crl::time _startedAt = 0; crl::time _pausedAt = 0; int _secondsTill = 0; int _rippleSize = 0; QPoint _rippleOrigin; bool _allowClose = false; }; Close::Close( not_null parent, const style::RippleAnimation &st, rpl::producer allowCloseAt) : RippleButton(parent, st) , _countdown(st::mediaSponsoredCloseFont, [=] { update(); }) , _progress([=](crl::time now) { updateProgress(now); }) , _noAnimationTimer([=] { updateProgress(crl::now()); }) , _startedAt(crl::now()) { resize(st::mediaSponsoredCloseFull, st::mediaSponsoredCloseFull); const auto size = st::mediaSponsoredCloseRipple; const auto cut = int(base::SafeRound((width() - size) / 2.)); _rippleSize = std::min(width() - 2 * cut, height() - 2 * cut); _rippleOrigin = QPoint( (width() - _rippleSize) / 2, (height() - _rippleSize) / 2); std::move( allowCloseAt ) | rpl::start_with_next([=](crl::time at) { const auto now = crl::now(); if (!at) { updateProgress(now); _pausedAt = now; _progress.stop(); } else { if (_pausedAt) { _startedAt += now - base::take(_pausedAt); } _allowCloseAt = at; updateProgress(now); if (!anim::Disabled()) { _progress.start(); } else if (!_allowClose) { _noAnimationTimer.callEach(crl::time(200)); } } }, lifetime()); updateProgress(_startedAt); setClickedCallback([=] { _actions.fire(_allowClose ? Action::Close : Action::PromotePremium); }); } rpl::producer Close::actions() const { return _actions.events(); } void Close::updateProgress(crl::time now) { update(); } QPoint Close::prepareRippleStartPosition() const { return mapFromGlobal(QCursor::pos()) - _rippleOrigin; } QImage Close::prepareRippleMask() const { return Ui::RippleAnimation::EllipseMask({ _rippleSize, _rippleSize }); } void Close::paintEvent(QPaintEvent *e) { auto p = QPainter(this); paintRipple(p, _rippleOrigin); const auto now = crl::now(); if (!_pausedAt) { _allowClose = (now >= _allowCloseAt); } const auto msTill = _allowCloseAt - (_pausedAt ? _pausedAt : now); const auto msFull = _allowCloseAt - _startedAt; const auto secondsTill = (std::max(msTill, crl::time()) + 999) / 1000; const auto secondsFull = (std::max(msFull, crl::time()) + 999) / 1000; const auto allowCloseLeft = anim::Disabled() ? (secondsFull ? (secondsTill / float64(secondsFull)) : 0) : std::max(msFull ? (msTill / float64(msFull)) : 0., 0.); const auto duration = crl::time(st::fadeWrapDuration); const auto allowedProgress = anim::Disabled() ? (secondsTill ? 0. : 1.) : std::clamp(-msTill, crl::time(), duration) / float64(duration); if (_secondsTill != secondsTill) { const auto initial = !_secondsTill; _secondsTill = secondsTill; _countdown.setText(QString::number(_secondsTill), _secondsTill); if (initial) { _countdown.finishAnimating(); } } auto pen = st::mediaviewTextLinkFg->p; if (allowedProgress < 1.) { if (allowedProgress > 0.) { p.setOpacity(1. - allowedProgress); } p.setPen(pen); const auto inner = QRect( (width() - st::mediaSponsoredCloseDiameter) / 2, (height() - st::mediaSponsoredCloseDiameter) / 2, st::mediaSponsoredCloseDiameter, st::mediaSponsoredCloseDiameter); p.setFont(st::mediaSponsoredCloseFont); _countdown.paint( p, inner.x() + (inner.width() - _countdown.countWidth()) / 2, (inner.y() + (inner.height() - st::mediaSponsoredCloseFont->height) / 2), width()); const auto skip = 0.23; const auto len = int(base::SafeRound( arc::kFullLength * (1. - skip) * allowCloseLeft)); if (len > 0) { const auto from = arc::kFullLength / 4; auto hq = PainterHighQualityEnabler(p); pen.setWidthF(st::mediaSponsoredCloseStroke); pen.setCapStyle(Qt::RoundCap); p.setPen(pen); p.drawArc(inner, from, len); } p.setOpacity(1.); } const auto sizeFinal = st::mediaSponsoredCloseSize; const auto sizeSmall = st::mediaSponsoredCloseCorner; const auto twiceFinal = st::mediaSponsoredCloseTwice; const auto twiceSmall = st::mediaSponsoredCloseSmall; const auto size = sizeSmall + allowedProgress * (sizeFinal - sizeSmall); const auto twice = twiceSmall + allowedProgress * (twiceFinal - twiceSmall); const auto leftFinal = (width() - size) / 2.; const auto leftSmall = (width() + st::mediaSponsoredCloseDiameter) / 2. - (st::mediaSponsoredCloseStroke / 2.) - sizeSmall; const auto topFinal = (height() - size) / 2.; const auto topSmall = (height() - st::mediaSponsoredCloseDiameter) / 2.; const auto left = leftSmall + allowedProgress * (leftFinal - leftSmall); const auto top = topSmall + allowedProgress * (topFinal - topSmall); auto hq = PainterHighQualityEnabler(p); pen.setWidthF(twice / 2.); p.setPen(pen); p.drawLine(QPointF(left, top), QPointF(left + size, top + size)); p.drawLine(QPointF(left + size, top), QPointF(left, top + size)); } [[nodiscard]] style::RoundButton PrepareAboutStyle() { static auto textBg = style::complex_color([] { auto result = st::mediaviewTextLinkFg->c; result.setAlphaF(result.alphaF() * 0.1); return result; }); static auto textBgOver = style::complex_color([] { auto result = st::mediaviewTextLinkFg->c; result.setAlphaF(result.alphaF() * 0.15); return result; }); static auto rippleColor = style::complex_color([] { auto result = st::mediaviewTextLinkFg->c; result.setAlphaF(result.alphaF() * 0.2); return result; }); auto result = st::mediaSponsoredAbout; result.textFg = st::mediaviewTextLinkFg; result.textFgOver = st::mediaviewTextLinkFg; result.textBg = textBg.color(); result.textBgOver = textBgOver.color(); result.ripple.color = rippleColor.color(); return result; } } // namespace class PlaybackSponsored::Message final : public Ui::RpWidget { public: Message( QWidget *parent, std::shared_ptr show, const Data::SponsoredMessage &data, rpl::producer allowCloseAt); [[nodiscard]] rpl::producer actions() const; void setFinalPosition(int x, int y); void fadeIn(); void fadeOut(Fn hidden); private: void paintEvent(QPaintEvent *e) override; void mouseMoveEvent(QMouseEvent *e) override; void mousePressEvent(QMouseEvent *e) override; void mouseReleaseEvent(QMouseEvent *e) override; int resizeGetHeight(int newWidth) override; void populate(); void startFadeIn(); void updateShown(Fn finished = nullptr); void startFade(Fn finished); const not_null _session; const std::shared_ptr _show; const Data::SponsoredMessage _data; style::RoundButton _aboutSt; std::unique_ptr _about; std::unique_ptr _close; base::unique_qptr _menu; rpl::event_stream _actions; std::shared_ptr _photo; Ui::Text::String _title; Ui::Text::String _text; QPoint _finalPosition; int _left = 0; int _top = 0; int _titleHeight = 0; int _textHeight = 0; QImage _cache; Ui::Animations::Simple _showAnimation; bool _shown = false; bool _over = false; bool _pressed = false; rpl::lifetime _photoLifetime; }; PlaybackSponsored::Message::Message( QWidget *parent, std::shared_ptr show, const Data::SponsoredMessage &data, rpl::producer allowCloseAt) : RpWidget(parent) , _session(&data.history->session()) , _show(std::move(show)) , _data(data) , _aboutSt(PrepareAboutStyle()) , _about(std::make_unique( this, tr::lng_search_sponsored_button(), _aboutSt)) , _close( std::make_unique( this, _aboutSt.ripple, std::move(allowCloseAt))) { _about->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); setMouseTracking(true); populate(); hide(); } rpl::producer PlaybackSponsored::Message::actions() const { return rpl::merge(_actions.events(), _close->actions()); } void PlaybackSponsored::Message::setFinalPosition(int x, int y) { _finalPosition = { x, y }; if (_shown) { updateShown(); } } void PlaybackSponsored::Message::fadeIn() { _shown = true; if (!_photo || _photo->loaded()) { startFadeIn(); return; } _photo->owner()->session().downloaderTaskFinished( ) | rpl::filter([=] { return _photo->loaded(); }) | rpl::start_with_next([=] { _photoLifetime.destroy(); startFadeIn(); }, _photoLifetime); } void PlaybackSponsored::Message::startFadeIn() { if (!_shown) { return; } startFade([=] { _session->sponsoredMessages().view(_data.randomId); }); show(); } void PlaybackSponsored::Message::fadeOut(Fn hidden) { if (!_shown) { if (const auto onstack = hidden) { onstack(); } return; } _shown = false; startFade(std::move(hidden)); } void PlaybackSponsored::Message::startFade(Fn finished) { _cache = Ui::GrabWidgetToImage(this); _about->hide(); _close->hide(); const auto from = _shown ? 0. : 1.; const auto till = _shown ? 1. : 0.; _showAnimation.start([=] { updateShown(finished); }, from, till, st::fadeWrapDuration); } void PlaybackSponsored::Message::updateShown(Fn finished) { const auto shown = _showAnimation.value(_shown ? 1. : 0.); const auto shift = anim::interpolate(st::mediaSponsoredShift, 0, shown); move(_finalPosition.x(), _finalPosition.y() + shift); update(); if (!_showAnimation.animating()) { _cache = QImage(); _close->show(); _about->show(); if (const auto onstack = finished) { onstack(); } } } void PlaybackSponsored::Message::paintEvent(QPaintEvent *e) { auto p = QPainter(this); const auto shown = _showAnimation.value(_shown ? 1. : 0.); if (!_cache.isNull()) { p.setOpacity(shown); p.drawImage(0, 0, _cache); return; } Ui::FillRoundRect( p, rect(), st::mediaviewSaveMsgBg, Ui::MediaviewSaveCorners); const auto &padding = st::mediaSponsoredPadding; if (_photo) { if (const auto image = _photo->image(Data::PhotoSize::Large)) { const auto size = st::mediaSponsoredThumb; const auto x = padding.left(); const auto y = (height() - size) / 2; p.drawPixmap( x, y, image->pixSingle( size, size, { .options = Images::Option::RoundCircle })); } } p.setPen(st::mediaviewControlFg); _title.draw(p, { .position = { _left, _top }, .availableWidth = _about->x() - _left, .palette = &st::mediaviewTextPalette, }); _text.draw(p, { .position = { _left, _top + _titleHeight }, .availableWidth = _close->x() - _left, .palette = &st::mediaviewTextPalette, }); } void PlaybackSponsored::Message::mouseMoveEvent(QMouseEvent *e) { const auto &padding = st::mediaSponsoredPadding; const auto point = e->pos(); const auto about = _about->geometry(); const auto close = _close->geometry(); const auto over = !about.marginsAdded(padding).contains(point) && !close.marginsAdded(padding).contains(point); if (_over != over) { _over = over; setCursor(_over ? style::cur_pointer : style::cur_default); } } void PlaybackSponsored::Message::mousePressEvent(QMouseEvent *e) { if (_over) { _pressed = true; } } void PlaybackSponsored::Message::mouseReleaseEvent(QMouseEvent *e) { if (base::take(_pressed) && _over) { _session->sponsoredMessages().clicked(_data.randomId, false, false); UrlClickHandler::Open(_data.link); } } int PlaybackSponsored::Message::resizeGetHeight(int newWidth) { const auto &padding = st::mediaSponsoredPadding; const auto userpic = st::mediaSponsoredThumb; const auto innerWidth = newWidth - _left - _close->width(); const auto titleWidth = innerWidth - _about->width() - padding.right(); _titleHeight = _title.countHeight(titleWidth); _textHeight = _text.countHeight(innerWidth); const auto use = std::max(_titleHeight + _textHeight, userpic); const auto height = padding.top() + use + padding.bottom(); _left = padding.left() + (_photo ? (userpic + padding.left()) : 0); _top = padding.top() + (use - _titleHeight - _textHeight) / 2; _about->move( _left + std::min(titleWidth, _title.maxWidth()) + padding.right(), _top); _close->move( newWidth - _close->width(), (height - _close->height()) / 2); return height; } void PlaybackSponsored::Message::populate() { const auto &from = _data.from; const auto photo = from.photoId ? _data.history->owner().photo(from.photoId).get() : nullptr; if (photo) { _photo = photo->createMediaView(); photo->load({}, LoadFromCloudOrLocal, true); } _title = Ui::Text::String( st::semiboldTextStyle, from.title, kDefaultTextOptions, st::msgMinWidth); _text = Ui::Text::String( st::defaultTextStyle, _data.textWithEntities, kMarkupTextOptions, st::msgMinWidth); _about->setClickedCallback([=] { _menu = nullptr; const auto parent = parentWidget(); _menu = base::make_unique_q( parent, st::mediaviewPopupMenu); const auto raw = _menu.get(); const auto addAction = Ui::Menu::CreateAddActionCallback(raw); Menu::FillSponsored( addAction, _show, Menu::SponsoredPhrases::Channel, _session->sponsoredMessages().lookupDetails(_data), _session->sponsoredMessages().createReportCallback( _data.randomId, crl::guard(this, [=] { _actions.fire(Action::Close); })), { .dark = true }); _actions.fire(Action::Pause); Ui::Connect(raw, &QObject::destroyed, this, [=] { _actions.fire(Action::Unpause); }); raw->popup(QCursor::pos()); }); } PlaybackSponsored::PlaybackSponsored( not_null controls, std::shared_ptr show, not_null item) : _parent(controls->parentWidget()) , _session(&item->history()->session()) , _show(std::move(show)) , _itemId(item->fullId()) , _controlsGeometry(controls->geometryValue()) , _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) { setPausedOutside(paused); } void PlaybackSponsored::updatePaused() { const auto paused = _pausedInside || _pausedOutside; if (_paused == paused) { return; } else if (_started && paused) { update(); } _paused = paused; if (!_started) { return; } else if (_paused) { _start = 0; _timer.cancel(); _allowCloseAt = 0; } else { _start = crl::now(); update(); } } void PlaybackSponsored::setPausedInside(bool paused) { if (_pausedInside == paused) { return; } _pausedInside = paused; updatePaused(); } void PlaybackSponsored::setPausedOutside(bool paused) { if (_pausedOutside == paused) { return; } _pausedOutside = paused; updatePaused(); } 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) { _allowCloseAt = now + message->durationMin; show(*message); _start = now; _timer.callOnce(duration); saveState(); } else { finish(); } } else if (_data->state.leftTillShow <= 0 && state.leftTillShow <= -duration) { hide(now); } else { if (state.leftTillShow <= 0 && duration) { _allowCloseAt = now + state.leftTillShow + message->durationMin; if (!_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, _show, data, _allowCloseAt.value()); const auto raw = _widget.get(); _controlsGeometry.value() | rpl::start_with_next([=](QRect controls) { raw->resizeToWidth(controls.width()); raw->setFinalPosition( controls.x(), controls.y() - st::mediaSponsoredSkip - raw->height()); }, raw->lifetime()); raw->actions() | rpl::start_with_next([=](Action action) { switch (action) { case Action::Close: hide(crl::now()); break; case Action::PromotePremium: showPremiumPromo(); break; case Action::Pause: setPausedInside(true); break; case Action::Unpause: setPausedInside(false); break; } }, raw->lifetime()); raw->fadeIn(); } void PlaybackSponsored::showPremiumPromo() { ShowPremiumPreviewBox(_show, PremiumFeature::NoAds); } void PlaybackSponsored::hide(crl::time now) { Expects(_widget != nullptr); _widget->fadeOut([this, raw = _widget.get()] { if (_widget.get() == raw) { _widget = nullptr; } }); ++_data->state.itemIndex; _data->state.leftTillShow = std::max( _data->betweenDelay, kStartDelayMin); _start = now; _timer.callOnce(_data->state.leftTillShow); saveState(); } 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