diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 0474f04b7..489d00e14 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -949,6 +949,8 @@ PRIVATE info/media/info_media_widget.h info/members/info_members_widget.cpp info/members/info_members_widget.h + info/peer_gifts/info_peer_gifts_common.cpp + info/peer_gifts/info_peer_gifts_common.h info/peer_gifts/info_peer_gifts_widget.cpp info/peer_gifts/info_peer_gifts_widget.h info/polls/info_polls_results_inner_widget.cpp diff --git a/Telegram/SourceFiles/api/api_premium.cpp b/Telegram/SourceFiles/api/api_premium.cpp index 81b1d685b..28f467fe6 100644 --- a/Telegram/SourceFiles/api/api_premium.cpp +++ b/Telegram/SourceFiles/api/api_premium.cpp @@ -600,23 +600,12 @@ auto PremiumGiftCodeOptions::requestStarGifts() result.match([&](const MTPDpayments_starGifts &data) { _giftsHash = data.vhash().v; const auto &list = data.vgifts().v; + const auto session = &_peer->session(); auto gifts = std::vector(); gifts.reserve(list.size()); for (const auto &gift : list) { - const auto &data = gift.data(); - const auto document = _peer->owner().processDocument( - data.vsticker()); - const auto remaining = data.vavailability_remains(); - const auto total = data.vavailability_total(); - if (document->sticker()) { - gifts.push_back(StarGift{ - .id = uint64(data.vid().v), - .stars = int64(data.vstars().v), - .convertStars = int64(data.vconvert_stars().v), - .document = document, - .limitedLeft = remaining.value_or_empty(), - .limitedCount = total.value_or_empty(), - }); + if (auto parsed = FromTL(session, gift)) { + gifts.push_back(std::move(*parsed)); } } _gifts = std::move(gifts); @@ -769,4 +758,53 @@ rpl::producer RandomHelloStickerValue( }) | rpl::take(1) | rpl::map(random)); } +std::optional FromTL( + not_null session, + const MTPstarGift &gift) { + const auto &data = gift.data(); + const auto document = session->data().processDocument( + data.vsticker()); + const auto remaining = data.vavailability_remains(); + const auto total = data.vavailability_total(); + if (!document->sticker()) { + return {}; + } + return StarGift{ + .id = uint64(data.vid().v), + .stars = int64(data.vstars().v), + .convertStars = int64(data.vconvert_stars().v), + .document = document, + .limitedLeft = remaining.value_or_empty(), + .limitedCount = total.value_or_empty(), + }; +} + +std::optional FromTL( + not_null session, + const MTPuserStarGift &gift) { + const auto &data = gift.data(); + auto parsed = FromTL(session, data.vgift()); + if (!parsed) { + return {}; + } + return UserStarGift{ + .gift = std::move(*parsed), + .message = (data.vmessage() + ? TextWithEntities{ + .text = qs(data.vmessage()->data().vtext()), + .entities = Api::EntitiesFromMTP( + session, + data.vmessage()->data().ventities().v), + } + : TextWithEntities()), + .convertStars = int64(data.vconvert_stars().value_or_empty()), + .fromId = (data.vfrom_id() + ? peerFromUser(data.vfrom_id()->v) + : PeerId()), + .messageId = data.vmsg_id().value_or_empty(), + .anonymous = data.is_name_hidden(), + .hidden = data.is_unsaved(), + }; +} + } // namespace Api diff --git a/Telegram/SourceFiles/api/api_premium.h b/Telegram/SourceFiles/api/api_premium.h index 0399a04ca..97d8c5013 100644 --- a/Telegram/SourceFiles/api/api_premium.h +++ b/Telegram/SourceFiles/api/api_premium.h @@ -82,6 +82,16 @@ struct StarGift { int limitedCount = 0; }; +struct UserStarGift { + StarGift gift; + TextWithEntities message; + int64 convertStars = 0; + PeerId fromId = 0; + MsgId messageId = 0; + bool anonymous = false; + bool hidden = false; +}; + class Premium final { public: explicit Premium(not_null api); @@ -264,4 +274,11 @@ enum class RequirePremiumState { [[nodiscard]] rpl::producer RandomHelloStickerValue( not_null session); +[[nodiscard]] std::optional FromTL( + not_null session, + const MTPstarGift &gift); +[[nodiscard]] std::optional FromTL( + not_null session, + const MTPuserStarGift &gift); + } // namespace Api diff --git a/Telegram/SourceFiles/boxes/gift_credits_box.cpp b/Telegram/SourceFiles/boxes/gift_credits_box.cpp index 976b2883a..d93749314 100644 --- a/Telegram/SourceFiles/boxes/gift_credits_box.cpp +++ b/Telegram/SourceFiles/boxes/gift_credits_box.cpp @@ -6,34 +6,33 @@ For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "boxes/gift_credits_box.h" - +// #include "base/random.h" -#include "api/api_credits.h" #include "api/api_premium.h" #include "boxes/peer_list_controllers.h" #include "boxes/send_credits_box.h" #include "chat_helpers/stickers_gift_box_pack.h" #include "chat_helpers/stickers_lottie.h" -#include "core/click_handler_types.h" -#include "core/local_url_handlers.h" -#include "data/data_peer.h" +#include "core/ui_integration.h" +#include "data/data_document.h" +#include "data/data_document_media.h" #include "data/data_session.h" #include "data/data_user.h" #include "data/stickers/data_custom_emoji.h" #include "history/admin_log/history_admin_log_item.h" -#include "history/view/media/history_view_sticker_player.h" #include "history/view/media/history_view_media_generic.h" #include "history/view/history_view_element.h" #include "history/history.h" #include "history/history_item.h" #include "history/history_item_helpers.h" +#include "info/peer_gifts/info_peer_gifts_common.h" #include "lang/lang_keys.h" -#include "main/session/session_show.h" +#include "lottie/lottie_common.h" +#include "lottie/lottie_single_player.h" #include "main/main_session.h" #include "payments/payments_form.h" #include "payments/payments_checkout_process.h" #include "payments/payments_non_panel_process.h" -#include "settings/settings_credits_graphics.h" #include "settings/settings_credits.h" #include "settings/settings_premium.h" #include "ui/chat/chat_style.h" @@ -42,9 +41,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/path_shift_gradient.h" #include "ui/effects/premium_graphics.h" #include "ui/effects/premium_stars_colored.h" -#include "ui/effects/ripple_animation.h" #include "ui/layers/generic_box.h" -#include "ui/basic_click_handlers.h" #include "ui/painter.h" #include "ui/rect.h" #include "ui/text/format_values.h" @@ -52,71 +49,33 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/toast/toast.h" #include "ui/vertical_list.h" #include "ui/widgets/fields/input_field.h" -#include "ui/widgets/label_with_custom_emoji.h" -#include "ui/widgets/shadow.h" +#include "ui/widgets/buttons.h" #include "window/themes/window_theme.h" #include "window/section_widget.h" #include "window/window_session_controller.h" #include "styles/style_boxes.h" -#include "styles/style_channel_earn.h" #include "styles/style_chat.h" #include "styles/style_credits.h" -#include "styles/style_giveaway.h" #include "styles/style_layers.h" #include "styles/style_premium.h" #include "styles/style_settings.h" -#include "data/stickers/data_stickers.h" -#include "data/data_document.h" -#include "data/data_document_media.h" - namespace Ui { namespace { constexpr auto kPriceTabAll = 0; constexpr auto kPriceTabLimited = -1; -constexpr auto kGiftsPerRow = 3; constexpr auto kGiftMessageLimit = 256; constexpr auto kSentToastDuration = 3 * crl::time(1000); using namespace HistoryView; - -struct GiftTypePremium { - int64 cost = 0; - QString currency; - int months = 0; - int discountPercent = 0; - - [[nodiscard]] friend inline bool operator==( - const GiftTypePremium &, - const GiftTypePremium &) = default; -}; +using namespace Info::PeerGifts; struct PremiumGiftsDescriptor { std::vector list; std::shared_ptr api; }; -struct GiftTypeStars { - uint64 id = 0; - int64 stars = 0; - int64 convertStars = 0; - DocumentData *document = nullptr; - bool limited = false; - - [[nodiscard]] friend inline bool operator==( - const GiftTypeStars&, - const GiftTypeStars&) = default; -}; - -struct GiftDescriptor : std::variant { - using variant::variant; - - [[nodiscard]] friend inline bool operator==( - const GiftDescriptor&, - const GiftDescriptor&) = default; -}; - struct GiftsDescriptor { std::vector list; std::shared_ptr api; @@ -739,234 +698,6 @@ struct GiftPriceTabs { }; } -class GiftButtonDelegate { -public: - [[nodiscard]] virtual TextWithEntities star() = 0; - [[nodiscard]] virtual std::any textContext() = 0; - [[nodiscard]] virtual QSize buttonSize() = 0; - [[nodiscard]] virtual QImage background() = 0; - [[nodiscard]] virtual DocumentData *lookupSticker( - const GiftDescriptor &descriptor) = 0; -}; - -class GiftButton final : public AbstractButton { -public: - GiftButton(QWidget *parent, not_null delegate); - using AbstractButton::AbstractButton; - - void setDescriptor(const GiftDescriptor &descriptor); - void setGeometry(QRect inner, QMargins extend); - -private: - void paintEvent(QPaintEvent *e) override; - - void setDocument(not_null document); - - const not_null _delegate; - GiftDescriptor _descriptor; - Text::String _text; - Text::String _price; - QRect _button; - QMargins _extend; - - std::unique_ptr _player; - rpl::lifetime _mediaLifetime; - -}; - -GiftButton::GiftButton( - QWidget *parent, - not_null delegate) -: AbstractButton(parent) -, _delegate(delegate) { -} - -void GiftButton::setDescriptor(const GiftDescriptor &descriptor) { - if (_descriptor == descriptor) { - return; - } - auto player = base::take(_player); - _mediaLifetime.destroy(); - _descriptor = descriptor; - v::match(descriptor, [&](const GiftTypePremium &data) { - const auto months = data.months; - const auto years = (months % 12) ? 0 : months / 12; - _text = Text::String(st::giftBoxGiftHeight / 4); - _text.setMarkedText( - st::defaultTextStyle, - Text::Bold(years - ? tr::lng_years(tr::now, lt_count, years) - : tr::lng_months(tr::now, lt_count, months) - ).append('\n').append( - tr::lng_gift_premium_label(tr::now) - )); - _price.setText( - st::semiboldTextStyle, - FillAmountAndCurrency( - data.cost, - data.currency, - true)); - }, [&](const GiftTypeStars &data) { - _price.setMarkedText( - st::semiboldTextStyle, - _delegate->star().append(QString::number(data.stars)), - kMarkupTextOptions, - _delegate->textContext()); - }); - if (const auto document = _delegate->lookupSticker(descriptor)) { - setDocument(document); - } - - const auto buttonw = _price.maxWidth(); - const auto buttonh = st::semiboldFont->height; - const auto inner = QRect( - QPoint(), - QSize(buttonw, buttonh) - ).marginsAdded(st::giftBoxButtonPadding); - const auto single = _delegate->buttonSize(); - const auto skipx = (single.width() - inner.width()) / 2; - const auto skipy = single.height() - - st::giftBoxButtonBottom - - inner.height(); - const auto outer = (single.width() - 2 * skipx); - _button = QRect(skipx, skipy, outer, inner.height()); -} - -void GiftButton::setDocument(not_null document) { - const auto media = document->createMediaView(); - media->checkStickerLarge(); - media->goodThumbnailWanted(); - - rpl::single() | rpl::then( - document->owner().session().downloaderTaskFinished() - ) | rpl::filter([=] { - return media->loaded(); - }) | rpl::start_with_next([=] { - _mediaLifetime.destroy(); - - auto result = std::unique_ptr(); - const auto sticker = document->sticker(); - if (sticker->isLottie()) { - result = std::make_unique( - ChatHelpers::LottiePlayerFromDocument( - media.get(), - ChatHelpers::StickerLottieSize::InlineResults, - st::giftBoxStickerSize, - Lottie::Quality::High)); - } else if (sticker->isWebm()) { - result = std::make_unique( - media->owner()->location(), - media->bytes(), - st::giftBoxStickerSize); - } else { - result = std::make_unique( - media->owner()->location(), - media->bytes(), - st::giftBoxStickerSize); - } - result->setRepaintCallback([=] { update(); }); - _player = std::move(result); - update(); - }, _mediaLifetime); -} - -void GiftButton::setGeometry(QRect inner, QMargins extend) { - _extend = extend; - AbstractButton::setGeometry(inner.marginsAdded(extend)); -} - -void GiftButton::paintEvent(QPaintEvent *e) { - auto p = QPainter(this); - const auto position = QPoint(_extend.left(), _extend.top()); - p.drawImage(0, 0, _delegate->background()); - - if (_player && _player->ready()) { - const auto paused = !isOver(); - auto info = _player->frame( - st::giftBoxStickerSize, - QColor(0, 0, 0, 0), - false, - crl::now(), - paused); - const auto finished = (info.index + 1 == _player->framesCount()); - if (!finished || !paused) { - _player->markFrameShown(); - } - const auto size = info.image.size() / style::DevicePixelRatio(); - p.drawImage( - QRect( - (width() - size.width()) / 2, - (_text.isEmpty() - ? st::giftBoxStickerStarTop - : st::giftBoxStickerTop), - size.width(), - size.height()), - info.image); - } - - auto hq = PainterHighQualityEnabler(p); - const auto premium = v::is(_descriptor); - const auto singlew = _delegate->buttonSize().width(); - const auto font = st::semiboldFont; - p.setFont(font); - const auto text = v::match(_descriptor, [&](GiftTypePremium data) { - if (data.discountPercent > 0) { - p.setBrush(st::attentionBoxButton.textFg); - const auto kMinus = QChar(0x2212); - return kMinus + QString::number(data.discountPercent) + '%'; - } - return QString(); - }, [&](const GiftTypeStars &data) { - if (data.limited) { - p.setBrush(st::windowActiveTextFg); - return tr::lng_gift_stars_limited(tr::now); - } - return QString(); - }); - if (!text.isEmpty()) { - p.setPen(Qt::NoPen); - const auto twidth = font->width(text); - const auto pos = position + QPoint(singlew - twidth, font->height); - p.save(); - p.translate(pos); - p.rotate(45.); - p.translate(-pos); - p.drawRect(-5 * twidth, position.y(), twidth * 12, font->height); - p.setPen(st::windowBg); - p.drawText(pos - QPoint(0, font->descent), text); - p.restore(); - } - p.setBrush(premium ? st::lightButtonBgOver : st::creditsBg3); - p.setPen(Qt::NoPen); - if (!premium) { - p.setOpacity(0.12); - } - const auto geometry = _button.translated(position); - const auto radius = geometry.height() / 2.; - p.drawRoundedRect(geometry, radius, radius); - if (!premium) { - p.setOpacity(1.); - } - - if (!_text.isEmpty()) { - p.setPen(st::windowFg); - _text.draw(p, { - .position = (position - + QPoint(0, st::giftBoxPremiumTextTop)), - .availableWidth = singlew, - .align = style::al_top, - }); - } - - const auto padding = st::giftBoxButtonPadding; - p.setPen(premium ? st::windowActiveTextFg : st::creditsFg); - _price.draw(p, { - .position = (geometry.topLeft() - + QPoint(padding.left(), padding.top())), - .availableWidth = _price.maxWidth(), - }); -} - [[nodiscard]] not_null AddPartInput( not_null container, rpl::producer placeholder, @@ -1136,122 +867,25 @@ void SendPremiumGift( auto result = object_ptr((QWidget*)nullptr); const auto raw = result.data(); - class Delegate final : public GiftButtonDelegate { - public: - Delegate( - not_null window, - not_null peer) - : _window(window) - , _peer(peer) { - } - - TextWithEntities star() override { - return _peer->owner().customEmojiManager().creditsEmoji(); - } - std::any textContext() override { - return Core::MarkedTextContext{ - .session = &_peer->session(), - .customEmojiRepaint = [] {}, - }; - } - QSize buttonSize() override { - if (!_single.isEmpty()) { - return _single; - } - const auto width = st::boxWideWidth; - const auto padding = st::giftBoxPadding; - const auto available = width - padding.left() - padding.right(); - const auto singlew = (available - 2 * st::giftBoxGiftSkip.x()) - / kGiftsPerRow; - _single = QSize(singlew, st::giftBoxGiftHeight); - return _single; - } - void setBackground(QImage bg) { - _bg = std::move(bg); - } - QImage background() override { - return _bg; - } - DocumentData *lookupSticker( - const GiftDescriptor &descriptor) override { - const auto &session = _window->session(); - auto &packs = session.giftBoxStickersPacks(); - packs.load(); - return v::match(descriptor, [&](GiftTypePremium data) { - return packs.lookup(data.months); - }, [&](GiftTypeStars data) { - return data.document - ? data.document - : packs.lookup(packs.monthsForStars(data.stars)); - }); - } - - private: - const not_null _window; - const not_null _peer; - QSize _single; - QImage _bg; - - }; - struct State { Delegate delegate; std::vector> buttons; bool sending = false; }; const auto state = raw->lifetime().make_state(State{ - .delegate = Delegate(window, peer), + .delegate = Delegate(window), }); const auto single = state->delegate.buttonSize(); const auto shadow = st::defaultDropdownMenu.wrap.shadow; const auto extend = shadow.extend; - const auto bgSize = QRect(QPoint(), single ).marginsAdded(extend).size(); - const auto ratio = style::DevicePixelRatio(); - auto bg = QImage( - bgSize * ratio, - QImage::Format_ARGB32_Premultiplied); - bg.setDevicePixelRatio(ratio); - bg.fill(Qt::transparent); - - const auto radius = st::giftBoxGiftRadius; - const auto rect = QRect(QPoint(), bgSize).marginsRemoved(extend); - - { - auto p = QPainter(&bg); - auto hq = PainterHighQualityEnabler(p); - p.setOpacity(0.3); - p.setPen(Qt::NoPen); - p.setBrush(st::windowShadowFg); - p.drawRoundedRect( - QRectF(rect).translated(0, radius / 12.), - radius, - radius); - } - bg = bg.scaled( - (bgSize * ratio) / 2, - Qt::IgnoreAspectRatio, - Qt::SmoothTransformation); - bg = Images::Blur(std::move(bg), true); - bg = bg.scaled( - bgSize * ratio, - Qt::IgnoreAspectRatio, - Qt::SmoothTransformation); - { - auto p = QPainter(&bg); - auto hq = PainterHighQualityEnabler(p); - p.setPen(Qt::NoPen); - p.setBrush(st::windowBg); - p.drawRoundedRect(rect, radius, radius); - } - - state->delegate.setBackground(std::move(bg)); std::move( gifts ) | rpl::start_with_next([=](const GiftsDescriptor &gifts) { const auto width = st::boxWideWidth; const auto padding = st::giftBoxPadding; const auto available = width - padding.left() - padding.right(); + const auto perRow = available / single.width(); auto x = padding.left(); auto y = padding.top(); @@ -1268,7 +902,7 @@ void SendPremiumGift( const auto &descriptor = gifts.list[i]; button->setDescriptor(descriptor); - const auto last = !((i + 1) % kGiftsPerRow); + const auto last = !((i + 1) % perRow); if (last) { x = padding.left() + available - single.width(); } @@ -1306,7 +940,7 @@ void SendPremiumGift( } }); } - if (gifts.list.size() % kGiftsPerRow) { + if (gifts.list.size() % perRow) { y += padding.bottom() + single.height(); } else { y += padding.bottom() - st::giftBoxGiftSkip.y(); @@ -1368,7 +1002,6 @@ void AddBlock( auto state = std::make_unique(); state->gifts = GiftsPremium(&window->session(), peer); - ; auto result = MakeGiftsList(window, peer, state->gifts.value( ) | rpl::map([=](const PremiumGiftsDescriptor &gifts) { diff --git a/Telegram/SourceFiles/history/view/history_view_contact_status.cpp b/Telegram/SourceFiles/history/view/history_view_contact_status.cpp index 524719f92..a764199da 100644 --- a/Telegram/SourceFiles/history/view/history_view_contact_status.cpp +++ b/Telegram/SourceFiles/history/view/history_view_contact_status.cpp @@ -983,19 +983,26 @@ int BusinessBotStatus::Bar::resizeGetHeight(int newWidth) { const auto &st = st::defaultPeerList.item; _settings->moveToRight(0, 0, newWidth); if (_userpic) { - _userpic->moveToLeft(st.photoPosition.x(), st.photoPosition.y()); + _userpic->moveToLeft( + st.photoPosition.x(), + st.photoPosition.y(), + newWidth); } auto available = newWidth - _settings->width() - st.namePosition.x(); if (!_togglePaused->isHidden()) { _togglePaused->moveToRight( _settings->width(), - (st.height - _togglePaused->height()) / 2); + (st.height - _togglePaused->height()) / 2, + newWidth); available -= _togglePaused->width(); } _name->resizeToWidth(available); - _name->moveToLeft(st.namePosition.x(), st.namePosition.y()); + _name->moveToLeft(st.namePosition.x(), st.namePosition.y(), newWidth); _status->resizeToWidth(available); - _status->moveToLeft(st.statusPosition.x(), st.statusPosition.y()); + _status->moveToLeft( + st.statusPosition.x(), + st.statusPosition.y(), + newWidth); return st.height; } diff --git a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp new file mode 100644 index 000000000..c15f55b31 --- /dev/null +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp @@ -0,0 +1,352 @@ +/* +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 "info/peer_gifts/info_peer_gifts_common.h" + +#include "chat_helpers/stickers_gift_box_pack.h" +#include "chat_helpers/stickers_lottie.h" +#include "core/ui_integration.h" +#include "data/stickers/data_custom_emoji.h" +#include "data/data_document.h" +#include "data/data_document_media.h" +#include "data/data_session.h" +#include "history/view/media/history_view_sticker_player.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "ui/text/format_values.h" +#include "ui/text/text_utilities.h" +#include "ui/painter.h" +#include "window/window_session_controller.h" +#include "styles/style_credits.h" +#include "styles/style_layers.h" +#include "styles/style_premium.h" + +namespace Info::PeerGifts { +namespace { + +constexpr auto kGiftsPerRow = 3; + +} // namespace + +GiftButton::GiftButton( + QWidget *parent, + not_null delegate) +: AbstractButton(parent) +, _delegate(delegate) { +} + +GiftButton::~GiftButton() = default; + +void GiftButton::setDescriptor(const GiftDescriptor &descriptor) { + if (_descriptor == descriptor) { + return; + } + auto player = base::take(_player); + _mediaLifetime.destroy(); + _descriptor = descriptor; + v::match(descriptor, [&](const GiftTypePremium &data) { + const auto months = data.months; + const auto years = (months % 12) ? 0 : months / 12; + _text = Ui::Text::String(st::giftBoxGiftHeight / 4); + _text.setMarkedText( + st::defaultTextStyle, + Ui::Text::Bold(years + ? tr::lng_years(tr::now, lt_count, years) + : tr::lng_months(tr::now, lt_count, months) + ).append('\n').append( + tr::lng_gift_premium_label(tr::now) + )); + _price.setText( + st::semiboldTextStyle, + Ui::FillAmountAndCurrency( + data.cost, + data.currency, + true)); + }, [&](const GiftTypeStars &data) { + _price.setMarkedText( + st::semiboldTextStyle, + _delegate->star().append(QString::number(data.stars)), + kMarkupTextOptions, + _delegate->textContext()); + }); + if (const auto document = _delegate->lookupSticker(descriptor)) { + setDocument(document); + } + + const auto buttonw = _price.maxWidth(); + const auto buttonh = st::semiboldFont->height; + const auto inner = QRect( + QPoint(), + QSize(buttonw, buttonh) + ).marginsAdded(st::giftBoxButtonPadding); + const auto skipy = _delegate->buttonSize().height() + - st::giftBoxButtonBottom + - inner.height(); + const auto skipx = (width() - inner.width()) / 2; + const auto outer = (width() - 2 * skipx); + _button = QRect(skipx, skipy, outer, inner.height()); +} + +void GiftButton::setDocument(not_null document) { + const auto media = document->createMediaView(); + media->checkStickerLarge(); + media->goodThumbnailWanted(); + + rpl::single() | rpl::then( + document->owner().session().downloaderTaskFinished() + ) | rpl::filter([=] { + return media->loaded(); + }) | rpl::start_with_next([=] { + _mediaLifetime.destroy(); + + auto result = std::unique_ptr(); + const auto sticker = document->sticker(); + if (sticker->isLottie()) { + result = std::make_unique( + ChatHelpers::LottiePlayerFromDocument( + media.get(), + ChatHelpers::StickerLottieSize::InlineResults, + st::giftBoxStickerSize, + Lottie::Quality::High)); + } else if (sticker->isWebm()) { + result = std::make_unique( + media->owner()->location(), + media->bytes(), + st::giftBoxStickerSize); + } else { + result = std::make_unique( + media->owner()->location(), + media->bytes(), + st::giftBoxStickerSize); + } + result->setRepaintCallback([=] { update(); }); + _player = std::move(result); + update(); + }, _mediaLifetime); +} + +void GiftButton::setGeometry(QRect inner, QMargins extend) { + _extend = extend; + AbstractButton::setGeometry(inner.marginsAdded(extend)); +} + +void GiftButton::resizeEvent(QResizeEvent *e) { + if (!_button.isEmpty()) { + _button.moveLeft((width() - _button.width()) / 2); + } +} + +void GiftButton::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + const auto position = QPoint(_extend.left(), _extend.top()); + const auto background = _delegate->background(); + const auto dpr = int(background.devicePixelRatio()); + const auto width = this->width(); + if (width * dpr <= background.width()) { + p.drawImage(0, 0, background); + } else { + const auto full = background.width(); + const auto half = ((full / 2) / dpr) * dpr; + const auto height = background.height(); + p.drawImage( + QRect(0, 0, half / dpr, height / dpr), + background, + QRect(0, 0, half, height)); + p.drawImage( + QRect(width - (half / dpr), 0, half / dpr, height / dpr), + background, + QRect(full - half, 0, half, height)); + p.drawImage( + QRect(half / dpr, 0, width - 2 * (half / dpr), height / dpr), + background, + QRect(half, 0, 1, height)); + } + + if (_player && _player->ready()) { + const auto paused = !isOver(); + auto info = _player->frame( + st::giftBoxStickerSize, + QColor(0, 0, 0, 0), + false, + crl::now(), + paused); + const auto finished = (info.index + 1 == _player->framesCount()); + if (!finished || !paused) { + _player->markFrameShown(); + } + const auto size = info.image.size() / style::DevicePixelRatio(); + p.drawImage( + QRect( + (width - size.width()) / 2, + (_text.isEmpty() + ? st::giftBoxStickerStarTop + : st::giftBoxStickerTop), + size.width(), + size.height()), + info.image); + } + + auto hq = PainterHighQualityEnabler(p); + const auto premium = v::is(_descriptor); + const auto singlew = width - _extend.left() - _extend.right(); + const auto font = st::semiboldFont; + p.setFont(font); + const auto text = v::match(_descriptor, [&](GiftTypePremium data) { + if (data.discountPercent > 0) { + p.setBrush(st::attentionBoxButton.textFg); + const auto kMinus = QChar(0x2212); + return kMinus + QString::number(data.discountPercent) + '%'; + } + return QString(); + }, [&](const GiftTypeStars &data) { + if (data.limited) { + p.setBrush(st::windowActiveTextFg); + return tr::lng_gift_stars_limited(tr::now); + } + return QString(); + }); + if (!text.isEmpty()) { + p.setPen(Qt::NoPen); + const auto twidth = font->width(text); + const auto pos = position + QPoint(singlew - twidth, font->height); + p.save(); + p.translate(pos); + p.rotate(45.); + p.translate(-pos); + p.drawRect(-5 * twidth, position.y(), twidth * 12, font->height); + p.setPen(st::windowBg); + p.drawText(pos - QPoint(0, font->descent), text); + p.restore(); + } + p.setBrush(premium ? st::lightButtonBgOver : st::creditsBg3); + p.setPen(Qt::NoPen); + if (!premium) { + p.setOpacity(0.12); + } + const auto geometry = _button; + const auto radius = geometry.height() / 2.; + p.drawRoundedRect(geometry, radius, radius); + if (!premium) { + p.setOpacity(1.); + } + + if (!_text.isEmpty()) { + p.setPen(st::windowFg); + _text.draw(p, { + .position = (position + + QPoint(0, st::giftBoxPremiumTextTop)), + .availableWidth = singlew, + .align = style::al_top, + }); + } + + const auto padding = st::giftBoxButtonPadding; + p.setPen(premium ? st::windowActiveTextFg : st::creditsFg); + _price.draw(p, { + .position = (geometry.topLeft() + + QPoint(padding.left(), padding.top())), + .availableWidth = _price.maxWidth(), + }); +} + +Delegate::Delegate(not_null window) +: _window(window) { +} + + +TextWithEntities Delegate::star() { + const auto owner = &_window->session().data(); + return owner->customEmojiManager().creditsEmoji(); +} + +std::any Delegate::textContext() { + return Core::MarkedTextContext{ + .session = &_window->session(), + .customEmojiRepaint = [] {}, + }; +} + +QSize Delegate::buttonSize() { + if (!_single.isEmpty()) { + return _single; + } + const auto width = st::boxWideWidth; + const auto padding = st::giftBoxPadding; + const auto available = width - padding.left() - padding.right(); + const auto singlew = (available - 2 * st::giftBoxGiftSkip.x()) + / kGiftsPerRow; + _single = QSize(singlew, st::giftBoxGiftHeight); + return _single; +} + +QMargins Delegate::buttonExtend() { + return st::defaultDropdownMenu.wrap.shadow.extend; +} + +QImage Delegate::background() { + if (!_bg.isNull()) { + return _bg; + } + const auto single = buttonSize(); + const auto extend = buttonExtend(); + const auto bgSize = single.grownBy(extend); + const auto ratio = style::DevicePixelRatio(); + auto bg = QImage( + bgSize * ratio, + QImage::Format_ARGB32_Premultiplied); + bg.setDevicePixelRatio(ratio); + bg.fill(Qt::transparent); + + const auto radius = st::giftBoxGiftRadius; + const auto rect = QRect(QPoint(), bgSize).marginsRemoved(extend); + + { + auto p = QPainter(&bg); + auto hq = PainterHighQualityEnabler(p); + p.setOpacity(0.3); + p.setPen(Qt::NoPen); + p.setBrush(st::windowShadowFg); + p.drawRoundedRect( + QRectF(rect).translated(0, radius / 12.), + radius, + radius); + } + bg = bg.scaled( + (bgSize * ratio) / 2, + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); + bg = Images::Blur(std::move(bg), true); + bg = bg.scaled( + bgSize * ratio, + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation); + { + auto p = QPainter(&bg); + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(st::windowBg); + p.drawRoundedRect(rect, radius, radius); + } + + _bg = std::move(bg); + return _bg; +} + +DocumentData *Delegate::lookupSticker(const GiftDescriptor &descriptor) { + const auto &session = _window->session(); + auto &packs = session.giftBoxStickersPacks(); + packs.load(); + return v::match(descriptor, [&](GiftTypePremium data) { + return packs.lookup(data.months); + }, [&](GiftTypeStars data) { + return data.document + ? data.document + : packs.lookup(packs.monthsForStars(data.stars)); + }); +} + +} // namespace Info::PeerGifts diff --git a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h new file mode 100644 index 000000000..42ec3ebf2 --- /dev/null +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h @@ -0,0 +1,111 @@ +/* +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 + +#include "ui/abstract_button.h" +#include "ui/text/text.h" + +namespace HistoryView { +class StickerPlayer; +} // namespace HistoryView + +namespace Window { +class SessionController; +} // namespace Window + +namespace Info::PeerGifts { + +struct GiftTypePremium { + int64 cost = 0; + QString currency; + int months = 0; + int discountPercent = 0; + + [[nodiscard]] friend inline bool operator==( + const GiftTypePremium &, + const GiftTypePremium &) = default; +}; + +struct GiftTypeStars { + uint64 id = 0; + int64 stars = 0; + int64 convertStars = 0; + DocumentData *document = nullptr; + PeerData *from = nullptr; + bool limited = false; + bool userpic = false; + + [[nodiscard]] friend inline bool operator==( + const GiftTypeStars&, + const GiftTypeStars&) = default; +}; + +struct GiftDescriptor : std::variant { + using variant::variant; + + [[nodiscard]] friend inline bool operator==( + const GiftDescriptor&, + const GiftDescriptor&) = default; +}; + +class GiftButtonDelegate { +public: + [[nodiscard]] virtual TextWithEntities star() = 0; + [[nodiscard]] virtual std::any textContext() = 0; + [[nodiscard]] virtual QSize buttonSize() = 0; + [[nodiscard]] virtual QMargins buttonExtend() = 0; + [[nodiscard]] virtual QImage background() = 0; + [[nodiscard]] virtual DocumentData *lookupSticker( + const GiftDescriptor &descriptor) = 0; +}; + +class GiftButton final : public Ui::AbstractButton { +public: + GiftButton(QWidget *parent, not_null delegate); + ~GiftButton(); + + void setDescriptor(const GiftDescriptor &descriptor); + void setGeometry(QRect inner, QMargins extend); + +private: + void paintEvent(QPaintEvent *e) override; + void resizeEvent(QResizeEvent *e) override; + + void setDocument(not_null document); + + const not_null _delegate; + GiftDescriptor _descriptor; + Ui::Text::String _text; + Ui::Text::String _price; + QRect _button; + QMargins _extend; + + std::unique_ptr _player; + rpl::lifetime _mediaLifetime; + +}; + +class Delegate final : public GiftButtonDelegate { +public: + explicit Delegate(not_null window); + + TextWithEntities star() override; + std::any textContext() override; + QSize buttonSize() override; + QMargins buttonExtend() override; + QImage background() override; + DocumentData *lookupSticker(const GiftDescriptor &descriptor) override; + +private: + const not_null _window; + QSize _single; + QImage _bg; + +}; + +} // namespace Info::PeerGifts diff --git a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp index 8c46130d5..1fb20aac7 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp @@ -7,16 +7,40 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "info/peer_gifts/info_peer_gifts_widget.h" +#include "data/data_session.h" #include "data/data_user.h" +#include "info/peer_gifts/info_peer_gifts_common.h" #include "info/info_controller.h" #include "ui/ui_utility.h" #include "lang/lang_keys.h" +#include "main/main_session.h" +#include "mtproto/sender.h" #include "window/window_session_controller.h" #include "styles/style_info.h" +#include "styles/style_credits.h" // giftBoxPadding namespace Info::PeerGifts { namespace { +constexpr auto kPreloadPages = 2; +constexpr auto kPerPage = 50; + +[[nodiscard]] GiftDescriptor DescriptorForGift( + not_null owner, + const Api::UserStarGift &gift) { + return GiftTypeStars{ + .id = gift.gift.id, + .stars = gift.gift.stars, + .convertStars = gift.gift.convertStars, + .document = gift.gift.document, + .from = ((gift.hidden || !gift.fromId) + ? nullptr + : owner->peer(gift.fromId).get()), + .limited = (gift.gift.limitedCount > 0), + .userpic = true, + }; +} + } // namespace class InnerWidget final : public Ui::RpWidget { @@ -30,24 +54,51 @@ public: return _user; } - rpl::producer scrollToRequests() const; - - int desiredHeight() const; - void saveState(not_null memento); void restoreState(not_null memento); -protected: +private: + struct Entry { + Api::UserStarGift gift; + GiftDescriptor descriptor; + }; + struct View { + std::unique_ptr button; + Api::UserStarGift gift; + }; + void visibleTopBottomUpdated( int visibleTop, int visibleBottom) override; + void paintEvent(QPaintEvent *e) override; -private: + void loadMore(); + void validateButtons(); + + int resizeGetHeight(int width) override; + + Delegate _delegate; const std::shared_ptr _show; not_null _controller; const not_null _user; + std::vector _entries; + int _totalCount = 0; - rpl::event_stream _scrollToRequests; + MTP::Sender _api; + mtpRequestId _loadMoreRequestId = 0; + QString _offset; + bool _allLoaded = false; + + std::vector _views; + int _viewsForWidth = 0; + int _viewsFromRow = 0; + int _viewsTillRow = 0; + + QSize _singleMin; + QSize _single; + int _perRow = 0; + int _visibleFrom = 0; + int _visibleTill = 0; }; @@ -56,14 +107,155 @@ InnerWidget::InnerWidget( not_null controller, not_null user) : RpWidget(parent) +, _delegate(controller->parentController()) , _show(controller->uiShow()) , _controller(controller) -, _user(user) { +, _user(user) +, _totalCount(_user->peerGiftsCount()) +, _api(&_user->session().mtp()) { + _singleMin = _delegate.buttonSize(); } void InnerWidget::visibleTopBottomUpdated( int visibleTop, int visibleBottom) { + const auto page = (visibleBottom - visibleTop); + if (visibleBottom + page * kPreloadPages >= height()) { + loadMore(); + } + _visibleFrom = visibleTop; + _visibleTill = visibleBottom; + validateButtons(); +} + +void InnerWidget::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + + p.fillRect(e->rect(), st::boxDividerBg); +} + +void InnerWidget::loadMore() { + if (_allLoaded || _loadMoreRequestId) { + return; + } + _loadMoreRequestId = _api.request(MTPpayments_GetUserStarGifts( + _user->inputUser, + MTP_string(_offset), + MTP_int(kPerPage) + )).done([=](const MTPpayments_UserStarGifts &result) { + _loadMoreRequestId = 0; + const auto &data = result.data(); + if (const auto next = data.vnext_offset()) { + _offset = qs(*next); + } else { + _allLoaded = true; + } + _totalCount = data.vcount().v; + + const auto owner = &_user->owner(); + owner->processUsers(data.vusers()); + + const auto session = &_show->session(); + _entries.reserve(_entries.size() + data.vgifts().v.size()); + for (const auto &gift : data.vgifts().v) { + if (auto parsed = Api::FromTL(session, gift)) { + auto descriptor = DescriptorForGift(owner, *parsed); + _entries.push_back({ + .gift = std::move(*parsed), + .descriptor = std::move(descriptor), + }); + } + } + _viewsForWidth = 0; + _viewsFromRow = 0; + _viewsTillRow = 0; + resizeToWidth(width()); + validateButtons(); + }).fail([=] { + _loadMoreRequestId = 0; + _allLoaded = true; + }).send(); +} + +void InnerWidget::validateButtons() { + if (!_perRow) { + return; + } + const auto row = _single.height() + st::giftBoxGiftSkip.y(); + const auto fromRow = _visibleFrom / row; + const auto tillRow = (_visibleTill + row - 1) / row; + Assert(tillRow >= fromRow); + if (_viewsFromRow == fromRow + && _viewsTillRow == tillRow + && _viewsForWidth == width()) { + return; + } + _viewsFromRow = fromRow; + _viewsTillRow = tillRow; + _viewsForWidth = width(); + + const auto padding = st::giftBoxPadding; + const auto available = _viewsForWidth - padding.left() - padding.right(); + const auto skipw = st::giftBoxGiftSkip.x(); + const auto fullw = _perRow * (_single.width() + skipw) - skipw; + const auto left = padding.left() + (available - fullw) / 2; + auto x = left; + auto y = padding.bottom(); + auto entry = 0; + for (auto j = fromRow; j != tillRow; ++j) { + for (auto i = 0; i != _perRow; ++i) { + const auto index = j * _perRow + i; + if (index >= _entries.size()) { + break; + } + const auto &descriptor = _entries[index].descriptor; + if (entry < _views.size()) { + _views[entry].button->setDescriptor(descriptor); + } else { + auto button = std::make_unique(this, &_delegate); + button->setDescriptor(descriptor); + _views.push_back({ + .button = std::move(button), + .gift = _entries[index].gift, + }); + } + _views[entry].button->show(); + _views[entry].button->setGeometry( + QRect(QPoint(x, y), _single), + _delegate.buttonExtend()); + ++entry; + x += _single.width() + skipw; + } + x = left; + y += _single.height() + st::giftBoxGiftSkip.y(); + } + for (auto k = entry; k != int(_views.size()); ++k) { + _views[k].button->hide(); + } +} + +int InnerWidget::resizeGetHeight(int width) { + const auto count = int(_entries.size()); + const auto padding = st::giftBoxPadding; + const auto available = width - padding.left() - padding.right(); + const auto skipw = st::giftBoxGiftSkip.x(); + _perRow = std::min( + (available + skipw) / (_singleMin.width() + skipw), + count); + if (!_perRow) { + return 0; + } + const auto singlew = std::min( + ((available + skipw) / _perRow) - skipw, + 2 * _singleMin.width()); + Assert(singlew >= _singleMin.width()); + const auto singleh = _singleMin.height(); + + _single = QSize(singlew, singleh); + const auto rows = (count + _perRow - 1) / _perRow; + const auto skiph = st::giftBoxGiftSkip.y(); + + return padding.bottom() * 2 + rows * (singleh + skiph) - skiph; } void InnerWidget::saveState(not_null memento) { @@ -77,16 +269,6 @@ void InnerWidget::restoreState(not_null memento) { } } -rpl::producer InnerWidget::scrollToRequests() const { - return _scrollToRequests.events(); -} - -int InnerWidget::desiredHeight() const { - auto desired = 0; - - return qMax(height(), desired); -} - Memento::Memento(not_null user) : ContentMemento(user, nullptr, PeerId()) { } diff --git a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.h b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.h index 3c6b216c6..3882fa5cb 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.h +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.h @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "api/api_premium.h" #include "info/info_content_widget.h" class UserData; @@ -14,17 +15,8 @@ struct PeerListState; namespace Info::PeerGifts { -struct ListEntry { - TextWithEntities message; - int64 convertStars = 0; - PeerId fromId = 0; - MsgId messageId = 0; - bool anonymous = false; - bool hidden = false; -}; - struct ListState { - std::vector list; + std::vector list; QString offset; };