init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
This commit is contained in:
426
Telegram/SourceFiles/ui/chat/sponsored_message_bar.cpp
Normal file
426
Telegram/SourceFiles/ui/chat/sponsored_message_bar.cpp
Normal file
@@ -0,0 +1,426 @@
|
||||
/*
|
||||
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/application.h"
|
||||
#include "core/click_handler_types.h"
|
||||
#include "core/ui_integration.h" // TextContext
|
||||
#include "data/components/sponsored_messages.h"
|
||||
#include "data/data_session.h"
|
||||
#include "history/history_item_helpers.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_session.h"
|
||||
#include "menu/menu_sponsored.h"
|
||||
#include "ui/chat/chat_style.h"
|
||||
#include "ui/chat/chat_theme.h"
|
||||
#include "ui/dynamic_image.h"
|
||||
#include "ui/dynamic_thumbnails.h"
|
||||
#include "ui/effects/animation_value.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/image/image_prepare.h"
|
||||
#include "ui/power_saving.h"
|
||||
#include "ui/rect.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/shadow.h"
|
||||
#include "window/section_widget.h"
|
||||
#include "window/window_controller.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
#include "styles/style_dialogs.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
struct Colors final {
|
||||
QColor bg;
|
||||
QColor fg;
|
||||
};
|
||||
|
||||
using ColorFactory = Fn<Colors()>;
|
||||
|
||||
class BadgeButton final : public Ui::RippleButton {
|
||||
public:
|
||||
BadgeButton(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
tr::phrase<> text,
|
||||
ColorFactory cache)
|
||||
: Ui::RippleButton(parent, st::defaultRippleAnimation) {
|
||||
text(
|
||||
) | rpl::on_next([this](const QString &t) {
|
||||
const auto height = st::stickersHeaderBadgeFont->height;
|
||||
resize(
|
||||
st::stickersHeaderBadgeFont->width(t) + height,
|
||||
height);
|
||||
update();
|
||||
}, lifetime());
|
||||
paintRequest() | rpl::on_next([this, cache, text] {
|
||||
auto p = QPainter(this);
|
||||
const auto colors = cache();
|
||||
const auto r = rect();
|
||||
const auto rippleColor = anim::with_alpha(colors.fg, .15);
|
||||
Ui::RippleButton::paintRipple(
|
||||
p,
|
||||
QPoint(),
|
||||
&rippleColor);
|
||||
p.setBrush(colors.bg);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.drawRoundedRect(r, r.height() / 2, r.height() / 2);
|
||||
p.setFont(st::stickersHeaderBadgeFont);
|
||||
p.setPen(colors.fg);
|
||||
p.drawText(r, text(tr::now), style::al_center);
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
QImage prepareRippleMask() const override {
|
||||
return Ui::RippleAnimation::RoundRectMask(size(), height() / 2);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] Window::SessionController *FindSessionController(
|
||||
not_null<RpWidget*> widget) {
|
||||
const auto window = Core::App().findWindow(widget);
|
||||
return window ? window->sessionController() : nullptr;
|
||||
}
|
||||
|
||||
[[nodiscard]] ColorFactory GenerateReplyColorCallback(
|
||||
not_null<Window::SessionController*> controller,
|
||||
not_null<RpWidget*> widget,
|
||||
FullMsgId fullId,
|
||||
int colorIndex) {
|
||||
const auto peer = controller->session().data().peer(fullId.peer);
|
||||
struct State final {
|
||||
std::shared_ptr<Ui::ChatTheme> theme;
|
||||
};
|
||||
const auto state = widget->lifetime().make_state<State>();
|
||||
Window::ChatThemeValueFromPeer(
|
||||
controller,
|
||||
peer
|
||||
) | rpl::on_next([=](std::shared_ptr<Ui::ChatTheme> &&theme) {
|
||||
state->theme = std::move(theme);
|
||||
}, widget->lifetime());
|
||||
|
||||
return [=]() -> Colors {
|
||||
if (!state->theme) {
|
||||
return {
|
||||
anim::with_alpha(st::windowBgActive->c, .15),
|
||||
st::windowActiveTextFg->c,
|
||||
};
|
||||
}
|
||||
const auto context = controller->preparePaintContext({
|
||||
.theme = state->theme.get(),
|
||||
});
|
||||
const auto selected = false;
|
||||
const auto cache = context.st->coloredReplyCache(
|
||||
selected,
|
||||
colorIndex);
|
||||
return { cache->bg, cache->icon };
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] ColorFactory GenerateReplyColorCallback(
|
||||
not_null<RpWidget*> widget,
|
||||
FullMsgId fullId,
|
||||
int colorIndex) {
|
||||
if (const auto window = FindSessionController(widget)) {
|
||||
return GenerateReplyColorCallback(window, widget, fullId, colorIndex);
|
||||
}
|
||||
const auto window
|
||||
= widget->lifetime().make_state<Window::SessionController*>();
|
||||
const auto callback = widget->lifetime().make_state<ColorFactory>();
|
||||
return [=, color = colorIndex]() -> Colors {
|
||||
if (*callback) {
|
||||
return (*callback)();
|
||||
}
|
||||
*window = FindSessionController(widget);
|
||||
if (const auto w = (*window)) {
|
||||
*callback = GenerateReplyColorCallback(w, widget, fullId, color);
|
||||
return (*callback)();
|
||||
} else {
|
||||
return {
|
||||
anim::with_alpha(st::windowBgActive->c, .15),
|
||||
st::windowActiveTextFg->c,
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void FillSponsoredMessageBar(
|
||||
not_null<RpWidget*> container,
|
||||
not_null<Main::Session*> session,
|
||||
FullMsgId fullId,
|
||||
Data::SponsoredFrom from,
|
||||
const TextWithEntities &textWithEntities) {
|
||||
const auto widget = CreateSimpleRectButton(
|
||||
container,
|
||||
st::defaultRippleAnimationBgOver);
|
||||
widget->show();
|
||||
container->sizeValue() | rpl::on_next([=](const QSize &s) {
|
||||
widget->resize(s);
|
||||
}, widget->lifetime());
|
||||
widget->setAcceptBoth();
|
||||
|
||||
widget->addClickHandler([=](Qt::MouseButton button) {
|
||||
if (button == Qt::RightButton) {
|
||||
if (const auto controller = FindSessionController(widget)) {
|
||||
::Menu::ShowSponsored(widget, controller->uiShow(), fullId);
|
||||
}
|
||||
} else if (button == Qt::LeftButton) {
|
||||
session->sponsoredMessages().clicked(fullId, false, false);
|
||||
UrlClickHandler::Open(from.link);
|
||||
}
|
||||
});
|
||||
|
||||
struct State final {
|
||||
Ui::Text::String title;
|
||||
Ui::Text::String contentTitle;
|
||||
Ui::Text::String contentText;
|
||||
rpl::variable<int> lastPaintedContentLineAmount = 0;
|
||||
rpl::variable<int> lastPaintedContentTop = 0;
|
||||
|
||||
std::shared_ptr<Ui::DynamicImage> rightPhoto;
|
||||
QImage rightPhotoImage;
|
||||
};
|
||||
const auto state = widget->lifetime().make_state<State>();
|
||||
const auto &titleSt = st::semiboldTextStyle;
|
||||
const auto &contentTitleSt = st::semiboldTextStyle;
|
||||
const auto &contentTextSt = st::defaultTextStyle;
|
||||
state->title.setText(
|
||||
titleSt,
|
||||
from.isRecommended
|
||||
? tr::lng_recommended_message_title(tr::now)
|
||||
: tr::lng_sponsored_message_title(tr::now));
|
||||
state->contentTitle.setText(contentTitleSt, from.title);
|
||||
state->contentText.setMarkedText(
|
||||
contentTextSt,
|
||||
textWithEntities,
|
||||
kMarkupTextOptions,
|
||||
Core::TextContext({
|
||||
.session = session,
|
||||
.repaint = [=] { widget->update(); },
|
||||
}));
|
||||
const auto hostedClick = [=](ClickHandlerPtr handler) {
|
||||
return [=] {
|
||||
if (const auto controller = FindSessionController(widget)) {
|
||||
ActivateClickHandler(widget, handler, {
|
||||
.other = QVariant::fromValue(ClickHandlerContext{
|
||||
.itemId = fullId,
|
||||
.sessionWindow = base::make_weak(controller),
|
||||
.show = controller->uiShow(),
|
||||
})
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
const auto paused = [=]() -> Fn<bool()> {
|
||||
if (const auto c = FindSessionController(widget)) {
|
||||
using Gif = Window::GifPauseReason;
|
||||
return [=] { return c->isGifPausedAtLeastFor(Gif::Any); };
|
||||
}
|
||||
return [] { return false; };
|
||||
};
|
||||
const auto kLinesForPhoto = 3;
|
||||
const auto rightPhotoSize = titleSt.font->ascent * kLinesForPhoto;
|
||||
const auto rightPhotoPlaceholder = titleSt.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();
|
||||
}
|
||||
const auto rightHide = hasRightPhoto
|
||||
? nullptr
|
||||
: Ui::CreateChild<Ui::IconButton>(
|
||||
container,
|
||||
st::dialogsCancelSearchInPeer);
|
||||
if (rightHide) {
|
||||
container->sizeValue(
|
||||
) | rpl::on_next([=](const QSize &s) {
|
||||
rightHide->moveToRight(st::buttonRadius, st::lineWidth);
|
||||
}, rightHide->lifetime());
|
||||
rightHide->setClickedCallback(
|
||||
hostedClick(HideSponsoredClickHandler()));
|
||||
}
|
||||
const auto badgeButton = Ui::CreateChild<BadgeButton>(
|
||||
widget,
|
||||
from.canReport
|
||||
? tr::lng_sponsored_message_revenue_button
|
||||
: tr::lng_sponsored_top_bar_hide,
|
||||
GenerateReplyColorCallback(
|
||||
widget,
|
||||
fullId,
|
||||
from.colorIndex ? from.colorIndex : 4/*blue*/));
|
||||
badgeButton->setClickedCallback(
|
||||
hostedClick(from.canReport
|
||||
? AboutSponsoredClickHandler()
|
||||
: HideSponsoredClickHandler()));
|
||||
badgeButton->show();
|
||||
|
||||
const auto draw = [=](QPainter &p) {
|
||||
const auto r = widget->rect();
|
||||
p.fillRect(r, st::historyPinnedBg);
|
||||
widget->paintRipple(p, 0, 0);
|
||||
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)
|
||||
- (rightHide ? rightHide->width() : 0);
|
||||
const auto titleRight = leftPadding
|
||||
+ state->title.maxWidth()
|
||||
+ titleSt.font->spacew * 2;
|
||||
const auto hasSecondLineTitle = (titleRight
|
||||
> (availableWidth
|
||||
- state->contentTitle.maxWidth()
|
||||
- badgeButton->width()));
|
||||
p.setPen(st::windowActiveTextFg);
|
||||
state->title.draw(p, {
|
||||
.position = QPoint(leftPadding, topPadding),
|
||||
.outerWidth = availableWidth,
|
||||
.availableWidth = availableWidth,
|
||||
});
|
||||
badgeButton->moveToLeft(
|
||||
hasSecondLineTitle
|
||||
? titleRight
|
||||
: std::min(
|
||||
titleRight
|
||||
+ state->contentTitle.maxWidth()
|
||||
+ titleSt.font->spacew * 2,
|
||||
r.width()
|
||||
- (hasRightPhoto
|
||||
? (rightPadding + rightPhotoSize)
|
||||
: 0)
|
||||
- (rightHide ? rightHide->width() : 0)
|
||||
- rightPadding),
|
||||
topPadding
|
||||
+ (titleSt.font->height - badgeButton->height()) / 2);
|
||||
p.setPen(st::windowFg);
|
||||
{
|
||||
const auto left = hasSecondLineTitle ? leftPadding : titleRight;
|
||||
const auto top = hasSecondLineTitle
|
||||
? (topPadding + titleSt.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
|
||||
+ titleSt.font->height
|
||||
+ contentTitleSt.font->height)
|
||||
: topPadding + titleSt.font->height;
|
||||
auto lastContentLineAmount = 0;
|
||||
const auto lineHeight = contentTextSt.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 {};
|
||||
}
|
||||
line += (hasSecondLineTitle ? 2 : 1)
|
||||
+ (hasRightPhoto ? 0 : 1);
|
||||
return {
|
||||
.width = (line > kLinesForPhoto)
|
||||
? availableWidthNoPhoto
|
||||
: availableWidth,
|
||||
};
|
||||
};
|
||||
state->contentText.draw(p, {
|
||||
.position = QPoint(left, top),
|
||||
.outerWidth = availableWidth,
|
||||
.availableWidth = availableWidth,
|
||||
.geometry = Ui::Text::GeometryDescriptor{
|
||||
.layout = std::move(lineLayout),
|
||||
},
|
||||
.pausedEmoji = On(PowerSaving::kEmojiChat) || paused(),
|
||||
.pausedSpoiler = On(PowerSaving::kChatSpoiler) || paused(),
|
||||
});
|
||||
state->lastPaintedContentTop = top;
|
||||
state->lastPaintedContentLineAmount = lastContentLineAmount;
|
||||
}
|
||||
if (hasRightPhoto) {
|
||||
p.drawImage(
|
||||
r.width() - rightPadding - rightPhotoSize,
|
||||
topPadding + (rightPhotoPlaceholder - rightPhotoSize) / 2,
|
||||
state->rightPhotoImage);
|
||||
}
|
||||
};
|
||||
widget->paintRequest() | rpl::on_next([=] {
|
||||
auto p = QPainter(widget);
|
||||
draw(p);
|
||||
}, widget->lifetime());
|
||||
rpl::combine(
|
||||
state->lastPaintedContentTop.value(),
|
||||
state->lastPaintedContentLineAmount.value()
|
||||
) | rpl::distinct_until_changed() | rpl::on_next([=](
|
||||
int lastTop,
|
||||
int lastLines) {
|
||||
const auto bottomPadding = st::msgReplyPadding.top();
|
||||
const auto desiredHeight = lastTop
|
||||
+ (lastLines * contentTextSt.font->height)
|
||||
+ bottomPadding;
|
||||
const auto minHeight = hasRightPhoto
|
||||
? (rightPhotoPlaceholder + bottomPadding * 2)
|
||||
: desiredHeight;
|
||||
container->resize(
|
||||
widget->width(),
|
||||
std::clamp(
|
||||
desiredHeight,
|
||||
minHeight,
|
||||
st::sponsoredMessageBarMaxHeight));
|
||||
}, widget->lifetime());
|
||||
{ // Calculate a good size for container.
|
||||
auto dummy = QImage(1, 1, QImage::Format_ARGB32);
|
||||
auto p = QPainter(&dummy);
|
||||
draw(p);
|
||||
}
|
||||
|
||||
{
|
||||
const auto top = Ui::CreateChild<PlainShadow>(widget);
|
||||
const auto bottom = Ui::CreateChild<PlainShadow>(widget);
|
||||
widget->sizeValue() | rpl::on_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
|
||||
Reference in New Issue
Block a user