diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 1b640372d..9656eda27 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1503,6 +1503,8 @@ PRIVATE ui/chat/choose_send_as.h ui/chat/choose_theme_controller.cpp ui/chat/choose_theme_controller.h + ui/chat/sponsored_message_bar.cpp + ui/chat/sponsored_message_bar.h ui/controls/emoji_button_factory.cpp ui/controls/emoji_button_factory.h ui/controls/location_picker.cpp diff --git a/Telegram/SourceFiles/data/components/sponsored_messages.cpp b/Telegram/SourceFiles/data/components/sponsored_messages.cpp index d08cde1b4..7d89b8f20 100644 --- a/Telegram/SourceFiles/data/components/sponsored_messages.cpp +++ b/Telegram/SourceFiles/data/components/sponsored_messages.cpp @@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_element.h" #include "lang/lang_keys.h" #include "main/main_session.h" +#include "ui/chat/sponsored_message_bar.h" #include "ui/text/text_utilities.h" // Ui::Text::RichLangValue. namespace Data { @@ -260,12 +261,33 @@ void SponsoredMessages::parse( list.postsBetween = postsBetween->v; list.state = State::InjectToMiddle; } else { - list.state = State::AppendToEnd; + list.state = history->peer->isChannel() + ? State::AppendToEnd + : State::AppendToTopBar; } }, [](const MTPDmessages_sponsoredMessagesEmpty &) { }); } +void SponsoredMessages::fillTopBar( + not_null history, + not_null widget) { + const auto it = _data.find(history); + if (it == end(_data)) { + return; + } + const auto &list = it->second; + if (list.entries.empty()) { + return; + } + Ui::FillSponsoredMessageBar( + widget, + _session, + list.entries.front().itemFullId, + list.entries.front().sponsored.from, + list.entries.front().sponsored.textWithEntities); +} + void SponsoredMessages::append( not_null history, List &list, diff --git a/Telegram/SourceFiles/data/components/sponsored_messages.h b/Telegram/SourceFiles/data/components/sponsored_messages.h index 028861af9..ceb43f588 100644 --- a/Telegram/SourceFiles/data/components/sponsored_messages.h +++ b/Telegram/SourceFiles/data/components/sponsored_messages.h @@ -18,6 +18,10 @@ namespace Main { class Session; } // namespace Main +namespace Ui { +class RpWidget; +} // namespace Ui + namespace Data { class MediaPreload; @@ -76,6 +80,7 @@ public: None, AppendToEnd, InjectToMiddle, + AppendToTopBar, }; struct Details { std::vector info; @@ -98,6 +103,9 @@ public: void clearItems(not_null history); [[nodiscard]] Details lookupDetails(const FullMsgId &fullId) const; void clicked(const FullMsgId &fullId, bool isMedia, bool isFullscreen); + void fillTopBar( + not_null history, + not_null widget); [[nodiscard]] AppendResult append(not_null history); void inject( diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index a6c6450e7..32f318605 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -1611,6 +1611,9 @@ void HistoryWidget::orderWidgets() { if (_translateBar) { _translateBar->raise(); } + if (_sponsoredMessageBar) { + _sponsoredMessageBar->raise(); + } if (_pinnedBar) { _pinnedBar->raise(); } @@ -2284,6 +2287,7 @@ void HistoryWidget::showHistory( _history->showAtMsgId = _showAtMsgId; destroyUnreadBarOnClose(); + _sponsoredMessageBar = nullptr; _pinnedBar = nullptr; _translateBar = nullptr; _pinnedTracker = nullptr; @@ -2520,6 +2524,26 @@ void HistoryWidget::showHistory( session().sponsoredMessages().canHaveFor(_history)); } else if (state == State::InjectToMiddle) { injectSponsoredMessages(); + } else if (state == State::AppendToTopBar) { + _sponsoredMessageBar + = base::make_unique_q>( + this, + object_ptr(this)); + session().sponsoredMessages().fillTopBar( + _history, + _sponsoredMessageBar->entity()); + _sponsoredMessageBarHeight = 0; + _sponsoredMessageBar->heightValue( + ) | rpl::start_with_next([=](int height) { + _topDelta = _preserveScrollTop + ? 0 + : (height - _sponsoredMessageBarHeight); + _sponsoredMessageBarHeight = height; + updateHistoryGeometry(); + updateControlsGeometry(); + _topDelta = 0; + }, _sponsoredMessageBar->lifetime()); + _sponsoredMessageBar->show(anim::type::normal); } }); session().sponsoredMessages().request(_history, checkState); @@ -2931,6 +2955,9 @@ void HistoryWidget::updateControlsVisibility() { if (_pinnedBar) { _pinnedBar->show(); } + if (_sponsoredMessageBar) { + _sponsoredMessageBar->show(anim::type::instant); + } if (_translateBar) { _translateBar->show(); } @@ -4128,6 +4155,9 @@ void HistoryWidget::hideChildWidgets() { if (_pinnedBar) { _pinnedBar->hide(); } + if (_sponsoredMessageBar) { + _sponsoredMessageBar->hide(anim::type::instant); + } if (_translateBar) { _translateBar->hide(); } @@ -4414,6 +4444,9 @@ void HistoryWidget::showAnimated( if (_pinnedBar) { _pinnedBar->finishAnimating(); } + if (_sponsoredMessageBar) { + _sponsoredMessageBar->finishAnimating(); + } if (_translateBar) { _translateBar->finishAnimating(); } @@ -4452,6 +4485,9 @@ void HistoryWidget::showFinished() { if (_pinnedBar) { _pinnedBar->finishAnimating(); } + if (_sponsoredMessageBar) { + _sponsoredMessageBar->finishAnimating(); + } if (_translateBar) { _translateBar->finishAnimating(); } @@ -4484,6 +4520,9 @@ void HistoryWidget::doneShow() { if (_pinnedBar) { _pinnedBar->finishAnimating(); } + if (_sponsoredMessageBar) { + _sponsoredMessageBar->finishAnimating(); + } if (_translateBar) { _translateBar->finishAnimating(); } @@ -5999,8 +6038,14 @@ void HistoryWidget::updateControlsGeometry() { _pinnedBar->move(0, pinnedBarTop); _pinnedBar->resizeToWidth(width()); } - const auto translateTop = pinnedBarTop + const auto sponsoredMessageBarTop = pinnedBarTop + (_pinnedBar ? _pinnedBar->height() : 0); + if (_sponsoredMessageBar) { + _sponsoredMessageBar->move(0, sponsoredMessageBarTop); + _sponsoredMessageBar->resizeToWidth(width()); + } + const auto translateTop = sponsoredMessageBarTop + + (_sponsoredMessageBar ? _sponsoredMessageBar->height() : 0); if (_translateBar) { _translateBar->move(0, translateTop); _translateBar->resizeToWidth(width()); @@ -6245,6 +6290,9 @@ void HistoryWidget::updateHistoryGeometry( if (_translateBar) { newScrollHeight -= _translateBar->height(); } + if (_sponsoredMessageBar) { + newScrollHeight -= _sponsoredMessageBar->height(); + } if (_pinnedBar) { newScrollHeight -= _pinnedBar->height(); } @@ -6662,6 +6710,7 @@ int HistoryWidget::computeMaxFieldHeight() const { - _topBar->height() - (_contactStatus ? _contactStatus->bar().height() : 0) - (_businessBotStatus ? _businessBotStatus->bar().height() : 0) + - (_sponsoredMessageBar ? _sponsoredMessageBar->height() : 0) - (_pinnedBar ? _pinnedBar->height() : 0) - (_groupCallBar ? _groupCallBar->height() : 0) - (_requestsBar ? _requestsBar->height() : 0) diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index ec06c0bd6..94bf0fff9 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -73,6 +73,8 @@ class SpoilerAnimation; class ChooseThemeController; class ContinuousScroll; struct ChatPaintHighlight; +template +class SlideWrap; } // namespace Ui namespace Ui::Emoji { @@ -687,6 +689,9 @@ private: std::unique_ptr _requestsBar; int _requestsBarHeight = 0; + base::unique_qptr> _sponsoredMessageBar; + int _sponsoredMessageBarHeight = 0; + bool _preserveScrollTop = false; bool _repaintFieldScheduled = false; diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 53b2ef183..ec1f9c2f6 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -1153,3 +1153,5 @@ purchasedTagPadding: margins(3px, 2px, 6px, 2px); msgSelectionCheck: RoundCheckbox(defaultPeerListCheck) { bgActive: boxTextFgGood; } + +sponsoredMessageBarMaxHeight: 156px; diff --git a/Telegram/SourceFiles/ui/chat/sponsored_message_bar.cpp b/Telegram/SourceFiles/ui/chat/sponsored_message_bar.cpp new file mode 100644 index 000000000..483ae3852 --- /dev/null +++ b/Telegram/SourceFiles/ui/chat/sponsored_message_bar.cpp @@ -0,0 +1,203 @@ +/* +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 "ui/chat/sponsored_message_bar.h" + +#include "core/ui_integration.h" // Core::MarkedTextContext. +#include "data/components/sponsored_messages.h" +#include "data/data_session.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "ui/dynamic_image.h" +#include "ui/dynamic_thumbnails.h" +#include "ui/image/image_prepare.h" +#include "ui/rect.h" +#include "ui/rp_widget.h" +#include "ui/widgets/shadow.h" +#include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" + +namespace Ui { + +void FillSponsoredMessageBar( + not_null widget, + not_null session, + FullMsgId fullId, + Data::SponsoredFrom from, + const TextWithEntities &textWithEntities) { + struct State final { + Ui::Text::String title; + Ui::Text::String contentTitle; + Ui::Text::String contentText; + rpl::variable lastPaintedContentLineAmount = 0; + rpl::variable lastPaintedContentTop = 0; + + std::shared_ptr rightPhoto; + QImage rightPhotoImage; + }; + const auto state = widget->lifetime().make_state(); + state->title.setText( + st::semiboldTextStyle, + from.isRecommended + ? tr::lng_recommended_message_title(tr::now) + : tr::lng_sponsored_message_title(tr::now)); + state->contentTitle.setText(st::semiboldTextStyle, from.title); + state->contentText.setMarkedText( + st::defaultTextStyle, + textWithEntities, + kMarkupTextOptions, + Core::MarkedTextContext{ + .session = session, + .customEmojiRepaint = [=] { widget->update(); }, + }); + const auto kLinesForPhoto = 3; + const auto rightPhotoSize = state->title.style()->font->ascent + * kLinesForPhoto; + const auto rightPhotoPlaceholder = state->title.style()->font->height + * kLinesForPhoto; + const auto hasRightPhoto = from.photoId > 0; + if (hasRightPhoto) { + state->rightPhoto = Ui::MakePhotoThumbnail( + session->data().photo(from.photoId), + fullId); + const auto callback = [=] { + state->rightPhotoImage = Images::Round( + state->rightPhoto->image(rightPhotoSize), + ImageRoundRadius::Small); + widget->update(); + }; + state->rightPhoto->subscribeToUpdates(callback); + callback(); + } + widget->paintRequest( + ) | rpl::start_with_next([=] { + auto p = QPainter(widget); + const auto r = widget->rect(); + p.fillRect(r, st::historyPinnedBg); + const auto leftPadding = st::msgReplyBarSkip + st::msgReplyBarSkip; + const auto rightPadding = st::msgReplyBarSkip; + const auto topPadding = st::msgReplyPadding.top(); + const auto availableWidthNoPhoto = r.width() + - leftPadding + - rightPadding; + const auto availableWidth = availableWidthNoPhoto + - (hasRightPhoto ? (rightPadding + rightPhotoSize) : 0); + const auto titleRight = leftPadding + + state->title.maxWidth() + + state->title.style()->font->spacew * 2; + const auto hasSecondLineTitle + = (availableWidth - state->contentTitle.maxWidth() < titleRight); + p.setPen(st::windowActiveTextFg); + state->title.draw(p, { + .position = QPoint(leftPadding, topPadding), + .outerWidth = availableWidth, + .availableWidth = availableWidth, + }); + p.setPen(st::windowFg); + { + const auto left = hasSecondLineTitle ? leftPadding : titleRight; + const auto top = hasSecondLineTitle + ? (topPadding + state->title.style()->font->height) + : topPadding; + state->contentTitle.draw(p, { + .position = QPoint(left, top), + .outerWidth = hasSecondLineTitle + ? availableWidth + : (availableWidth - titleRight), + .availableWidth = availableWidth, + .elisionLines = 1, + }); + } + { + const auto left = leftPadding; + const auto top = hasSecondLineTitle + ? (topPadding + + state->title.style()->font->height + + state->contentTitle.style()->font->height) + : topPadding + state->title.style()->font->height; + auto lastContentLineAmount = 0; + const auto lineHeight = state->contentText.style()->font->height; + const auto lineLayout = [&](int line) -> Ui::Text::LineGeometry { + line++; + lastContentLineAmount = line; + const auto diff = (st::sponsoredMessageBarMaxHeight) + - line * lineHeight; + if (diff < 3 * lineHeight) { + return { + .width = availableWidthNoPhoto, + .elided = true, + }; + } else if (diff < 2 * lineHeight) { + return {}; + } + if (hasRightPhoto) { + line += (hasSecondLineTitle ? 2 : 1); + return { + .width = (line > kLinesForPhoto) + ? availableWidthNoPhoto + : availableWidth, + }; + } else { + return { .width = availableWidth }; + } + }; + state->contentText.draw(p, { + .position = QPoint(left, top), + .outerWidth = availableWidth, + .availableWidth = availableWidth, + .geometry = Ui::Text::GeometryDescriptor{ + .layout = std::move(lineLayout), + }, + }); + state->lastPaintedContentTop = top; + state->lastPaintedContentLineAmount = lastContentLineAmount; + } + if (hasRightPhoto) { + p.drawImage( + r.width() - rightPadding - rightPhotoSize, + topPadding + (rightPhotoPlaceholder - rightPhotoSize) / 2, + state->rightPhotoImage); + } + }, widget->lifetime()); + rpl::combine( + state->lastPaintedContentTop.value(), + state->lastPaintedContentLineAmount.value() + ) | rpl::distinct_until_changed() | rpl::start_with_next([=]( + int lastTop, + int lastLines) { + const auto bottomPadding = st::msgReplyPadding.top(); + const auto desiredHeight = lastTop + + (lastLines * state->contentText.style()->font->height) + + bottomPadding; + const auto minHeight = hasRightPhoto + ? (rightPhotoPlaceholder + bottomPadding * 2) + : desiredHeight; + widget->resize( + widget->width(), + std::clamp( + desiredHeight, + minHeight, + st::sponsoredMessageBarMaxHeight)); + }, widget->lifetime()); + widget->resize(widget->width(), 1); + + { + const auto top = Ui::CreateChild(widget); + const auto bottom = Ui::CreateChild(widget); + widget->sizeValue() | rpl::start_with_next([=] (const QSize &s) { + top->show(); + top->raise(); + top->resizeToWidth(s.width()); + bottom->show(); + bottom->raise(); + bottom->resizeToWidth(s.width()); + bottom->moveToLeft(0, s.height() - bottom->height()); + }, top->lifetime()); + } +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/chat/sponsored_message_bar.h b/Telegram/SourceFiles/ui/chat/sponsored_message_bar.h new file mode 100644 index 000000000..d18ec9b05 --- /dev/null +++ b/Telegram/SourceFiles/ui/chat/sponsored_message_bar.h @@ -0,0 +1,29 @@ +/* +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 + +namespace Data { +struct SponsoredFrom; +} // namespace Data + +namespace Main { +class Session; +} // namespace Main + +namespace Ui { + +class RpWidget; + +void FillSponsoredMessageBar( + not_null widget, + not_null session, + FullMsgId fullId, + Data::SponsoredFrom from, + const TextWithEntities &textWithEntities); + +} // namespace Ui