Implement unique gift view in chat.

This commit is contained in:
John Preston 2024-12-27 12:30:29 +04:00
parent 13d2f70c3a
commit 2d53ec5d34
19 changed files with 736 additions and 55 deletions

View file

@ -799,6 +799,8 @@ PRIVATE
history/view/media/history_view_story_mention.h
history/view/media/history_view_theme_document.cpp
history/view/media/history_view_theme_document.h
history/view/media/history_view_unique_gift.cpp
history/view/media/history_view_unique_gift.h
history/view/media/history_view_userpic_suggestion.cpp
history/view/media/history_view_userpic_suggestion.h
history/view/media/history_view_web_page.cpp

View file

@ -774,7 +774,7 @@ std::optional<Data::StarGift> FromTL(
.id = uint64(data.vid().v),
.stars = int64(data.vstars().v),
.starsConverted = int64(data.vconvert_stars().v),
.starsUpgraded = int64(data.vupgrade_stars().value_or_empty()),
.starsToUpgrade = int64(data.vupgrade_stars().value_or_empty()),
.document = document,
.limitedLeft = remaining.value_or_empty(),
.limitedCount = total.value_or_empty(),
@ -849,7 +849,8 @@ std::optional<Data::UserStarGift> FromTL(
}
: TextWithEntities()),
.starsConverted = int64(data.vconvert_stars().value_or_empty()),
.starsUpgraded = int64(data.vupgrade_stars().value_or_empty()),
.starsUpgradedBySender = int64(
data.vupgrade_stars().value_or_empty()),
.fromId = (data.vfrom_id()
? peerFromUser(data.vfrom_id()->v)
: PeerId()),

View file

