diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 80287bd6a..05e9e228e 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -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 diff --git a/Telegram/SourceFiles/api/api_premium.cpp b/Telegram/SourceFiles/api/api_premium.cpp index 772f46e98..3b63568e7 100644 --- a/Telegram/SourceFiles/api/api_premium.cpp +++ b/Telegram/SourceFiles/api/api_premium.cpp @@ -774,7 +774,7 @@ std::optional 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 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()), diff --git a/Telegram/SourceFiles/boxes/star_gift_box.cpp b/Telegram/SourceFiles/boxes/star_gift_box.cpp index 000c4749f..cdb2057f4 100644 --- a/Telegram/SourceFiles/boxes/star_gift_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_box.cpp @@ -212,6 +212,7 @@ auto GenerateGiftMedia( push(std::make_unique( std::move(text), margins, + st::defaultTextStyle, links, context)); }; @@ -276,9 +277,9 @@ struct PatternPoint { }; [[nodiscard]] const std::vector &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{ { { 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 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 &cache, + not_null 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 controller, uint64 stargiftId, diff --git a/Telegram/SourceFiles/boxes/star_gift_box.h b/Telegram/SourceFiles/boxes/star_gift_box.h index a3b30112b..b9b42e900 100644 --- a/Telegram/SourceFiles/boxes/star_gift_box.h +++ b/Telegram/SourceFiles/boxes/star_gift_box.h @@ -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, rpl::producer subtitleOverride = nullptr); +void PaintPoints( + QPainter &p, + base::flat_map &cache, + not_null emoji, + const Data::UniqueGift &gift, + const QRect &rect, + float64 shown = 1.); + void ShowStarGiftUpgradeBox( not_null controller, uint64 stargiftId, diff --git a/Telegram/SourceFiles/data/data_credits.h b/Telegram/SourceFiles/data/data_credits.h index ccf1a86ca..c84f55f0a 100644 --- a/Telegram/SourceFiles/data/data_credits.h +++ b/Telegram/SourceFiles/data/data_credits.h @@ -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; diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index bdcf6f95b..3b9e81f50 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -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 MediaGiftBox::createView( not_null message, not_null realParent, HistoryView::Element *replacing) { + if (const auto raw = _data.unique.get()) { + return std::make_unique( + message, + HistoryView::GenerateUniqueGiftMedia(message, replacing, raw), + HistoryView::MediaGenericDescriptor{ + .maxWidth = st::chatIntroWidth, + .paintBg = HistoryView::UniqueGiftBg(message, raw), + .service = true, + }); + } return std::make_unique( message, std::make_unique(message, this)); diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h index cd2100437..428717ca1 100644 --- a/Telegram/SourceFiles/data/data_media_types.h +++ b/Telegram/SourceFiles/data/data_media_types.h @@ -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; diff --git a/Telegram/SourceFiles/data/data_star_gift.h b/Telegram/SourceFiles/data/data_star_gift.h index 3619788a9..733b54143 100644 --- a/Telegram/SourceFiles/data/data_star_gift.h +++ b/Telegram/SourceFiles/data/data_star_gift.h @@ -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 unique; int64 stars = 0; int64 starsConverted = 0; - int64 starsUpgraded = 0; + int64 starsToUpgrade = 0; not_null 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; diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index ecd6490d6..8ba9b36c2 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -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())) { diff --git a/Telegram/SourceFiles/history/view/history_view_about_view.cpp b/Telegram/SourceFiles/history/view/history_view_about_view.cpp index 0c87a6489..61c2cd8b9 100644 --- a/Telegram/SourceFiles/history/view/history_view_about_view.cpp +++ b/Telegram/SourceFiles/history/view/history_view_about_view.cpp @@ -94,6 +94,7 @@ auto GenerateChatIntro( push(std::make_unique( std::move(text), margins, + st::defaultTextStyle, links)); }; const auto title = data.customPhrases() diff --git a/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp b/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp index 4641caa07..76733e4f7 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp @@ -74,6 +74,7 @@ auto GenerateGiveawayStart( push(std::make_unique( std::move(text), margins, + st::defaultTextStyle, links)); }; pushText( @@ -242,6 +243,7 @@ auto GenerateGiveawayResults( push(std::make_unique( std::move(text), margins, + st::defaultTextStyle, links)); }; const auto isSingleWinner = (data->winnersCount == 1); diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp b/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp index a3db7897f..0018f917b 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media_generic.cpp @@ -74,6 +74,7 @@ MediaGeneric::MediaGeneric( Fn)>)> 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 &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 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 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, diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_generic.h b/Telegram/SourceFiles/history/view/media/history_view_media_generic.h index f406e4449..a79218ae3 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_generic.h +++ b/Telegram/SourceFiles/history/view/media/history_view_media_generic.h @@ -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 paintBg; ClickHandlerPtr serviceLink; bool service = false; bool hideServiceText = false; @@ -110,17 +119,19 @@ private: [[nodiscard]] QMargins inBubblePadding() const; std::vector _entries; + Fn _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 &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 owner, + const PaintContext &context) const; + private: Ui::Text::String _text; QMargins _margins; diff --git a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp index c589d4d6a..a854b4108 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp @@ -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 { diff --git a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h index db976370e..59943bd63 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h +++ b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.h @@ -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; diff --git a/Telegram/SourceFiles/history/view/media/history_view_unique_gift.cpp b/Telegram/SourceFiles/history/view/media/history_view_unique_gift.cpp new file mode 100644 index 000000000..ab791f05a --- /dev/null +++ b/Telegram/SourceFiles/history/view/media/history_view_unique_gift.cpp @@ -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 &links = {}, + const std::any &context = {}); + +private: + void setupPen( + Painter &p, + not_null owner, + const PaintContext &context) const override; + + QColor _color; + +}; + +class AttributeTable final : public MediaGenericPart { +public: + struct Entry { + QString label; + QString value; + }; + + AttributeTable( + std::vector entries, + QMargins margins, + QColor labelColor); + + void draw( + Painter &p, + not_null 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 _parts; + QMargins _margins; + QColor _labelColor; + int _valueLeft = 0; + +}; + +class ButtonPart final : public MediaGenericPart { +public: + ButtonPart( + const QString &text, + QMargins margins, + QColor bg, + Fn repaint, + ClickHandlerPtr link); + + void draw( + Painter &p, + not_null 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 _ripple; + mutable Ui::Premium::ColoredMiniStars _stars; + Fn _repaint; + + mutable QPoint _lastPoint; + +}; + +ButtonPart::ButtonPart( + const QString &text, + QMargins margins, + QColor bg, + Fn 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 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( + 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 &links, + const std::any &context) +: MediaGenericTextPart(text, margins, st, links, context) +, _color(color) { +} + +void TextPartColored::setupPen( + Painter &p, + not_null owner, + const PaintContext &context) const { + p.setPen(_color); +} + +AttributeTable::AttributeTable( + std::vector 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 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 parent, + Element *replacing, + not_null gift) +-> Fn)>)> { + return [=](Fn)> push) { + auto pushText = [&]( + TextWithEntities text, + const style::TextStyle &st, + QColor color, + QMargins margins) { + if (text.empty()) { + return; + } + push(std::make_unique( + 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( + 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{ + { 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( + std::move(attributes), + (withButton + ? st::chatUniqueTextPadding + : st::chatUniqueTableAtBottomPadding), + gift->backdrop.textColor)); + + if (withButton) { + const auto itemId = parent->data()->fullId(); + auto link = std::make_shared([=]( + ClickContext context) { + const auto my = context.other.value(); + 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( + tr::lng_sticker_premium_view(tr::now), + st::chatUniqueButtonPadding, + gift->backdrop.patternColor, + [=] { parent->repaint(); }, + std::move(link))); + } + }; +} + +Fn UniqueGiftBg( + not_null view, + not_null gift) { + struct State { + QImage bg; + base::flat_map cache; + std::unique_ptr pattern; + }; + const auto state = std::make_shared(); + 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 diff --git a/Telegram/SourceFiles/history/view/media/history_view_unique_gift.h b/Telegram/SourceFiles/history/view/media/history_view_unique_gift.h new file mode 100644 index 000000000..aa6d661b0 --- /dev/null +++ b/Telegram/SourceFiles/history/view/media/history_view_unique_gift.h @@ -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 parent, + Element *replacing, + not_null gift) +-> Fn)>)>; + +Fn UniqueGiftBg( + not_null view, + not_null gift); + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp index 4122878c7..1a85dab97 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp @@ -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, diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 5ca540b99..ab2e51dce 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -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);