/* 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 "boxes/star_gift_box.h" #include "apiwrap.h" #include "api/api_credits.h" #include "api/api_premium.h" #include "base/event_filter.h" #include "base/random.h" #include "base/timer_rpl.h" #include "base/unixtime.h" #include "boxes/filters/edit_filter_chats_list.h" #include "boxes/peers/edit_peer_color_box.h" #include "boxes/gift_premium_box.h" #include "boxes/peer_list_controllers.h" #include "boxes/premium_preview_box.h" #include "boxes/send_credits_box.h" #include "chat_helpers/emoji_suggestions_widget.h" #include "chat_helpers/message_field.h" #include "chat_helpers/stickers_gift_box_pack.h" #include "chat_helpers/stickers_lottie.h" #include "chat_helpers/tabbed_panel.h" #include "chat_helpers/tabbed_selector.h" #include "core/ui_integration.h" #include "data/data_channel.h" #include "data/data_credits.h" #include "data/data_document.h" #include "data/data_document_media.h" #include "data/data_emoji_statuses.h" #include "data/data_file_origin.h" #include "data/data_peer_values.h" #include "data/data_premium_limits.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_media_generic.h" #include "history/view/media/history_view_unique_gift.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 "info/profile/info_profile_icon.h" #include "lang/lang_keys.h" #include "lottie/lottie_common.h" #include "lottie/lottie_single_player.h" #include "main/main_app_config.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.h" #include "settings/settings_credits_graphics.h" #include "settings/settings_premium.h" #include "ui/boxes/boost_box.h" #include "ui/chat/chat_style.h" #include "ui/chat/chat_theme.h" #include "ui/controls/emoji_button.h" #include "ui/controls/userpic_button.h" #include "ui/effects/path_shift_gradient.h" #include "ui/effects/premium_graphics.h" #include "ui/effects/premium_stars_colored.h" #include "ui/layers/generic_box.h" #include "ui/new_badges.h" #include "ui/painter.h" #include "ui/rect.h" #include "ui/text/format_values.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" #include "ui/ui_utility.h" #include "ui/vertical_list.h" #include "ui/widgets/fields/input_field.h" #include "ui/widgets/buttons.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/popup_menu.h" #include "ui/widgets/shadow.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_chat.h" #include "styles/style_chat_helpers.h" #include "styles/style_credits.h" #include "styles/style_layers.h" #include "styles/style_menu_icons.h" #include "styles/style_premium.h" #include "styles/style_settings.h" #include "styles/style_widgets.h" #include namespace Ui { namespace { constexpr auto kPriceTabAll = 0; constexpr auto kPriceTabLimited = -2; constexpr auto kPriceTabInStock = -1; constexpr auto kGiftMessageLimit = 255; constexpr auto kSentToastDuration = 3 * crl::time(1000); constexpr auto kSwitchUpgradeCoverInterval = 3 * crl::time(1000); constexpr auto kCrossfadeDuration = crl::time(400); constexpr auto kUpgradeDoneToastDuration = 4 * crl::time(1000); using namespace HistoryView; using namespace Info::PeerGifts; struct PremiumGiftsDescriptor { std::vector list; std::shared_ptr api; }; struct GiftsDescriptor { std::vector list; std::shared_ptr api; }; struct GiftDetails { GiftDescriptor descriptor; TextWithEntities text; uint64 randomId = 0; bool anonymous = false; bool upgraded = false; }; class PreviewDelegate final : public DefaultElementDelegate { public: PreviewDelegate( not_null parent, not_null st, Fn update); bool elementAnimationsPaused() override; not_null elementPathShiftGradient() override; Context elementContext() override; private: const not_null _parent; const std::unique_ptr _pathGradient; }; class PreviewWrap final : public RpWidget { public: PreviewWrap( not_null parent, not_null recipient, rpl::producer details); ~PreviewWrap(); private: void paintEvent(QPaintEvent *e) override; void resizeTo(int width); void prepare(rpl::producer details); const not_null _history; const not_null _recipient; const std::unique_ptr _theme; const std::unique_ptr _style; const std::unique_ptr _delegate; AdminLog::OwnedItem _item; QPoint _position; }; [[nodiscard]] bool SortForBirthday(not_null peer) { const auto user = peer->asUser(); if (!user) { return false; } const auto birthday = user->birthday(); if (!birthday) { return false; } const auto is = [&](const QDate &date) { return (date.day() == birthday.day()) && (date.month() == birthday.month()); }; const auto now = QDate::currentDate(); return is(now) || is(now.addDays(1)) || is(now.addDays(-1)); } [[nodiscard]] bool IsSoldOut(const Data::StarGift &info) { return info.limitedCount && !info.limitedLeft; } PreviewDelegate::PreviewDelegate( not_null parent, not_null st, Fn update) : _parent(parent) , _pathGradient(MakePathShiftGradient(st, update)) { } bool PreviewDelegate::elementAnimationsPaused() { return _parent->window()->isActiveWindow(); } auto PreviewDelegate::elementPathShiftGradient() -> not_null { return _pathGradient.get(); } Context PreviewDelegate::elementContext() { return Context::History; } auto GenerateGiftMedia( not_null parent, Element *replacing, not_null recipient, const GiftDetails &data) -> Fn, Fn)>)> { return [=]( not_null media, Fn)> push) { const auto &descriptor = data.descriptor; auto pushText = [&]( TextWithEntities text, QMargins margins = {}, const base::flat_map &links = {}, const std::any &context = {}) { if (text.empty()) { return; } push(std::make_unique( std::move(text), margins, st::defaultTextStyle, links, context)); }; const auto sticker = [=] { using Tag = ChatHelpers::StickerLottieSize; const auto session = &parent->history()->session(); const auto sticker = LookupGiftSticker(session, descriptor); return StickerInBubblePart::Data{ .sticker = sticker, .size = st::chatIntroStickerSize, .cacheTag = Tag::ChatIntroHelloSticker, .singleTimePlayback = v::is(descriptor), }; }; push(std::make_unique( parent, replacing, sticker, st::giftBoxPreviewStickerPadding)); auto title = v::match(descriptor, [&](GiftTypePremium gift) { return tr::lng_action_gift_premium_months( tr::now, lt_count, gift.months, Text::Bold); }, [&](const GiftTypeStars &gift) { return recipient->isSelf() ? tr::lng_action_gift_self_subtitle(tr::now, Text::Bold) : tr::lng_action_gift_got_subtitle( tr::now, lt_user, TextWithEntities() .append(Text::SingleCustomEmoji( recipient->owner().customEmojiManager( ).peerUserpicEmojiData( recipient->session().user()))) .append(' ') .append(recipient->session().user()->shortName()), Text::Bold); }); auto textFallback = v::match(descriptor, [&](GiftTypePremium gift) { return tr::lng_action_gift_premium_about( tr::now, Text::RichLangValue); }, [&](const GiftTypeStars &gift) { return data.upgraded ? tr::lng_action_gift_got_upgradable_text( tr::now, Text::RichLangValue) : (recipient->isSelf() && gift.info.starsToUpgrade) ? tr::lng_action_gift_self_about_unique( tr::now, Text::RichLangValue) : (recipient->isBroadcast() && gift.info.starsToUpgrade) ? tr::lng_action_gift_channel_about_unique( tr::now, Text::RichLangValue) : (recipient->isSelf() ? tr::lng_action_gift_self_about : recipient->isBroadcast() ? tr::lng_action_gift_channel_about : tr::lng_action_gift_got_stars_text)( tr::now, lt_count, gift.info.starsConverted, Text::RichLangValue); }); auto description = data.text.empty() ? std::move(textFallback) : data.text; const auto context = Core::MarkedTextContext{ .session = &parent->history()->session(), .customEmojiRepaint = [parent] { parent->repaint(); }, }; pushText( std::move(title), st::giftBoxPreviewTitlePadding, {}, context); pushText( std::move(description), st::giftBoxPreviewTextPadding, {}, context); push(HistoryView::MakeGenericButtonPart( (data.upgraded ? tr::lng_gift_view_unpack(tr::now) : tr::lng_sticker_premium_view(tr::now)), st::giftBoxButtonMargin, [parent] { parent->repaint(); }, nullptr)); }; } [[nodiscard]] QImage CreateGradient( QSize size, const Data::UniqueGift &gift) { const auto ratio = style::DevicePixelRatio(); auto result = QImage(size * ratio, QImage::Format_ARGB32_Premultiplied); result.setDevicePixelRatio(ratio); auto p = QPainter(&result); auto hq = PainterHighQualityEnabler(p); auto gradient = QRadialGradient( QRect(QPoint(), size).center(), size.height() / 2); gradient.setStops({ { 0., gift.backdrop.centerColor }, { 1., gift.backdrop.edgeColor }, }); p.setBrush(gradient); p.setPen(Qt::NoPen); p.drawRect(QRect(QPoint(), size)); p.end(); const auto mask = Images::CornersMask(st::boxRadius); return Images::Round(std::move(result), mask, RectPart::FullTop); } void PrepareImage( QImage &image, not_null emoji, const PatternPoint &point, const Data::UniqueGift &gift) { if (!image.isNull() || !emoji->ready()) { return; } const auto ratio = style::DevicePixelRatio(); const auto size = Emoji::GetSizeNormal() / ratio; image = QImage( 2 * QSize(size, size) * ratio, QImage::Format_ARGB32_Premultiplied); image.setDevicePixelRatio(ratio); image.fill(Qt::transparent); auto p = QPainter(&image); auto hq = PainterHighQualityEnabler(p); p.setOpacity(point.opacity); if (point.scale < 1.) { p.translate(size, size); p.scale(point.scale, point.scale); p.translate(-size, -size); } const auto shift = (2 * size - (Emoji::GetSizeLarge() / ratio)) / 2; emoji->paint(p, { .textColor = gift.backdrop.patternColor, .position = QPoint(shift, shift), }); } PreviewWrap::PreviewWrap( not_null parent, not_null recipient, rpl::producer details) : RpWidget(parent) , _history(recipient->owner().history(recipient->session().userPeerId())) , _recipient(recipient) , _theme(Window::Theme::DefaultChatThemeOn(lifetime())) , _style(std::make_unique( _history->session().colorIndicesValue())) , _delegate(std::make_unique( parent, _style.get(), [=] { update(); })) , _position(0, st::msgMargin.bottom()) { _style->apply(_theme.get()); using namespace HistoryView; _history->owner().viewRepaintRequest( ) | rpl::start_with_next([=](not_null view) { if (view == _item.get()) { update(); } }, lifetime()); _history->session().downloaderTaskFinished() | rpl::start_with_next([=] { update(); }, lifetime()); prepare(std::move(details)); } void ShowSentToast( not_null window, const GiftDescriptor &descriptor, const GiftDetails &details) { const auto &st = st::historyPremiumToast; const auto skip = st.padding.top(); const auto size = st.style.font->height * 2; const auto document = LookupGiftSticker(&window->session(), descriptor); const auto leftSkip = document ? (skip + size + skip - st.padding.left()) : 0; auto text = v::match(descriptor, [&](const GiftTypePremium &gift) { return tr::lng_action_gift_premium_about( tr::now, Text::RichLangValue); }, [&](const GiftTypeStars &gift) { const auto amount = gift.info.stars + (details.upgraded ? gift.info.starsToUpgrade : 0); return tr::lng_gift_sent_about( tr::now, lt_count, amount, Text::RichLangValue); }); const auto strong = window->showToast({ .title = tr::lng_gift_sent_title(tr::now), .text = std::move(text), .padding = rpl::single(QMargins(leftSkip, 0, 0, 0)), .st = &st, .attach = RectPart::Top, .duration = kSentToastDuration, }).get(); if (!strong || !document) { return; } const auto widget = strong->widget(); const auto preview = CreateChild(widget.get()); preview->moveToLeft(skip, skip); preview->resize(size, size); preview->show(); const auto bytes = document->createMediaView()->bytes(); const auto filepath = document->filepath(); const auto ratio = style::DevicePixelRatio(); const auto player = preview->lifetime().make_state( Lottie::ReadContent(bytes, filepath), Lottie::FrameRequest{ QSize(size, size) * ratio }, Lottie::Quality::Default); preview->paintRequest( ) | rpl::start_with_next([=] { if (!player->ready()) { return; } const auto image = player->frame(); QPainter(preview).drawImage( QRect(QPoint(), image.size() / ratio), image); if (player->frameIndex() + 1 != player->framesCount()) { player->markFrameShown(); } }, preview->lifetime()); player->updates( ) | rpl::start_with_next([=] { preview->update(); }, preview->lifetime()); } PreviewWrap::~PreviewWrap() { _item = {}; } void PreviewWrap::prepare(rpl::producer details) { std::move(details) | rpl::start_with_next([=](GiftDetails details) { const auto &descriptor = details.descriptor; const auto cost = v::match(descriptor, [&](GiftTypePremium data) { return FillAmountAndCurrency(data.cost, data.currency, true); }, [&](GiftTypeStars data) { const auto stars = data.info.stars + (details.upgraded ? data.info.starsToUpgrade : 0); return stars ? tr::lng_gift_stars_title(tr::now, lt_count, stars) : QString(); }); const auto name = _history->session().user()->shortName(); const auto text = cost.isEmpty() ? tr::lng_action_gift_unique_received(tr::now, lt_user, name) : _recipient->isSelf() ? tr::lng_action_gift_self_bought(tr::now, lt_cost, cost) : _recipient->isBroadcast() ? tr::lng_action_gift_sent_channel( tr::now, lt_user, name, lt_name, _recipient->name(), lt_cost, cost) : tr::lng_action_gift_received( tr::now, lt_user, name, lt_cost, cost); const auto item = _history->makeMessage({ .id = _history->nextNonHistoryEntryId(), .flags = (MessageFlag::FakeAboutView | MessageFlag::FakeHistoryItem | MessageFlag::Local), .from = _history->peer->id, }, PreparedServiceText{ { text } }); auto owned = AdminLog::OwnedItem(_delegate.get(), item); owned->overrideMedia(std::make_unique( owned.get(), GenerateGiftMedia(owned.get(), _item.get(), _recipient, details), MediaGenericDescriptor{ .maxWidth = st::chatIntroWidth, .service = true, })); _item = std::move(owned); if (width() >= st::msgMinWidth) { resizeTo(width()); } update(); }, lifetime()); widthValue( ) | rpl::filter([=](int width) { return width >= st::msgMinWidth; }) | rpl::start_with_next([=](int width) { resizeTo(width); }, lifetime()); _history->owner().itemResizeRequest( ) | rpl::start_with_next([=](not_null item) { if (_item && item == _item->data() && width() >= st::msgMinWidth) { resizeTo(width()); } }, lifetime()); } void PreviewWrap::resizeTo(int width) { const auto height = _position.y() + _item->resizeGetHeight(width) + _position.y() + st::msgServiceMargin.top() + st::msgServiceGiftBoxTopSkip - st::msgServiceMargin.bottom(); resize(width, height); } void PreviewWrap::paintEvent(QPaintEvent *e) { auto p = Painter(this); const auto clip = e->rect(); if (!clip.isEmpty()) { p.setClipRect(clip); Window::SectionWidget::PaintBackground( p, _theme.get(), QSize(width(), window()->height()), clip); } auto context = _theme->preparePaintContext( _style.get(), rect(), e->rect(), !window()->isActiveWindow()); p.translate(_position); _item->draw(p, context); } [[nodiscard]] rpl::producer GiftsPremium( not_null session, not_null peer) { struct Session { PremiumGiftsDescriptor last; }; static auto Map = base::flat_map, Session>(); return [=](auto consumer) { auto lifetime = rpl::lifetime(); auto i = Map.find(session); if (i == end(Map)) { i = Map.emplace(session, Session()).first; session->lifetime().add([=] { Map.remove(session); }); } if (!i->second.last.list.empty()) { consumer.put_next_copy(i->second.last); } using namespace Api; const auto api = std::make_shared(peer); api->request() | rpl::start_with_error_done([=](QString error) { consumer.put_next({}); }, [=] { const auto &options = api->optionsForPeer(); auto list = std::vector(); list.reserve(options.size()); auto minMonthsGift = GiftTypePremium(); for (const auto &option : options) { list.push_back({ .cost = option.cost, .currency = option.currency, .months = option.months, }); if (!minMonthsGift.months || option.months < minMonthsGift.months) { minMonthsGift = list.back(); } } for (auto &gift : list) { if (gift.months > minMonthsGift.months && gift.currency == minMonthsGift.currency) { const auto costPerMonth = gift.cost / (1. * gift.months); const auto maxCostPerMonth = minMonthsGift.cost / (1. * minMonthsGift.months); const auto costRatio = costPerMonth / maxCostPerMonth; const auto discount = 1. - costRatio; const auto discountPercent = 100 * discount; const auto value = int(base::SafeRound(discountPercent)); if (value > 0 && value < 100) { gift.discountPercent = value; } } } ranges::sort(list, ranges::less(), &GiftTypePremium::months); auto &map = Map[session]; if (map.last.list != list) { map.last = PremiumGiftsDescriptor{ std::move(list), api, }; consumer.put_next_copy(map.last); } }, lifetime); return lifetime; }; } [[nodiscard]] rpl::producer> GiftsStars( not_null session, not_null peer) { struct Session { std::vector last; }; static auto Map = base::flat_map, Session>(); return [=](auto consumer) { auto lifetime = rpl::lifetime(); auto i = Map.find(session); if (i == end(Map)) { i = Map.emplace(session, Session()).first; session->lifetime().add([=] { Map.remove(session); }); } if (!i->second.last.empty()) { consumer.put_next_copy(i->second.last); } using namespace Api; const auto api = lifetime.make_state(peer); api->requestStarGifts( ) | rpl::start_with_error_done([=](QString error) { consumer.put_next({}); }, [=] { auto list = std::vector(); const auto &gifts = api->starGifts(); list.reserve(gifts.size()); for (auto &gift : gifts) { list.push_back({ .info = gift }); } ranges::sort(list, []( const GiftTypeStars &a, const GiftTypeStars &b) { if (!a.info.limitedCount && !b.info.limitedCount) { return a.info.stars <= b.info.stars; } else if (!a.info.limitedCount) { return true; } else if (!b.info.limitedCount) { return false; } else if (a.info.limitedLeft != b.info.limitedLeft) { return a.info.limitedLeft > b.info.limitedLeft; } return a.info.stars <= b.info.stars; }); auto &map = Map[session]; if (map.last != list) { map.last = list; consumer.put_next_copy(list); } }, lifetime); return lifetime; }; } [[nodiscard]] Text::String TabTextForPrice( not_null session, int price) { const auto simple = [](const QString &text) { return Text::String(st::semiboldTextStyle, text); }; if (price == kPriceTabAll) { return simple(tr::lng_gift_stars_tabs_all(tr::now)); } else if (price == kPriceTabLimited) { return simple(tr::lng_gift_stars_tabs_limited(tr::now)); } else if (price == kPriceTabInStock) { return simple(tr::lng_gift_stars_tabs_in_stock(tr::now)); } auto &manager = session->data().customEmojiManager(); auto result = Text::String(); const auto context = Core::MarkedTextContext{ .session = session, .customEmojiRepaint = [] {}, }; result.setMarkedText( st::semiboldTextStyle, manager.creditsEmoji().append(QString::number(price)), kMarkupTextOptions, context); return result; } struct GiftPriceTabs { rpl::producer priceTab; object_ptr widget; }; [[nodiscard]] GiftPriceTabs MakeGiftsPriceTabs( not_null window, not_null peer, rpl::producer> gifts) { auto widget = object_ptr((QWidget*)nullptr); const auto raw = widget.data(); struct Button { QRect geometry; Text::String text; int price = 0; bool active = false; }; struct State { rpl::variable> prices; rpl::variable priceTab = kPriceTabAll; rpl::variable fullWidth; std::vector