From b08869abdb63a2148e05258b4222e1824cc67e0d Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 6 Oct 2023 11:15:26 +0400 Subject: [PATCH] Support giveaway message layout. --- Telegram/Resources/langs/lang.strings | 2 +- .../history/history_inner_widget.cpp | 41 ++ .../history/view/history_view_element.cpp | 92 +++- .../history/view/history_view_element.h | 21 + .../history/view/history_view_message.cpp | 14 + .../history/view/history_view_view_button.cpp | 34 +- .../view/media/history_view_giveaway.cpp | 446 ++++++++++-------- .../view/media/history_view_giveaway.h | 77 ++- .../history/view/media/history_view_media.h | 3 + .../payments/payments_checkout_process.cpp | 50 +- .../payments/payments_checkout_process.h | 4 + .../SourceFiles/payments/payments_form.cpp | 87 +++- Telegram/SourceFiles/payments/payments_form.h | 28 +- Telegram/SourceFiles/ui/chat/chat.style | 11 +- 14 files changed, 659 insertions(+), 251 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index aca64ad73..e3986b800 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2091,7 +2091,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_prizes_participants_new#one" = "All users who joined the channel below after this date:"; "lng_prizes_participants_new#other" = "All users who joined the channels below after this date:"; "lng_prizes_date" = "Winners Selection Date"; -"lng_prizes_how_works" = "How does it work?"; +"lng_prizes_how_works" = "Learn more"; "lng_prizes_how_text#one" = "This giveaway is sponsored by the admins of {channel}, who aquired **{count} Telegram Premium** subscription for {duration} for its followers."; "lng_prizes_how_text#other" = "This giveaway is sponsored by the admins of {channel}, who aquired **{count} Telegram Premium** subscriptions for {duration} for its followers."; "lng_prizes_how_when_all_of_one#one" = "On {date}, Telegram will automatically select {count} random subscribers of {channel}."; diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index a575ce312..23d3f1c4e 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -96,6 +96,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_chat.h" #include "styles/style_menu_icons.h" +#include "payments/payments_checkout_process.h" +#include "payments/payments_form.h" +#include "base/random.h" + #include #include #include @@ -449,6 +453,43 @@ HistoryInner::HistoryInner( _migrated->translateTo(_history->translatedTo()); } +#if 0 + if (const auto channel = _history->peer->asBroadcast()) { + if (channel->amCreator()) { + const auto weak = base::make_weak(_controller); + channel->session().api().request(MTPpayments_GetPremiumGiftCodeOptions( + MTP_flags(MTPpayments_GetPremiumGiftCodeOptions::Flag::f_boost_peer), + channel->input + )).done(crl::guard(weak, [=](const MTPVector &result) { + if (result.v.isEmpty()) { + return; + } + const auto &data = result.v.front().data(); + const auto randomId = base::RandomValue(); + Payments::CheckoutProcess::Start( + Payments::InvoicePremiumGiftCode{ + .purpose = Payments::InvoicePremiumGiftCodeGiveaway{ + .boostPeer = channel, + //.additionalChannels = , + .untilDate = (base::unixtime::now() + 300), + .onlyNewSubscribers = true, + }, + .randomId = randomId, + .currency = qs(data.vcurrency()), + .amount = data.vamount().v, + .storeProduct = qs(data.vstore_product().value_or_empty()), + .storeQuantity = data.vstore_quantity().value_or_empty(), + .users = data.vusers().v, + .months = data.vmonths().v, + }, + crl::guard(weak, [=](auto) { weak->window().activate(); })); + })).fail(crl::guard(weak, [=](const MTP::Error &error) { + weak.get()->showToast(error.type()); + })).send(); + } + } +#endif + Window::ChatThemeValueFromPeer( controller, _peer diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index 22dd45ab4..aab999319 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -338,7 +338,6 @@ void UnreadBar::paint( text); } - void DateBadge::init(const QString &date) { text = date; width = st::msgServiceFont->width(text); @@ -361,6 +360,81 @@ void DateBadge::paint( ServiceMessagePainter::PaintDate(p, st, text, width, y, w, chatWide); } +void ServicePreMessage::init(TextWithEntities string) { + text = Ui::Text::String( + st::serviceTextStyle, + string, + kMarkupTextOptions, + st::msgMinWidth); +} + +int ServicePreMessage::resizeToWidth(int newWidth, bool chatWide) { + width = newWidth; + if (chatWide) { + accumulate_min( + width, + st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left()); + } + auto contentWidth = width; + contentWidth -= st::msgServiceMargin.left() + st::msgServiceMargin.left(); // two small margins + if (contentWidth < st::msgServicePadding.left() + st::msgServicePadding.right() + 1) { + contentWidth = st::msgServicePadding.left() + st::msgServicePadding.right() + 1; + } + + auto maxWidth = text.maxWidth() + + st::msgServicePadding.left() + + st::msgServicePadding.right(); + auto minHeight = text.minHeight(); + + auto nwidth = qMax(contentWidth + - st::msgServicePadding.left() + - st::msgServicePadding.right(), 0); + height = (contentWidth >= maxWidth) + ? minHeight + : text.countHeight(nwidth); + height += st::msgServicePadding.top() + + st::msgServicePadding.bottom() + + st::msgServiceMargin.top() + + st::msgServiceMargin.bottom(); + return height; +} + +void ServicePreMessage::paint( + Painter &p, + const PaintContext &context, + QRect g, + bool chatWide) const { + const auto top = g.top() - height - st::msgMargin.top(); + p.translate(0, top); + + const auto rect = QRect(0, 0, width, height) + - st::msgServiceMargin; + const auto trect = rect - st::msgServicePadding; + + ServiceMessagePainter::PaintComplexBubble( + p, + context.st, + rect.left(), + rect.width(), + text, + trect); + + p.setBrush(Qt::NoBrush); + p.setPen(context.st->msgServiceFg()); + p.setFont(st::msgServiceFont); + text.draw(p, { + .position = trect.topLeft(), + .availableWidth = trect.width(), + .align = style::al_top, + .palette = &context.st->serviceTextPalette(), + .now = context.now, + //.selection = context.selection, + .fullWidthSelection = false, + }); + + p.translate(0, -top); +} + void FakeBotAboutTop::init() { if (!text.isEmpty()) { return; @@ -971,7 +1045,9 @@ bool Element::computeIsAttachToPrevious(not_null previous) { || !item->from()->isChannel()); }; const auto item = data(); - if (!Has() && !Has()) { + if (!Has() + && !Has() + && !Has()) { const auto prev = previous->data(); const auto previousMarkup = prev->inlineReplyMarkup(); const auto possible = (std::abs(prev->date() - item->date()) @@ -1183,6 +1259,18 @@ void Element::setDisplayDate(bool displayDate) { } } +void Element::setServicePreMessage(TextWithEntities text) { + if (!text.empty()) { + AddComponents(ServicePreMessage::Bit()); + const auto service = Get(); + service->init(std::move(text)); + setPendingResize(); + } else if (Has()) { + RemoveComponents(ServicePreMessage::Bit()); + setPendingResize(); + } +} + void Element::setAttachToNext(bool attachToNext, Element *next) { Expects(next || !attachToNext); diff --git a/Telegram/SourceFiles/history/view/history_view_element.h b/Telegram/SourceFiles/history/view/history_view_element.h index 1824f7137..4f62303c7 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.h +++ b/Telegram/SourceFiles/history/view/history_view_element.h @@ -234,6 +234,26 @@ struct DateBadge : public RuntimeComponent { }; +// Any HistoryView::Element can have this Component for +// displaying some text in layout of a service message above the message. +struct ServicePreMessage + : public RuntimeComponent { + void init(TextWithEntities string); + + int resizeToWidth(int newWidth, bool chatWide); + + void paint( + Painter &p, + const PaintContext &context, + QRect g, + bool chatWide) const; + + Ui::Text::String text; + int width = 0; + int height = 0; + +}; + struct FakeBotAboutTop : public RuntimeComponent { void init(); @@ -339,6 +359,7 @@ public: // For blocks context this should be called only from recountDisplayDate(). void setDisplayDate(bool displayDate); + void setServicePreMessage(TextWithEntities text); bool computeIsAttachToPrevious(not_null previous); diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index dca4e0af0..a50b92d27 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -874,6 +874,9 @@ int Message::marginTop() const { if (const auto bar = Get()) { result += bar->height(); } + if (const auto service = Get()) { + result += service->height; + } return result; } @@ -911,6 +914,10 @@ void Message::draw(Painter &p, const PaintContext &context) const { } } + if (const auto service = Get()) { + service->paint(p, context, g, delegate()->elementIsChatWide()); + } + if (isHidden()) { return; } @@ -3043,6 +3050,9 @@ bool Message::hasFromName() const { if (hasOutLayout() && !item->from()->isChannel()) { return false; } else if (!peer->isUser()) { + if (const auto media = this->media()) { + return !media->hideFromName(); + } return true; } if (const auto forwarded = item->Get()) { @@ -3703,6 +3713,10 @@ int Message::resizeContentGetHeight(int newWidth) { auto newHeight = minHeight(); + if (const auto service = Get()) { + service->resizeToWidth(newWidth, delegate()->elementIsChatWide()); + } + const auto item = data(); const auto botTop = item->isFakeBotAbout() ? Get() diff --git a/Telegram/SourceFiles/history/view/history_view_view_button.cpp b/Telegram/SourceFiles/history/view/history_view_view_button.cpp index 197918968..70c49452a 100644 --- a/Telegram/SourceFiles/history/view/history_view_view_button.cpp +++ b/Telegram/SourceFiles/history/view/history_view_view_button.cpp @@ -83,12 +83,23 @@ inline auto WebPageToPhrase(not_null webpage) { : QString()); } -[[nodiscard]] ClickHandlerPtr MakeWebPageButtonClickHandler( +[[nodiscard]] ClickHandlerPtr MakeMediaButtonClickHandler( not_null media) { - Expects(media->webpage() != nullptr); + if (const auto giveaway = media->giveaway()) { + return std::make_shared([=]( + ClickContext context) { + const auto my = context.other.value(); + const auto controller = my.sessionWindow.get(); + if (!controller) { + return; + } + }); + } + const auto webpage = media->webpage(); + Assert(webpage != nullptr); - const auto url = media->webpage()->url; - const auto type = media->webpage()->type; + const auto url = webpage->url; + const auto type = webpage->type; return std::make_shared([=](ClickContext context) { const auto my = context.other.value(); if (const auto controller = my.sessionWindow.get()) { @@ -105,6 +116,15 @@ inline auto WebPageToPhrase(not_null webpage) { }); } +[[nodiscard]] QString MakeMediaButtonText(not_null media) { + if (const auto giveaway = media->giveaway()) { + return Ui::Text::Upper(tr::lng_prizes_how_works(tr::now)); + } + const auto webpage = media->webpage(); + Assert(webpage != nullptr); + return WebPageToPhrase(webpage); +} + [[nodiscard]] ClickHandlerPtr SponsoredLink( not_null sponsored) { if (!sponsored->externalLink.isEmpty()) { @@ -170,7 +190,7 @@ struct ViewButton::Inner { bool ViewButton::MediaHasViewButton(not_null media) { return media->webpage() ? MediaHasViewButton(media->webpage()) - : false; + : (media->giveaway() != nullptr); } bool ViewButton::MediaHasViewButton( @@ -209,10 +229,10 @@ ViewButton::Inner::Inner( not_null media, Fn updateCallback) : margins(st::historyViewButtonMargins) -, link(MakeWebPageButtonClickHandler(media)) +, link(MakeMediaButtonClickHandler(media)) , updateCallback(std::move(updateCallback)) , belowInfo(false) -, text(st::historyViewButtonTextStyle, WebPageToPhrase(media->webpage())) { +, text(st::historyViewButtonTextStyle, MakeMediaButtonText(media)) { } void ViewButton::Inner::updateMask(int height) { diff --git a/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp b/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp index 3835a1a3b..3c637ec98 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_giveaway.cpp @@ -8,74 +8,91 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/media/history_view_giveaway.h" #include "base/unixtime.h" +#include "chat_helpers/stickers_gift_box_pack.h" #include "data/data_channel.h" +#include "data/data_document.h" #include "data/data_media_types.h" -#include "lang/lang_keys.h" +#include "data/data_session.h" +#include "dialogs/ui/dialogs_stories_content.h" +#include "dialogs/ui/dialogs_stories_list.h" +#include "history/history.h" #include "history/history_item.h" +#include "history/history_item_components.h" #include "history/view/history_view_element.h" #include "history/view/history_view_cursor_state.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" #include "ui/chat/chat_style.h" #include "ui/text/text_utilities.h" +#include "ui/widgets/tooltip.h" +#include "ui/painter.h" +#include "ui/round_rect.h" #include "styles/style_chat.h" namespace HistoryView { +namespace { -void TextRows::add(Ui::Text::String text, int skipTop) { +constexpr auto kChannelBgAlpha = 32; + +[[nodiscard]] QSize CountOptimalTextSize( + const Ui::Text::String &text, + int minWidth, + int maxWidth) { + if (text.maxWidth() <= maxWidth) { + return { text.maxWidth(), text.minHeight() }; + } + const auto height = text.countHeight(maxWidth); + return { Ui::FindNiceTooltipWidth(minWidth, maxWidth, [&](int width) { + return text.countHeight(width); + }), height }; } -bool TextRows::isEmpty() const { - return _rows.empty() - || (_rows.size() == 1 && _rows.front().text.isEmpty()); -} - -int TextRows::maxWidth() const { - return 0; -} - -int TextRows::minHeight() const { - return 0; -} - -int TextRows::countHeight(int newWidth) const { - return 0; -} - -int TextRows::length() const { - return 0; -} - -TextSelection UnshiftItemSelection( - TextSelection selection, - const TextRows &byText) { - return UnshiftItemSelection(selection, byText.length()); -} - -TextSelection ShiftItemSelection( - TextSelection selection, - const TextRows &byText) { - return ShiftItemSelection(selection, byText.length()); -} +} // namespace Giveaway::Giveaway( not_null parent, not_null giveaway) -: Media(parent) { +: Media(parent) +, _prizesTitle(st::msgMinWidth) +, _prizes(st::msgMinWidth) +, _participantsTitle(st::msgMinWidth) +, _participants(st::msgMinWidth) +, _winnersTitle(st::msgMinWidth) +, _winners(st::msgMinWidth) { fillFromData(giveaway); + + if (!parent->data()->Has() + && ranges::contains( + giveaway->channels, + parent->data()->history()->peer)) { + parent->setServicePreMessage({ + tr::lng_action_giveaway_started( + tr::now, + lt_from, + parent->data()->history()->peer->name()), + }); + } } -Giveaway::~Giveaway() = default; +Giveaway::~Giveaway() { + if (hasHeavyPart()) { + unloadHeavyPart(); + _parent->checkHeavyPart(); + } +} void Giveaway::fillFromData(not_null giveaway) { - _rows.add(Ui::Text::String( + _months = giveaway->months; + + _prizesTitle.setText( st::semiboldTextStyle, tr::lng_prizes_title(tr::now, lt_count, giveaway->quantity), - kDefaultTextOptions, - st::msgMinWidth), st::chatGiveawayPrizesTop); + kDefaultTextOptions); const auto duration = (giveaway->months < 12) ? tr::lng_months(tr::now, lt_count, giveaway->months) : tr::lng_years(tr::now, lt_count, giveaway->months / 12); - _rows.add(Ui::Text::String( + _prizes.setMarkedText( st::defaultTextStyle, tr::lng_prizes_about( tr::now, @@ -84,94 +101,123 @@ void Giveaway::fillFromData(not_null giveaway) { lt_duration, Ui::Text::Bold(duration), Ui::Text::RichLangValue), - kDefaultTextOptions, - st::msgMinWidth), st::chatGiveawayPrizesSkip); - - _rows.add(Ui::Text::String( + kDefaultTextOptions); + _participantsTitle.setText( st::semiboldTextStyle, tr::lng_prizes_participants(tr::now), - kDefaultTextOptions, - st::msgMinWidth), st::chatGiveawayParticipantsTop); + kDefaultTextOptions); for (const auto &channel : giveaway->channels) { - _channels.push_back({ Ui::Text::String( - st::semiboldTextStyle, - channel->name(), - kDefaultTextOptions, - st::msgMinWidth) + _channels.push_back({ + .name = Ui::Text::String( + st::semiboldTextStyle, + channel->name(), + kDefaultTextOptions, + st::msgMinWidth), + .thumbnail = Dialogs::Stories::MakeUserpicThumbnail(channel), + .link = channel->openLink(), }); } - const auto channels = int(_channels.size()); - _rows.add(Ui::Text::String( + + _participants.setText( st::defaultTextStyle, (giveaway->all ? tr::lng_prizes_participants_all : tr::lng_prizes_participants_new)(tr::now, lt_count, channels), - kDefaultTextOptions, - st::msgMinWidth), st::chatGiveawayParticipantsSkip); - - _date.add(Ui::Text::String( + kDefaultTextOptions); + _winnersTitle.setText( st::semiboldTextStyle, tr::lng_prizes_date(tr::now), - kDefaultTextOptions, - st::msgMinWidth), st::chatGiveawayDateTop); - - _rows.add(Ui::Text::String( + kDefaultTextOptions); + _winners.setText( st::defaultTextStyle, langDateTime(base::unixtime::parse(giveaway->untilDate)), - kDefaultTextOptions, - st::msgMinWidth), st::chatGiveawayDateSkip); + kDefaultTextOptions); + + ensureStickerCreated(); } QSize Giveaway::countOptimalSize() { - // init dimensions - auto skipBlockWidth = _parent->skipBlockWidth(); - auto maxWidth = skipBlockWidth; - auto minHeight = 0; + const auto maxWidth = st::chatGiveawayWidth; + const auto padding = inBubblePadding(); + const auto available = maxWidth - padding.left() - padding.right(); - accumulate_max(maxWidth, _rows.maxWidth()); - minHeight += _rows.minHeight(); + _stickerTop = st::chatGiveawayStickerTop; + _prizesTitleTop = _stickerTop + + st::msgServiceGiftBoxStickerSize.height() + + st::chatGiveawayPrizesTop; + _prizesTop = _prizesTitleTop + + _prizesTitle.countHeight(available) + + st::chatGiveawayPrizesSkip; + const auto prizesSize = CountOptimalTextSize( + _prizes, + st::msgMinWidth, + available); + _prizesWidth = prizesSize.width(); + _participantsTitleTop = _prizesTop + + prizesSize.height() + + st::chatGiveawayParticipantsTop; + _participantsTop = _participantsTitleTop + + _participantsTitle.countHeight(available) + + st::chatGiveawayParticipantsSkip; + const auto participantsSize = CountOptimalTextSize( + _participants, + st::msgMinWidth, + available); + _participantsWidth = participantsSize.width(); + const auto channelsTop = _participantsTop + + participantsSize.height() + + st::chatGiveawayChannelTop; + const auto channelsBottom = layoutChannels( + padding.left(), + channelsTop, + available); + _winnersTitleTop = channelsBottom + st::chatGiveawayDateTop; + _winnersTop = _winnersTitleTop + + _winnersTitle.countHeight(available) + + st::chatGiveawayDateSkip; + const auto height = _winnersTop + + _winners.countHeight(available) + + st::chatGiveawayBottomSkip; + return { maxWidth, height }; +} - //minHeight += - - accumulate_max(maxWidth, _date.maxWidth()); - minHeight += _date.minHeight(); - - auto padding = inBubblePadding(); - maxWidth += padding.left() + padding.right(); - minHeight += padding.top() + padding.bottom(); - return { maxWidth, minHeight }; +int Giveaway::layoutChannels(int x, int y, int available) { + const auto size = st::chatGiveawayChannelSize; + const auto skip = st::chatGiveawayChannelSkip; + const auto padding = st::chatGiveawayChannelPadding; + auto left = available; + const auto shiftRow = [&](int i, int top, int shift) { + for (auto j = i; j != 0; --j) { + auto &geometry = _channels[j - 1].geometry; + if (geometry.top() != top) { + break; + } + geometry.moveLeft(geometry.x() + shift); + } + }; + const auto count = int(_channels.size()); + for (auto i = 0; i != count; ++i) { + const auto desired = size + + padding.left() + + _channels[i].name.maxWidth() + + padding.right(); + const auto width = std::min(desired, available); + if (left < width) { + shiftRow(i, y, (left + skip) / 2); + left = available; + y += size + skip; + } + _channels[i].geometry = { x + available - left, y, width, size }; + left -= width + skip; + } + shiftRow(count, y, (left + skip) / 2); + return y + size + skip; } QSize Giveaway::countCurrentSize(int newWidth) { - accumulate_min(newWidth, maxWidth()); - auto innerWidth = newWidth - - st::msgPadding.left() - - st::msgPadding.right(); - - auto newHeight = 0; - _rowsHeight = _rows.countHeight(innerWidth); - newHeight += _rowsHeight; - - //newHeight += - - newHeight += _date.countHeight(innerWidth); - _dateHeight = _date.minHeight(); - newHeight += _dateHeight; - - auto padding = inBubblePadding(); - newHeight += padding.top() + padding.bottom(); - - return { newWidth, newHeight }; -} - -TextSelection Giveaway::toDateSelection(TextSelection selection) const { - return UnshiftItemSelection(selection, _rows); -} - -TextSelection Giveaway::fromDateSelection(TextSelection selection) const { - return ShiftItemSelection(selection, _rows); + return { maxWidth(), minHeight()}; } void Giveaway::draw(Painter &p, const PaintContext &context) const { @@ -184,24 +230,107 @@ void Giveaway::draw(Painter &p, const PaintContext &context) const { auto &semibold = stm->msgServiceFg; auto padding = inBubblePadding(); - auto tshift = padding.top(); - //_rows.draw(p, { - // .position = { padding.left(), tshift }, - // .outerWidth = width(), - // .availableWidth = paintw, - // .now = context.now, - // .selection = context.selection, - //}); - //tshift += _rows.countHeight(paintw); + const auto outer = width(); + const auto paintw = outer - padding.left() - padding.right(); + const auto stickerSize = st::msgServiceGiftBoxStickerSize; + const auto sticker = QRect( + (outer - stickerSize.width()) / 2, + _stickerTop, + stickerSize.width(), + stickerSize.height()); - //_date.draw(p, { - // .position = { padding.left(), tshift }, - // .outerWidth = width(), - // .availableWidth = paintw, - // .now = context.now, - // .selection = toDateSelection(context.selection), - //}); + if (_sticker) { + _sticker->draw(p, context, sticker); + } else { + ensureStickerCreated(); + } + const auto paintText = [&]( + const Ui::Text::String &text, + int top, + int width) { + p.setPen(stm->historyTextFg); + text.draw(p, { + .position = { padding.left() + (paintw - width) / 2, top}, + .outerWidth = outer, + .availableWidth = width, + .align = style::al_top, + .palette = &stm->textPalette, + .now = context.now, + }); + }; + paintText(_prizesTitle, _prizesTitleTop, paintw); + paintText(_prizes, _prizesTop, _prizesWidth); + paintText(_participantsTitle, _participantsTitleTop, paintw); + paintText(_participants, _participantsTop, _participantsWidth); + paintText(_winnersTitle, _winnersTitleTop, paintw); + paintText(_winners, _winnersTop, paintw); + paintChannels(p, context); +} + +void Giveaway::paintChannels( + Painter &p, + const PaintContext &context) const { + if (_channels.empty()) { + return; + } + + const auto size = _channels[0].geometry.height(); + const auto ratio = style::DevicePixelRatio(); + const auto stm = context.messageStyle(); + auto bg = stm->msgReplyBarColor->c; + bg.setAlpha(kChannelBgAlpha); + if (_channelCorners[0].isNull() || _channelBg != bg) { + _channelBg = bg; + _channelCorners = Images::CornersMask(size / 2); + for (auto &image : _channelCorners) { + style::colorizeImage(image, bg, &image); + } + } + p.setPen(stm->msgReplyBarColor); + const auto padding = st::chatGiveawayChannelPadding; + for (const auto &channel : _channels) { + const auto &thumbnail = channel.thumbnail; + const auto &geometry = channel.geometry; + if (!_subscribedToThumbnails) { + thumbnail->subscribeToUpdates([view = parent()] { + view->history()->owner().requestViewRepaint(view); + }); + } + Ui::DrawRoundedRect(p, geometry, _channelBg, _channelCorners); + p.drawImage(geometry.topLeft(), thumbnail->image(size)); + const auto left = size + padding.left(); + const auto top = padding.top(); + const auto available = geometry.width() - left - padding.right(); + channel.name.draw(p, { + .position = { geometry.left() + left, geometry.top() + top }, + .outerWidth = width(), + .availableWidth = available, + .align = style::al_left, + .palette = &stm->textPalette, + .now = context.now, + .elisionOneLine = true, + .elisionBreakEverywhere = true, + }); + } + _subscribedToThumbnails = true; +} + +void Giveaway::ensureStickerCreated() const { + if (_sticker) { + return; + } + const auto &session = _parent->history()->session(); + auto &packs = session.giftBoxStickersPacks(); + if (const auto document = packs.lookup(_months)) { + if (const auto sticker = document->sticker()) { + const auto skipPremiumEffect = false; + _sticker.emplace(_parent, document, skipPremiumEffect, _parent); + _sticker->setDiceIndex(sticker->alt, 1); + _sticker->setGiftBoxSticker(true); + _sticker->initSize(); + } + } } TextState Giveaway::textState(QPoint point, StateRequest request) const { @@ -211,80 +340,29 @@ TextState Giveaway::textState(QPoint point, StateRequest request) const { return result; } - auto padding = inBubblePadding(); - auto tshift = padding.top(); - auto bshift = padding.bottom(); - - auto symbolAdd = 0; - if (_rowsHeight > 0) { - if (point.y() >= tshift && point.y() < tshift + _rowsHeight) { - //result = TextState(_parent, _rows.getState( - // point - QPoint(padding.left(), tshift), - // paintw, - // width(), - // request.forText())); - } else if (point.y() >= tshift + _rowsHeight) { - symbolAdd += _rows.length(); + for (const auto &channel : _channels) { + if (channel.geometry.contains(point)) { + result.link = channel.link; + return result; } - tshift += _rowsHeight; } - if (_channelsHeight > 0) { - tshift += _channelsHeight; - } - if (_dateHeight > 0) { - if (point.y() >= tshift && point.y() < tshift + _dateHeight) { - //result = TextState(_parent, _date.getState( - // point - QPoint(padding.left(), tshift), - // paintw, - // width(), - // request.forText())); - } else if (point.y() >= tshift + _dateHeight) { - symbolAdd += _date.length(); - } - tshift += _dateHeight; - } - result.symbol += symbolAdd; return result; } -TextSelection Giveaway::adjustSelection( - TextSelection selection, - TextSelectType type) const { - //if (_date.isEmpty() || selection.to <= _rows.length()) { - // return _rows.adjustSelection(selection, type); - //} - //const auto dateSelection = _date.adjustSelection( - // toDateSelection(selection), - // type); - //if (selection.from >= _rows.length()) { - // return fromDateSelection(dateSelection); - //} - //const auto rowsSelection = _rows.adjustSelection(selection, type); - //return { rowsSelection.from, fromDateSelection(dateSelection).to }; - return selection; +bool Giveaway::hideFromName() const { + return !parent()->data()->Has(); } bool Giveaway::hasHeavyPart() const { - return false; + return _subscribedToThumbnails; } void Giveaway::unloadHeavyPart() { -} - -uint16 Giveaway::fullSelectionLength() const { - return 0; -} - -TextForMimeData Giveaway::selectedText(TextSelection selection) const { - //auto rowsResult = _rows.toTextForMimeData(selection); - //auto dateResult = _date.toTextForMimeData(toDateSelection(selection)); - //if (rowsResult.empty()) { - // return dateResult; - //} else if (dateResult.empty()) { - // return rowsResult; - //} - //return rowsResult.append('\n').append(std::move(dateResult)); - return {}; + if (base::take(_subscribedToThumbnails)) { + for (const auto &channel : _channels) { + channel.thumbnail->subscribeToUpdates(nullptr); + } + } } QMargins Giveaway::inBubblePadding() const { diff --git a/Telegram/SourceFiles/history/view/media/history_view_giveaway.h b/Telegram/SourceFiles/history/view/media/history_view_giveaway.h index 17dcdbebf..cdacdade8 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_giveaway.h +++ b/Telegram/SourceFiles/history/view/media/history_view_giveaway.h @@ -8,40 +8,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "history/view/media/history_view_media.h" +#include "history/view/media/history_view_sticker.h" namespace Data { struct Giveaway; } // namespace Data +namespace Dialogs::Stories { +class Thumbnail; +} // namespace Dialogs::Stories + namespace HistoryView { -class TextRows final { -public: - void add(Ui::Text::String text, int skipTop); - - [[nodiscard]] bool isEmpty() const; - [[nodiscard]] int maxWidth() const; - [[nodiscard]] int minHeight() const; - - [[nodiscard]] int countHeight(int newWidth) const; - - [[nodiscard]] int length() const; - -private: - struct Row { - Ui::Text::String text; - int skipTop = 0; - }; - std::vector _rows; -}; - -[[nodiscard]] TextSelection UnshiftItemSelection( - TextSelection selection, - const TextRows &byText); -[[nodiscard]] TextSelection ShiftItemSelection( - TextSelection selection, - const TextRows &byText); - class Giveaway final : public Media { public: Giveaway( @@ -67,35 +45,54 @@ public: return true; } - [[nodiscard]] TextSelection adjustSelection( - TextSelection selection, - TextSelectType type) const override; - uint16 fullSelectionLength() const override; - TextForMimeData selectedText(TextSelection selection) const override; + bool hideFromName() const override; void unloadHeavyPart() override; bool hasHeavyPart() const override; private: + using Thumbnail = Dialogs::Stories::Thumbnail; struct Channel { Ui::Text::String name; + std::shared_ptr thumbnail; + QRect geometry; + ClickHandlerPtr link; }; + void paintChannels(Painter &p, const PaintContext &context) const; + int layoutChannels(int x, int y, int available); QSize countOptimalSize() override; QSize countCurrentSize(int newWidth) override; void fillFromData(not_null giveaway); + void ensureStickerCreated() const; - TextSelection toDateSelection(TextSelection selection) const; - TextSelection fromDateSelection(TextSelection selection) const; - QMargins inBubblePadding() const; + [[nodiscard]] QMargins inBubblePadding() const; - TextRows _rows; + mutable std::optional _sticker; + + Ui::Text::String _prizesTitle; + Ui::Text::String _prizes; + Ui::Text::String _participantsTitle; + Ui::Text::String _participants; std::vector _channels; - TextRows _date; - int _rowsHeight = 0; - int _channelsHeight = 0; - int _dateHeight = 0; + Ui::Text::String _winnersTitle; + Ui::Text::String _winners; + + mutable QColor _channelBg; + mutable std::array _channelCorners; + + int _months = 0; + int _stickerTop = 0; + int _prizesTitleTop = 0; + int _prizesTop = 0; + int _prizesWidth = 0; + int _participantsTitleTop = 0; + int _participantsTop = 0; + int _participantsWidth = 0; + int _winnersTitleTop = 0; + int _winnersTop = 0; + mutable bool _subscribedToThumbnails = false; }; diff --git a/Telegram/SourceFiles/history/view/media/history_view_media.h b/Telegram/SourceFiles/history/view/media/history_view_media.h index 768107f65..386f4e868 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media.h +++ b/Telegram/SourceFiles/history/view/media/history_view_media.h @@ -101,6 +101,9 @@ public: [[nodiscard]] virtual bool hideServiceText() const { return false; } + [[nodiscard]] virtual bool hideFromName() const { + return false; + } [[nodiscard]] virtual bool allowsFastShare() const { return false; } diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.cpp b/Telegram/SourceFiles/payments/payments_checkout_process.cpp index 937358f1f..b9e1b631d 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.cpp +++ b/Telegram/SourceFiles/payments/payments_checkout_process.cpp @@ -38,6 +38,7 @@ namespace { struct SessionProcesses { base::flat_map> byItem; base::flat_map> bySlug; + base::flat_map> byRandomId; base::flat_map paymentStartedByItem; base::flat_map paymentStartedBySlug; rpl::lifetime lifetime; @@ -118,6 +119,28 @@ void CheckoutProcess::Start( j->second->requestActivate(); } +void CheckoutProcess::Start( + InvoicePremiumGiftCode giftCodeInvoice, + Fn reactivate) { + const auto randomId = giftCodeInvoice.randomId; + auto id = InvoiceId{ std::move(giftCodeInvoice) }; + auto &processes = LookupSessionProcesses(SessionFromId(id)); + const auto i = processes.byRandomId.find(randomId); + if (i != end(processes.byRandomId)) { + i->second->setReactivateCallback(std::move(reactivate)); + i->second->requestActivate(); + return; + } + const auto j = processes.byRandomId.emplace( + randomId, + std::make_unique( + std::move(id), + Mode::Payment, + std::move(reactivate), + PrivateTag{})).first; + j->second->requestActivate(); +} + std::optional CheckoutProcess::InvoicePaid( not_null item) { const auto session = &item->history()->session(); @@ -139,7 +162,8 @@ std::optional CheckoutProcess::InvoicePaid( } else if (i->second.paymentStartedByItem.empty() && i->second.byItem.empty() && i->second.paymentStartedBySlug.empty() - && i->second.bySlug.empty()) { + && i->second.bySlug.empty() + && i->second.byRandomId.empty()) { Processes.erase(i); } return result; @@ -165,7 +189,8 @@ std::optional CheckoutProcess::InvoicePaid( } else if (i->second.paymentStartedByItem.empty() && i->second.byItem.empty() && i->second.paymentStartedBySlug.empty() - && i->second.bySlug.empty()) { + && i->second.bySlug.empty() + && i->second.byRandomId.empty()) { Processes.erase(i); } return result; @@ -192,6 +217,11 @@ void CheckoutProcess::RegisterPaymentStart( return; } } + for (const auto &[randomId, itemProcess] : i->second.byRandomId) { + if (itemProcess.get() == process) { + return; + } + } } void CheckoutProcess::UnregisterPaymentStart( @@ -212,10 +242,16 @@ void CheckoutProcess::UnregisterPaymentStart( break; } } + for (const auto &[randomId, itemProcess] : i->second.byRandomId) { + if (itemProcess.get() == process) { + break; + } + } if (i->second.paymentStartedByItem.empty() && i->second.byItem.empty() && i->second.paymentStartedBySlug.empty() - && i->second.bySlug.empty()) { + && i->second.bySlug.empty() + && i->second.byRandomId.empty()) { Processes.erase(i); } } @@ -497,8 +533,16 @@ void CheckoutProcess::close() { if (k != end(entry.bySlug)) { entry.bySlug.erase(k); } + const auto l = ranges::find( + entry.byRandomId, + this, + [](const auto &pair) { return pair.second.get(); }); + if (l != end(entry.byRandomId)) { + entry.byRandomId.erase(l); + } if (entry.byItem.empty() && entry.bySlug.empty() + && i->second.byRandomId.empty() && entry.paymentStartedByItem.empty() && entry.paymentStartedBySlug.empty()) { Processes.erase(i); diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.h b/Telegram/SourceFiles/payments/payments_checkout_process.h index 5aa050259..84c0624cc 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.h +++ b/Telegram/SourceFiles/payments/payments_checkout_process.h @@ -37,6 +37,7 @@ class Form; struct FormUpdate; struct Error; struct InvoiceId; +struct InvoicePremiumGiftCode; enum class Mode { Payment, @@ -68,6 +69,9 @@ public: not_null session, const QString &slug, Fn reactivate); + static void Start( + InvoicePremiumGiftCode giftCodeInvoice, + Fn reactivate); [[nodiscard]] static std::optional InvoicePaid( not_null item); [[nodiscard]] static std::optional InvoicePaid( diff --git a/Telegram/SourceFiles/payments/payments_form.cpp b/Telegram/SourceFiles/payments/payments_form.cpp index 62682b9db..37bb927a8 100644 --- a/Telegram/SourceFiles/payments/payments_form.cpp +++ b/Telegram/SourceFiles/payments/payments_form.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "payments/payments_form.h" #include "main/main_session.h" +#include "data/data_channel.h" #include "data/data_session.h" #include "data/data_media_types.h" #include "data/data_user.h" @@ -112,10 +113,21 @@ constexpr auto kPasswordPeriod = 15 * TimeId(60); } // namespace not_null SessionFromId(const InvoiceId &id) { - if (const auto slug = std::get_if(&id.value)) { + if (const auto message = std::get_if(&id.value)) { + return &message->peer->session(); + } else if (const auto slug = std::get_if(&id.value)) { return slug->session; } - return &v::get(id.value).peer->session(); + const auto &giftCode = v::get(id.value); + const auto users = std::get_if( + &giftCode.purpose); + if (users) { + Assert(!users->users.empty()); + return &users->users.front()->session(); + } + const auto &giveaway = v::get( + giftCode.purpose); + return &giveaway.boostPeer->session(); } Form::Form(InvoiceId id, bool receipt) @@ -207,11 +219,10 @@ void Form::loadThumbnail(not_null photo) { } Data::FileOrigin Form::thumbnailFileOrigin() const { - if (const auto slug = std::get_if(&_id.value)) { - return Data::FileOrigin(); + if (const auto message = std::get_if(&_id.value)) { + return FullMsgId(message->peer->id, message->itemId); } - const auto message = v::get(_id.value); - return FullMsgId(message.peer->id, message.itemId); + return Data::FileOrigin(); } QImage Form::prepareGoodThumbnail( @@ -257,13 +268,67 @@ QImage Form::prepareEmptyThumbnail() const { } MTPInputInvoice Form::inputInvoice() const { - if (const auto slug = std::get_if(&_id.value)) { + if (const auto message = std::get_if(&_id.value)) { + return MTP_inputInvoiceMessage( + message->peer->input, + MTP_int(message->itemId.bare)); + } else if (const auto slug = std::get_if(&_id.value)) { return MTP_inputInvoiceSlug(MTP_string(slug->slug)); } - const auto message = v::get(_id.value); - return MTP_inputInvoiceMessage( - message.peer->input, - MTP_int(message.itemId.bare)); + const auto &giftCode = v::get(_id.value); + using Flag = MTPDpremiumGiftCodeOption::Flag; + const auto option = MTP_premiumGiftCodeOption( + MTP_flags((giftCode.storeQuantity ? Flag::f_store_quantity : Flag()) + | (giftCode.storeProduct.isEmpty() + ? Flag() + : Flag::f_store_product)), + MTP_int(giftCode.users), + MTP_int(giftCode.months), + MTP_string(giftCode.storeProduct), + MTP_int(giftCode.storeQuantity), + MTP_string(giftCode.currency), + MTP_long(giftCode.amount)); + const auto users = std::get_if( + &giftCode.purpose); + if (users) { + using Flag = MTPDinputStorePaymentPremiumGiftCode::Flag; + return MTP_inputInvoicePremiumGiftCode( + MTP_inputStorePaymentPremiumGiftCode( + MTP_flags(users->boostPeer ? Flag::f_boost_peer : Flag()), + MTP_vector(ranges::views::all( + users->users + ) | ranges::views::transform([](not_null user) { + return MTPInputUser(user->inputUser); + }) | ranges::to), + users->boostPeer ? users->boostPeer->input : MTPInputPeer(), + MTP_string(giftCode.currency), + MTP_long(giftCode.amount)), + option); + } else { + const auto &giveaway = v::get( + giftCode.purpose); + using Flag = MTPDinputStorePaymentPremiumGiveaway::Flag; + return MTP_inputInvoicePremiumGiftCode( + MTP_inputStorePaymentPremiumGiveaway( + MTP_flags(Flag() + | (giveaway.onlyNewSubscribers + ? Flag::f_only_new_subscribers + : Flag()) + | (giveaway.additionalChannels.empty() + ? Flag() + : Flag::f_additional_peers)), + giveaway.boostPeer->input, + MTP_vector(ranges::views::all( + giveaway.additionalChannels + ) | ranges::views::transform([](not_null c) { + return MTPInputPeer(c->input); + }) | ranges::to()), + MTP_long(giftCode.randomId), + MTP_int(giveaway.untilDate), + MTP_string(giftCode.currency), + MTP_long(giftCode.amount)), + option); + } } void Form::requestForm() { diff --git a/Telegram/SourceFiles/payments/payments_form.h b/Telegram/SourceFiles/payments/payments_form.h index b1e1797c4..ee7850c16 100644 --- a/Telegram/SourceFiles/payments/payments_form.h +++ b/Telegram/SourceFiles/payments/payments_form.h @@ -186,8 +186,34 @@ struct InvoiceSlug { QString slug; }; +struct InvoicePremiumGiftCodeGiveaway { + not_null boostPeer; + std::vector> additionalChannels; + TimeId untilDate = 0; + bool onlyNewSubscribers = false; +}; + +struct InvoicePremiumGiftCodeUsers { + std::vector> users; + ChannelData *boostPeer = nullptr; +}; + +struct InvoicePremiumGiftCode { + std::variant< + InvoicePremiumGiftCodeUsers, + InvoicePremiumGiftCodeGiveaway> purpose; + + uint64 randomId = 0; + QString currency; + uint64 amount = 0; + QString storeProduct; + int storeQuantity = 0; + int users = 0; + int months = 0; +}; + struct InvoiceId { - std::variant value; + std::variant value; }; [[nodiscard]] not_null SessionFromId(const InvoiceId &id); diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 2b16826ce..80205783f 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -932,9 +932,16 @@ storyMentionReadSkipTwice: 7px; storyMentionReadStrokeTwice: 3px; storyMentionButtonSkip: 5px; -chatGiveawayPrizesTop: 4px; +chatGiveawayWidth: 292px; +chatGiveawayStickerTop: -16px; +chatGiveawayPrizesTop: 16px; chatGiveawayPrizesSkip: 4px; chatGiveawayParticipantsTop: 16px; chatGiveawayParticipantsSkip: 4px; -chatGiveawayDateTop: 16px; +chatGiveawayChannelTop: 6px; +chatGiveawayChannelSize: 32px; +chatGiveawayChannelPadding: margins(5px, 7px, 12px, 0px); +chatGiveawayChannelSkip: 8px; +chatGiveawayDateTop: 6px; chatGiveawayDateSkip: 4px; +chatGiveawayBottomSkip: 16px;