@ -212,6 +212,7 @@ auto GenerateGiftMedia(
push(std::make_unique<MediaGenericTextPart>(
std::move(text),
margins,
st::defaultTextStyle,
links,
context));
};
@ -276,9 +277,9 @@ struct PatternPoint {
};
[[nodiscard]] const std::vector<PatternPoint> &PatternPoints() {
static const auto kSmall = 0.7;
static const auto kFaded = 0.7;
static const auto kFaded = 0.5;
static const auto kLarge = 0.85;
static const auto kOpaque = 0.9;
static const auto kOpaque = 0.7;
static const auto result = std::vector<PatternPoint>{
{ { 0.5, 0.066 }, kSmall, kFaded },
@ -1088,10 +1089,6 @@ void SendGift(
return result;
}
[[nodiscard]] QString ComputeTitle(const Data::UniqueGift &gift) {
return gift.title + u" #"_q + QString::number(gift.number);
}
void SendUpgradeRequest(
not_null<Window::SessionController*> controller,
Settings::SmallBalanceResult result,
@ -1117,7 +1114,7 @@ void SendUpgradeRequest(
.text = tr::lng_gift_upgraded_about(
tr::now,
lt_name,
Text::Bold(ComputeTitle(*gift)),
Text::Bold(Data::UniqueGiftName(*gift)),
Ui::Text::WithEntities),
});
}
@ -1159,7 +1156,7 @@ void UpgradeGift(
.text = tr::lng_gift_upgraded_about(
tr::now,
lt_name,
Text::Bold(ComputeTitle(*gift)),
Text::Bold(Data::UniqueGiftName(*gift)),
Ui::Text::WithEntities),
});
}
@ -1846,32 +1843,14 @@ void AddUniqueGiftCover(
gift.gradient = CreateGradient(cover->size(), *gift.gift);
}
p.drawImage(0, 0, gift.gradient);
const auto paintPoint = [&](const PatternPoint &point) {
const auto key = (1. + point.opacity) * 10. + point.scale;
auto &image = gift.emojis[key];
PrepareImage(image, gift.emoji.get(), point, *gift.gift);
if (!image.isNull()) {
const auto x = int(point.position.x() * width);
const auto y = int(point.position.y() * pointsHeight);
if (shown < 1.) {
p.save();
p.translate(x, y);
p.scale(shown, shown);
p.translate(-x, -y);
}
const auto size = image.size() / ratio;
p.drawImage(
x - size.width() / 2,
y - size.height() / 2,
image);
if (shown < 1.) {
p.restore();
}
}
};
for (const auto point : PatternPoints()) {
paintPoint(point);
}
PaintPoints(
p,
gift.emojis,
gift.emoji.get(),
*gift.gift,
QRect(0, 0, width, pointsHeight),
shown);
const auto lottie = gift.lottie.get();
const auto factor = style::DevicePixelRatio();
@ -2095,6 +2074,45 @@ void UpgradeBox(
}, box->lifetime());
}
void PaintPoints(
QPainter &p,
base::flat_map<float64, QImage> &cache,
not_null<Text::CustomEmoji*> emoji,
const Data::UniqueGift &gift,
const QRect &rect,
float64 shown) {
const auto origin = rect.topLeft();
const auto width = rect.width();
const auto height = rect.height();
const auto ratio = style::DevicePixelRatio();
const auto paintPoint = [&](const PatternPoint &point) {
const auto key = (1. + point.opacity) * 10. + point.scale;
auto &image = cache[key];
PrepareImage(image, emoji, point, gift);
if (!image.isNull()) {
const auto position = origin + QPoint(
int(point.position.x() * width),
int(point.position.y() * height));
if (shown < 1.) {
p.save();
p.translate(position);
p.scale(shown, shown);
p.translate(-position);
}
const auto size = image.size() / ratio;
p.drawImage(
position - QPoint(size.width() / 2, size.height() / 2),
image);
if (shown < 1.) {
p.restore();
}
}
};
for (const auto point : PatternPoints()) {
paintPoint(point);
}
}
void ShowStarGiftUpgradeBox(
not_null<Window::SessionController*> controller,
uint64 stargiftId,

View file

@ -15,6 +15,10 @@ namespace Window {
class SessionController;
} // namespace Window
namespace Ui::Text {
class CustomEmoji;
} // namespace Ui::Text
namespace Ui {
class VerticalLayout;
@ -31,6 +35,14 @@ void AddUniqueGiftCover(
rpl::producer<Data::UniqueGift> data,
rpl::producer<QString> subtitleOverride = nullptr);
void PaintPoints(
QPainter &p,
base::flat_map<float64, QImage> &cache,
not_null<Text::CustomEmoji*> emoji,
const Data::UniqueGift &gift,
const QRect &rect,
float64 shown = 1.);
void ShowStarGiftUpgradeBox(
not_null<Window::SessionController*> controller,
uint64 stargiftId,

View file

@ -77,7 +77,8 @@ struct CreditsHistoryEntry final {
int limitedCount = 0;
int limitedLeft = 0;
int starsConverted = 0;
int starsUpgraded = 0;
int starsToUpgrade = 0;
int starsUpgradedBySender = 0;
int floodSkip = 0;
bool converted : 1 = false;
bool anonymous : 1 = false;

View file

@ -34,6 +34,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/media/history_view_service_box.h"
#include "history/view/media/history_view_story_mention.h"
#include "history/view/media/history_view_premium_gift.h"
#include "history/view/media/history_view_unique_gift.h"
#include "history/view/media/history_view_userpic_suggestion.h"
#include "dialogs/ui/dialogs_message_view.h"
#include "ui/image/image.h"
@ -2374,6 +2375,16 @@ std::unique_ptr<HistoryView::Media> MediaGiftBox::createView(
not_null<HistoryView::Element*> message,
not_null<HistoryItem*> realParent,
HistoryView::Element *replacing) {
if (const auto raw = _data.unique.get()) {
return std::make_unique<HistoryView::MediaGeneric>(
message,
HistoryView::GenerateUniqueGiftMedia(message, replacing, raw),
HistoryView::MediaGenericDescriptor{
.maxWidth = st::chatIntroWidth,
.paintBg = HistoryView::UniqueGiftBg(message, raw),
.service = true,
});
}
return std::make_unique<HistoryView::ServiceBox>(
message,
std::make_unique<HistoryView::PremiumGift>(message, this));

View file

@ -132,6 +132,7 @@ enum class GiftType : uchar {
Premium, // count - months
Credits, // count - credits
StarGift, // count - stars
StarGiftUpgrade,
};
struct GiftCode {
@ -143,7 +144,8 @@ struct GiftCode {
ChannelData *channel = nullptr;
MsgId giveawayMsgId = 0;
int starsConverted = 0;
int starsUpgraded = 0;
int starsToUpgrade = 0;
int starsUpgradedBySender = 0;
int limitedCount = 0;
int limitedLeft = 0;
int count = 0;

View file

@ -46,12 +46,16 @@ struct UniqueGift {
UniqueGiftOriginalDetails originalDetails;
};
[[nodiscard]] inline QString UniqueGiftName(const UniqueGift &gift) {
return gift.title + u" #"_q + QString::number(gift.number);
}
struct StarGift {
uint64 id = 0;
std::shared_ptr<UniqueGift> unique;
int64 stars = 0;
int64 starsConverted = 0;
int64 starsUpgraded = 0;
int64 starsToUpgrade = 0;
not_null<DocumentData*> document;
int limitedLeft = 0;
int limitedCount = 0;
@ -69,7 +73,7 @@ struct UserStarGift {
StarGift info;
TextWithEntities message;
int64 starsConverted = 0;
int64 starsUpgraded = 0;
int64 starsUpgradedBySender = 0;
PeerId fromId = 0;
MsgId messageId = 0;
TimeId date = 0;

View file

@ -5657,7 +5657,8 @@ void HistoryItem::applyAction(const MTPMessageAction &action) {
}
: TextWithEntities()),
.starsConverted = int(data.vconvert_stars().value_or_empty()),
.starsUpgraded = int(data.vupgrade_stars().value_or_empty()),
.starsUpgradedBySender = int(
data.vupgrade_stars().value_or_empty()),
.type = Data::GiftType::StarGift,
.upgradable = data.is_can_upgrade(),
.anonymous = data.is_name_hidden(),
@ -5667,6 +5668,7 @@ void HistoryItem::applyAction(const MTPMessageAction &action) {
};
if (auto gift = Api::FromTL(&history()->session(), data.vgift())) {
fields.stargiftId = gift->id;
fields.starsToUpgrade = gift->starsToUpgrade;
fields.document = gift->document;
fields.limitedCount = gift->limitedCount;
fields.limitedLeft = gift->limitedLeft;
@ -5680,7 +5682,7 @@ void HistoryItem::applyAction(const MTPMessageAction &action) {
}, [&](const MTPDmessageActionStarGiftUnique &data) {
using Fields = Data::GiftCode;
auto fields = Fields{
.type = Data::GiftType::StarGift,
.type = Data::GiftType::StarGiftUpgrade,
.saved = data.is_saved(),
};
if (auto gift = Api::FromTL(&history()->session(), data.vgift())) {

View file

@ -94,6 +94,7 @@ auto GenerateChatIntro(
push(std::make_unique<MediaGenericTextPart>(
std::move(text),
margins,
st::defaultTextStyle,
links));
};
const auto title = data.customPhrases()

View file

@ -74,6 +74,7 @@ auto GenerateGiveawayStart(
push(std::make_unique<MediaGenericTextPart>(
std::move(text),
margins,
st::defaultTextStyle,
links));
};
pushText(
@ -242,6 +243,7 @@ auto GenerateGiveawayResults(
push(std::make_unique<MediaGenericTextPart>(
std::move(text),
margins,
st::defaultTextStyle,
links));
};
const auto isSingleWinner = (data->winnersCount == 1);

View file

@ -74,6 +74,7 @@ MediaGeneric::MediaGeneric(
Fn<void(Fn<void(std::unique_ptr<Part>)>)> generate,
MediaGenericDescriptor &&descriptor)
: Media(parent)
, _paintBg(std::move(descriptor.paintBg))
, _maxWidthCap(descriptor.maxWidth)
, _service(descriptor.service)
, _hideServiceText(descriptor.hideServiceText) {
@ -123,6 +124,8 @@ void MediaGeneric::draw(Painter &p, const PaintContext &context) const {
const auto outer = width();
if (outer < st::msgPadding.left() + st::msgPadding.right() + 1) {
return;
} else if (_paintBg) {
_paintBg(p, context);
} else if (_service) {
PainterHighQualityEnabler hq(p);
const auto radius = st::msgServiceGiftBoxRadius;
@ -229,12 +232,13 @@ QMargins MediaGeneric::inBubblePadding() const {
MediaGenericTextPart::MediaGenericTextPart(
TextWithEntities text,
QMargins margins,
const style::TextStyle &st,
const base::flat_map<uint16, ClickHandlerPtr> &links,
const std::any &context)
: _text(st::msgMinWidth)
, _margins(margins) {
_text.setMarkedText(
st::defaultTextStyle,
st,
text,
kMarkupTextOptions,
context);
@ -248,16 +252,13 @@ void MediaGenericTextPart::draw(
not_null<const MediaGeneric*> owner,
const PaintContext &context,
int outerWidth) const {
const auto service = owner->service();
p.setPen(service
? context.st->msgServiceFg()
: context.messageStyle()->historyTextFg);
setupPen(p, owner, context);
_text.draw(p, {
.position = { (outerWidth - width()) / 2, _margins.top() },
.outerWidth = outerWidth,
.availableWidth = width(),
.align = style::al_top,
.palette = &(service
.palette = &(owner->service()
? context.st->serviceTextPalette()
: context.messageStyle()->textPalette),
.spoiler = Ui::Text::DefaultSpoilerCache(),
@ -267,6 +268,16 @@ void MediaGenericTextPart::draw(
});
}
void MediaGenericTextPart::setupPen(
Painter &p,
not_null<const MediaGeneric*> owner,
const PaintContext &context) const {
const auto service = owner->service();
p.setPen(service
? context.st->msgServiceFg()
: context.messageStyle()->historyTextFg);
}
TextState MediaGenericTextPart::textState(
QPoint point,
StateRequest request,

View file

@ -15,6 +15,14 @@ class DynamicImage;
class RippleAnimation;
} // namespace Ui
namespace style {
struct TextStyle;
} // namespace style
namespace st {
extern const style::TextStyle &defaultTextStyle;
} // namespace st
namespace HistoryView {
class MediaGeneric;
@ -45,6 +53,7 @@ public:
struct MediaGenericDescriptor {
int maxWidth = 0;
Fn<void(Painter&, const PaintContext&)> paintBg;
ClickHandlerPtr serviceLink;
bool service = false;
bool hideServiceText = false;
@ -110,17 +119,19 @@ private:
[[nodiscard]] QMargins inBubblePadding() const;
std::vector<Entry> _entries;
Fn<void(Painter&, const PaintContext&)> _paintBg;
int _maxWidthCap = 0;
bool _service : 1 = false;
bool _hideServiceText : 1 = false;
};
class MediaGenericTextPart final : public MediaGenericPart {
class MediaGenericTextPart : public MediaGenericPart {
public:
MediaGenericTextPart(
TextWithEntities text,
QMargins margins,
const style::TextStyle &st = st::defaultTextStyle,
const base::flat_map<uint16, ClickHandlerPtr> &links = {},
const std::any &context = {});
@ -137,6 +148,12 @@ public:
QSize countOptimalSize() override;
QSize countCurrentSize(int newWidth) override;
protected:
virtual void setupPen(
Painter &p,
not_null<const MediaGeneric*> owner,
const PaintContext &context) const;
private:
Ui::Text::String _text;
QMargins _margins;

View file

@ -266,11 +266,13 @@ void PremiumGift::unloadHeavyPart() {
}
bool PremiumGift::incomingGift() const {
return gift() && !_parent->data()->out();
const auto out = _parent->data()->out();
return gift() && (starGiftUpgrade() ? out : !out);
}
bool PremiumGift::outgoingGift() const {
return gift() && _parent->data()->out();
const auto out = _parent->data()->out();
return gift() && (starGiftUpgrade() ? !out : out);
}
bool PremiumGift::gift() const {
@ -278,7 +280,12 @@ bool PremiumGift::gift() const {
}
bool PremiumGift::starGift() const {
return _data.type == Data::GiftType::StarGift;
return (_data.type == Data::GiftType::StarGift)
|| (_data.type == Data::GiftType::StarGiftUpgrade);
}
bool PremiumGift::starGiftUpgrade() const {
return (_data.type == Data::GiftType::StarGiftUpgrade);
}
bool PremiumGift::creditsPrize() const {

View file

@ -51,6 +51,7 @@ private:
[[nodiscard]] bool incomingGift() const;
[[nodiscard]] bool outgoingGift() const;
[[nodiscard]] bool starGift() const;
[[nodiscard]] bool starGiftUpgrade() const;
[[nodiscard]] bool gift() const;
[[nodiscard]] bool creditsPrize() const;
[[nodiscard]] int credits() const;

View file

@ -0,0 +1,539 @@
/*
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/media/history_view_unique_gift.h"
#include "boxes/star_gift_box.h"
#include "chat_helpers/stickers_lottie.h"
#include "core/click_handler_types.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/data_media_types.h"
#include "data/data_session.h"
#include "data/data_star_gift.h"
#include "history/view/media/history_view_media_generic.h"
#include "history/view/history_view_cursor_state.h"
#include "history/view/history_view_element.h"
#include "history/history.h"
#include "history/history_item.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "settings/settings_credits_graphics.h"
#include "ui/chat/chat_style.h"
#include "ui/effects/premium_stars_colored.h"
#include "ui/effects/ripple_animation.h"
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include "ui/painter.h"
#include "ui/power_saving.h"
#include "ui/rect.h"
#include "window/window_session_controller.h"
#include "styles/style_chat.h"
#include "styles/style_credits.h"
namespace HistoryView {
namespace {
class TextPartColored final : public MediaGenericTextPart {
public:
TextPartColored(
TextWithEntities text,
QMargins margins,
QColor color,
const style::TextStyle &st = st::defaultTextStyle,
const base::flat_map<uint16, ClickHandlerPtr> &links = {},
const std::any &context = {});
private:
void setupPen(
Painter &p,
not_null<const MediaGeneric*> owner,
const PaintContext &context) const override;
QColor _color;
};
class AttributeTable final : public MediaGenericPart {
public:
struct Entry {
QString label;
QString value;
};
AttributeTable(
std::vector<Entry> entries,
QMargins margins,
QColor labelColor);
void draw(
Painter &p,
not_null<const MediaGeneric*> owner,
const PaintContext &context,
int outerWidth) const override;
QSize countOptimalSize() override;
QSize countCurrentSize(int newWidth) override;
private:
struct Part {
Ui::Text::String label;
Ui::Text::String value;
};
std::vector<Part> _parts;
QMargins _margins;
QColor _labelColor;
int _valueLeft = 0;
};
class ButtonPart final : public MediaGenericPart {
public:
ButtonPart(
const QString &text,
QMargins margins,
QColor bg,
Fn<void()> repaint,
ClickHandlerPtr link);
void draw(
Painter &p,
not_null<const MediaGeneric*> owner,
const PaintContext &context,
int outerWidth) const override;
TextState textState(
QPoint point,
StateRequest request,
int outerWidth) const override;
void clickHandlerPressedChanged(
const ClickHandlerPtr &p,
bool pressed) override;
QSize countOptimalSize() override;
QSize countCurrentSize(int newWidth) override;
private:
Ui::Text::String _text;
QMargins _margins;
QColor _bg;
QSize _size;
ClickHandlerPtr _link;
std::unique_ptr<Ui::RippleAnimation> _ripple;
mutable Ui::Premium::ColoredMiniStars _stars;
Fn<void()> _repaint;
mutable QPoint _lastPoint;
};
ButtonPart::ButtonPart(
const QString &text,
QMargins margins,
QColor bg,
Fn<void()> repaint,
ClickHandlerPtr link)
: _text(st::semiboldTextStyle, text)
, _margins(margins)
, _bg(bg)
, _size(
(_text.maxWidth()
+ st::msgServiceGiftBoxButtonHeight
+ st::msgServiceGiftBoxButtonPadding.left()
+ st::msgServiceGiftBoxButtonPadding.right()),
st::msgServiceGiftBoxButtonHeight)
, _link(std::move(link))
, _stars([=](const QRect &) {
repaint();
}, Ui::Premium::MiniStars::Type::SlowStars)
, _repaint(std::move(repaint)) {
_stars.setColorOverride(QGradientStops{
{ 0., QColor(255, 255, 255, 255 * .3) },
{ 1., QColor(255, 255, 255) },
});
const auto padding = _size.height() / 2;
_stars.setCenter(
Rect(_size) - QMargins(padding, 0, padding, 0));
}
void ButtonPart::draw(
Painter &p,
not_null<const MediaGeneric*> owner,
const PaintContext &context,
int outerWidth) const {
PainterHighQualityEnabler hq(p);
const auto position = QPoint(
(outerWidth - width()) / 2 + _margins.left(),
_margins.top());
p.translate(position);
p.setPen(Qt::NoPen);
p.setBrush(_bg);
const auto radius = _size.height() / 2.;
const auto r = Rect(_size);
p.drawRoundedRect(r, radius, radius);
auto clipPath = QPainterPath();
clipPath.addRoundedRect(r, radius, radius);
p.setClipPath(clipPath);
_stars.paint(p);
p.setClipping(false);
auto white = QColor(255, 255, 255);
p.setPen(white);
if (_ripple) {
const auto opacity = p.opacity();
p.setOpacity(st::historyPollRippleOpacity);
white.setAlphaF(0.3);
_ripple->paint(
p,
0,
0,
width(),
&white);
p.setOpacity(opacity);
}
_text.draw(
p,
0,
(_size.height() - _text.minHeight()) / 2,
_size.width(),
style::al_top);
p.translate(-position);
}
TextState ButtonPart::textState(
QPoint point,
StateRequest request,
int outerWidth) const {
point -= QPoint{
(outerWidth - width()) / 2 + _margins.left(),
_margins.top()
};
if (QRect(QPoint(), _size).contains(point)) {
auto result = TextState();
result.link = _link;
_lastPoint = point;
return result;
}
return {};
}
void ButtonPart::clickHandlerPressedChanged(
const ClickHandlerPtr &p,
bool pressed) {
if (p != _link) {
return;
} else if (pressed) {
if (!_ripple) {
const auto radius = _size.height() / 2;
_ripple = std::make_unique<Ui::RippleAnimation>(
st::defaultRippleAnimation,
Ui::RippleAnimation::RoundRectMask(_size, radius),
_repaint);
}
_ripple->add(_lastPoint);
} else if (_ripple) {
_ripple->lastStop();
}
}
QSize ButtonPart::countOptimalSize() {
return {
_margins.left() + _size.width() + _margins.right(),
_margins.top() + _size.height() + _margins.bottom(),
};
}
QSize ButtonPart::countCurrentSize(int newWidth) {
return optimalSize();
}
TextPartColored::TextPartColored(
TextWithEntities text,
QMargins margins,
QColor color,
const style::TextStyle &st,
const base::flat_map<uint16, ClickHandlerPtr> &links,
const std::any &context)
: MediaGenericTextPart(text, margins, st, links, context)
, _color(color) {
}
void TextPartColored::setupPen(
Painter &p,
not_null<const MediaGeneric*> owner,
const PaintContext &context) const {
p.setPen(_color);
}
AttributeTable::AttributeTable(
std::vector<Entry> entries,
QMargins margins,
QColor labelColor)
: _margins(margins)
, _labelColor(labelColor) {
for (const auto &entry : entries) {
_parts.emplace_back();
auto &part = _parts.back();
part.label.setText(st::defaultTextStyle, entry.label);
part.value.setMarkedText(
st::defaultTextStyle,
Ui::Text::Bold(entry.value));
}
}
void AttributeTable::draw(
Painter &p,
not_null<const MediaGeneric*> owner,
const PaintContext &context,
int outerWidth) const {
const auto labelRight = _valueLeft - st::chatUniqueTableSkip;
const auto palette = &context.st->serviceTextPalette();
auto top = _margins.top();
const auto paint = [&](
const Ui::Text::String &text,
int left,
int availableWidth,
style::align align) {
text.draw(p, {
.position = { left, top },
.outerWidth = outerWidth,
.availableWidth = availableWidth,
.align = align,
.palette = palette,
.spoiler = Ui::Text::DefaultSpoilerCache(),
.now = context.now,
.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),
.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),
.elisionLines = 1,
});
};
const auto forLabel = labelRight - _margins.left();
const auto forValue = width() - _valueLeft - _margins.right();
const auto white = QColor(255, 255, 255);
for (const auto &part : _parts) {
p.setPen(_labelColor);
paint(part.label, _margins.left(), forLabel, style::al_topright);
p.setPen(white);
paint(part.value, _valueLeft, forValue, style::al_topleft);
top += st::normalFont->height + st::chatUniqueRowSkip;
}
}
QSize AttributeTable::countOptimalSize() {
auto maxLabel = 0;
auto maxValue = 0;
for (const auto &part : _parts) {
maxLabel = std::max(maxLabel, part.label.maxWidth());
maxValue = std::max(maxValue, part.value.maxWidth());
}
const auto skip = st::chatUniqueTableSkip;
const auto row = st::normalFont->height + st::chatUniqueRowSkip;
const auto height = int(_parts.size()) * row - st::chatUniqueRowSkip;
return {
_margins.left() + maxLabel + skip + maxValue + _margins.right(),
_margins.top() + height + _margins.bottom(),
};
}
QSize AttributeTable::countCurrentSize(int newWidth) {
const auto skip = st::chatUniqueTableSkip;
const auto width = newWidth - _margins.left() - _margins.right() - skip;
auto maxLabel = 0;
auto maxValue = 0;
for (const auto &part : _parts) {
maxLabel = std::max(maxLabel, part.label.maxWidth());
maxValue = std::max(maxValue, part.value.maxWidth());
}
if (width <= 0 || !maxLabel) {
_valueLeft = _margins.left();
} else if (!maxValue) {
_valueLeft = newWidth - _margins.right();
} else {
_valueLeft = _margins.left()
+ int((int64(maxLabel) * width) / (maxLabel + maxValue))
+ skip;
}
return { newWidth, minHeight() };
}
}; // namespace
auto GenerateUniqueGiftMedia(
not_null<Element*> parent,
Element *replacing,
not_null<Data::UniqueGift*> gift)
-> Fn<void(Fn<void(std::unique_ptr<MediaGenericPart>)>)> {
return [=](Fn<void(std::unique_ptr<MediaGenericPart>)> push) {
auto pushText = [&](
TextWithEntities text,
const style::TextStyle &st,
QColor color,
QMargins margins) {
if (text.empty()) {
return;
}
push(std::make_unique<TextPartColored>(
std::move(text),
margins,
color,
st));
};
const auto white = QColor(255, 255, 255);
const auto sticker = [=] {
using Tag = ChatHelpers::StickerLottieSize;
return StickerInBubblePart::Data{
.sticker = gift->model.document,
.size = st::chatIntroStickerSize,
.cacheTag = Tag::ChatIntroHelloSticker,
};
};
push(std::make_unique<StickerInBubblePart>(
parent,
replacing,
sticker,
st::chatUniqueStickerPadding));
pushText(
Ui::Text::Bold((!parent->data()->out()
? tr::lng_action_gift_sent_subtitle
: tr::lng_action_gift_got_subtitle)(
tr::now,
lt_user,
parent->history()->peer->shortName())),
st::chatUniqueTitle,
white,
st::chatUniqueTitlePadding);
pushText(
Ui::Text::Bold(Data::UniqueGiftName(*gift)),
st::defaultTextStyle,
gift->backdrop.textColor,
st::chatUniqueTextPadding);
const auto withButton = parent->data()->out();
auto attributes = std::vector<AttributeTable::Entry>{
{ tr::lng_gift_unique_model(tr::now), gift->model.name },
{ tr::lng_gift_unique_backdrop(tr::now), gift->backdrop.name },
{ tr::lng_gift_unique_symbol(tr::now), gift->pattern.name },
};
push(std::make_unique<AttributeTable>(
std::move(attributes),
(withButton
? st::chatUniqueTextPadding
: st::chatUniqueTableAtBottomPadding),
gift->backdrop.textColor));
if (withButton) {
const auto itemId = parent->data()->fullId();
auto link = std::make_shared<LambdaClickHandler>([=](
ClickContext context) {
const auto my = context.other.value<ClickHandlerContext>();
if (const auto controller = my.sessionWindow.get()) {
const auto owner = &controller->session().data();
if (const auto item = owner->message(itemId)) {
if (const auto media = item->media()) {
if (const auto gift = media->gift()) {
controller->show(Box(
Settings::StarGiftViewBox,
controller,
*gift,
item));
}
}
}
}
});
push(std::make_unique<ButtonPart>(
tr::lng_sticker_premium_view(tr::now),
st::chatUniqueButtonPadding,
gift->backdrop.patternColor,
[=] { parent->repaint(); },
std::move(link)));
}
};
}
Fn<void(Painter&, const Ui::ChatPaintContext &)> UniqueGiftBg(
not_null<Element*> view,
not_null<Data::UniqueGift*> gift) {
struct State {
QImage bg;
base::flat_map<float64, QImage> cache;
std::unique_ptr<Ui::Text::CustomEmoji> pattern;
};
const auto state = std::make_shared<State>();
state->pattern = view->history()->owner().customEmojiManager().create(
gift->pattern.document,
[=] { view->repaint(); },
Data::CustomEmojiSizeTag::Large);
[[maybe_unused]] const auto preload = state->pattern->ready();
return [=](Painter &p, const Ui::ChatPaintContext &context) {
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
const auto thickness = st::chatUniqueGiftBorder * 2;
auto pen = context.st->msgServiceBg()->p;
pen.setWidthF(thickness);
p.setPen(pen);
p.setBrush(Qt::transparent);
const auto radius = st::msgServiceGiftBoxRadius - thickness;
const auto media = view->media();
const auto full = QRect(0, 0, media->width(), media->height());
const auto inner = full.marginsRemoved(
{ thickness, thickness, thickness, thickness });
p.drawRoundedRect(inner, radius, radius);
auto gradient = QRadialGradient(
inner.center(),
inner.height() / 2);
gradient.setStops({
{ 0., gift->backdrop.centerColor },
{ 1., gift->backdrop.edgeColor },
});
p.setBrush(gradient);
p.setPen(Qt::NoPen);
p.drawRoundedRect(inner, radius, radius);
const auto width = media->width();
const auto shift = width / 12;
const auto doubled = width + 2 * shift;
const auto outer = QRect(-shift, -shift, doubled, doubled);
p.setClipRect(inner);
Ui::PaintPoints(p, state->cache, state->pattern.get(), *gift, outer);
p.setClipping(false);
p.save();
p.translate(inner.topLeft());
const auto tag = tr::lng_gift_limited_of_one(tr::now);
const auto font = st::semiboldFont;
p.setFont(font);
p.setPen(Qt::NoPen);
const auto twidth = font->width(tag);
const auto pos = QPoint(inner.width() - twidth, font->height);
const auto add = style::ConvertScale(2);
p.setClipRect(
-add,
-add,
inner.width() + 2 * add,
inner.height() + 2 * add);
p.translate(pos);
p.rotate(45.);
p.translate(-pos);
p.setPen(Qt::NoPen);
p.setBrush(gift->backdrop.patternColor);
p.drawRect(-5 * twidth, 0, twidth * 12, font->height);
p.setPen(gift->backdrop.textColor);
p.drawText(pos - QPoint(0, font->descent), tag);
p.restore();
};
}
} // namespace HistoryView

View file

@ -0,0 +1,36 @@
/*
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
class Painter;
namespace Data {
class MediaGiftBox;
struct UniqueGift;
} // namespace Data
namespace Ui {
struct ChatPaintContext;
} // namespace Ui
namespace HistoryView {
class Element;
class MediaGenericPart;
auto GenerateUniqueGiftMedia(
not_null<Element*> parent,
Element *replacing,
not_null<Data::UniqueGift*> gift)
-> Fn<void(Fn<void(std::unique_ptr<MediaGenericPart>)>)>;
Fn<void(Painter&, const Ui::ChatPaintContext &)> UniqueGiftBg(
not_null<Element*> view,
not_null<Data::UniqueGift*> gift);
} // namespace HistoryView

View file

@ -1342,7 +1342,7 @@ void ReceiptCreditsBox(
e.stargiftId,
starGiftSender,
itemId,
e.starsUpgraded,
e.starsUpgradedBySender ? 0 : e.starsToUpgrade,
[=](bool) { *upgradeGuard = false; });
}
};
@ -1611,7 +1611,8 @@ void UserStarGiftBox(
.limitedCount = data.info.limitedCount,
.limitedLeft = data.info.limitedLeft,
.starsConverted = int(data.info.starsConverted),
.starsUpgraded = int(data.starsUpgraded),
.starsToUpgrade = int(data.info.starsToUpgrade),
.starsUpgradedBySender = int(data.starsUpgradedBySender),
.converted = false,
.anonymous = data.anonymous,
.stargift = true,
@ -1645,7 +1646,8 @@ void StarGiftViewBox(
.limitedCount = data.limitedCount,
.limitedLeft = data.limitedLeft,
.starsConverted = data.starsConverted,
.starsUpgraded = data.starsUpgraded,
.starsToUpgrade = data.starsToUpgrade,
.starsUpgradedBySender = data.starsUpgradedBySender,
.converted = data.converted,
.anonymous = data.anonymous,
.stargift = true,

View file

@ -1205,3 +1205,15 @@ botDownloadCancel: IconButton {
rippleAreaSize: 20px;
ripple: defaultRippleAnimationBgOver;
}
chatUniqueGiftBorder: 4px;
chatUniqueStickerPadding: margins(10px, 12px, 10px, 8px);
chatUniqueTitle: TextStyle(defaultTextStyle) {
font: font(16px semibold);
}
chatUniqueTitlePadding: margins(12px, 4px, 12px, 2px);
chatUniqueTextPadding: margins(12px, 2px, 12px, 8px);
chatUniqueTableSkip: 10px;
chatUniqueTableAtBottomPadding: margins(12px, 2px, 12px, 20px);
chatUniqueRowSkip: 4px;
chatUniqueButtonPadding: margins(12px, 4px, 12px, 16px);