/* 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_global_privacy.h" #include "api/api_premium.h" #include "api/api_text_entities.h" //#include "base/call_delayed.h" #include "base/event_filter.h" #include "base/qt_signal_producer.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/peers/prepare_short_info_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 "boxes/star_gift_auction_box.h" #include "boxes/star_gift_resale_box.h" #include "boxes/transfer_gift_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/application.h" #include "core/click_handler_types.h" #include "core/ui_integration.h" #include "data/components/gift_auctions.h" #include "data/components/promo_suggestions.h" #include "data/data_birthday.h" #include "data/data_changes.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/controls/history_view_suggest_options.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/channel_statistics/boosts/giveaway/boost_badge.h" // InfiniteRadialAnimationWidget. #include "info/channel_statistics/earn/earn_icons.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 "menu/gift_resale_filter.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/boxes/confirm_box.h" #include "ui/chat/chat_style.h" #include "ui/chat/chat_theme.h" #include "ui/controls/button_labels.h" #include "ui/controls/emoji_button.h" #include "ui/controls/ton_common.h" #include "ui/controls/userpic_button.h" #include "ui/effects/path_shift_gradient.h" #include "ui/effects/premium_bubble.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/new_badges.h" #include "ui/painter.h" #include "ui/rect.h" #include "ui/text/custom_emoji_helper.h" #include "ui/text/format_values.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" #include "ui/top_background_gradient.h" #include "ui/ui_utility.h" #include "ui/vertical_list.h" #include "ui/widgets/fields/input_field.h" #include "ui/widgets/fields/number_input.h" #include "ui/widgets/buttons.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/popup_menu.h" #include "ui/widgets/shadow.h" #include "ui/wrap/fade_wrap.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/table_layout.h" #include "window/themes/window_theme.h" #include "window/section_widget.h" #include "window/window_controller.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_giveaway.h" #include "styles/style_info.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 kPriceTabMy = -1; constexpr auto kPriceTabCollectibles = -2; 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); constexpr auto kGiftsPreloadTimeout = 3 * crl::time(1000); constexpr auto kResellPriceCacheLifetime = 60 * crl::time(1000); constexpr auto kGradientButtonBgOpacity = 0.6; constexpr auto kSpinnerBackdrops = 6; constexpr auto kSpinnerPatterns = 6; constexpr auto kSpinnerModels = 6; constexpr auto kBackdropSpinDuration = crl::time(300); constexpr auto kBackdropStopsAt = crl::time(2.5 * 1000); constexpr auto kPatternSpinDuration = crl::time(600); constexpr auto kPatternStopsAt = crl::time(4 * 1000); constexpr auto kModelSpinDuration = crl::time(160); constexpr auto kModelStopsAt = crl::time(5.5 * 1000); constexpr auto kModelScaleFrom = 0.7; using namespace HistoryView; using namespace Info::PeerGifts; using Data::GiftAttributeId; using Data::GiftAttributeIdType; using Data::ResaleGiftsSort; using Data::ResaleGiftsFilter; using Data::ResaleGiftsDescriptor; using Data::MyGiftsDescriptor; enum class PickType { Activate, SendMessage, OpenProfile, }; using PickCallback = Fn, PickType)>; struct PremiumGiftsDescriptor { std::vector list; std::shared_ptr api; }; struct SessionResalePrices { explicit SessionResalePrices(not_null session) : api(std::make_unique(session->user())) { } std::unique_ptr api; base::flat_map prices; std::vector> waiting; rpl::lifetime requestLifetime; crl::time lastReceived = 0; }; [[nodiscard]] not_null ResalePrices( not_null session) { static auto result = base::flat_map< not_null, std::unique_ptr>(); if (const auto i = result.find(session); i != end(result)) { return i->second.get(); } const auto i = result.emplace( session, std::make_unique(session)).first; session->lifetime().add([session] { result.remove(session); }); return i->second.get(); } class PeerRow final : public PeerListRow { public: using PeerListRow::PeerListRow; QSize rightActionSize() const override; QMargins rightActionMargins() const override; void rightActionPaint( Painter &p, int x, int y, int outerWidth, bool selected, bool actionSelected) override; void rightActionAddRipple( QPoint point, Fn updateCallback) override; void rightActionStopLastRipple() override; private: std::unique_ptr _actionRipple; }; QSize PeerRow::rightActionSize() const { return QSize( st::inviteLinkThreeDotsIcon.width(), st::inviteLinkThreeDotsIcon.height()); } QMargins PeerRow::rightActionMargins() const { return QMargins( 0, (st::inviteLinkList.item.height - rightActionSize().height()) / 2, st::inviteLinkThreeDotsSkip, 0); } void PeerRow::rightActionPaint( Painter &p, int x, int y, int outerWidth, bool selected, bool actionSelected) { if (_actionRipple) { _actionRipple->paint(p, x, y, outerWidth); if (_actionRipple->empty()) { _actionRipple.reset(); } } (actionSelected ? st::inviteLinkThreeDotsIconOver : st::inviteLinkThreeDotsIcon).paint(p, x, y, outerWidth); } void PeerRow::rightActionAddRipple(QPoint point, Fn updateCallback) { if (!_actionRipple) { auto mask = Ui::RippleAnimation::EllipseMask( Size(st::inviteLinkThreeDotsIcon.height())); _actionRipple = std::make_unique( st::defaultRippleAnimation, std::move(mask), std::move(updateCallback)); } _actionRipple->add(point); } void PeerRow::rightActionStopLastRipple() { if (_actionRipple) { _actionRipple->lastStop(); } } 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; }; class TextBubblePart final : public MediaGenericTextPart { public: TextBubblePart( TextWithEntities text, QMargins margins, const style::TextStyle &st = st::defaultTextStyle, const base::flat_map &links = {}, const Ui::Text::MarkedContext &context = {}, style::align align = style::al_top); void draw( Painter &p, not_null owner, const PaintContext &context, int outerWidth) const override; private: void setupPen( Painter &p, not_null owner, const PaintContext &context) const override; int elisionLines() const override; }; TextBubblePart::TextBubblePart( TextWithEntities text, QMargins margins, const style::TextStyle &st, const base::flat_map &links, const Ui::Text::MarkedContext &context, style::align align) : MediaGenericTextPart(std::move(text), margins, st, links, context, align) { } void TextBubblePart::draw( Painter &p, not_null owner, const PaintContext &context, int outerWidth) const { p.setPen(Qt::NoPen); p.setBrush(context.st->msgServiceBg()); const auto radius = height() / 2.; const auto left = (outerWidth - width()) / 2; const auto r = QRect(left, 0, width(), height()); p.drawRoundedRect(r, radius, radius); MediaGenericTextPart::draw(p, owner, context, outerWidth); } void TextBubblePart::setupPen( Painter &p, not_null owner, const PaintContext &context) const { auto pen = context.st->msgServiceFg()->c; pen.setAlphaF(pen.alphaF() * 0.65); p.setPen(pen); } int TextBubblePart::elisionLines() const { return 1; } [[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; } struct UpgradePrice { TimeId date = 0; int stars = 0; }; [[nodiscard]] std::vector ParsePrices( const MTPVector &list) { return ranges::views::all( list.v ) | ranges::views::transform([](const MTPStarGiftUpgradePrice &price) { const auto &data = price.data(); return UpgradePrice{ .date = data.vdate().v, .stars = int(data.vupgrade_stars().v), }; }) | ranges::to_vector; } 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 GiftSendDetails &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 = {}, Ui::Text::MarkedContext context = {}) { if (text.empty()) { return; } push(std::make_unique( std::move(text), margins, st::defaultTextStyle, links, std::move(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, .stopOnLastFrame = 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, tr::bold); }, [&](const GiftTypeStars &gift) { return recipient->isSelf() ? tr::lng_action_gift_self_subtitle(tr::now, tr::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()), tr::bold); }); auto textFallback = v::match(descriptor, [&](GiftTypePremium gift) { return tr::lng_action_gift_premium_about( tr::now, tr::rich); }, [&](const GiftTypeStars &gift) { return data.upgraded ? tr::lng_action_gift_got_upgradable_text(tr::now, tr::rich) : (recipient->isSelf() && gift.info.starsToUpgrade) ? tr::lng_action_gift_self_about_unique(tr::now, tr::rich) : (recipient->isBroadcast() && gift.info.starsToUpgrade) ? tr::lng_action_gift_channel_about_unique(tr::now, tr::rich) : gift.info.auction() ? (recipient->isBroadcast() ? tr::lng_action_gift_got_gift_channel(tr::now, tr::rich) : tr::lng_action_gift_got_gift_text(tr::now, tr::rich)) : (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, tr::rich); }); auto description = data.text.empty() ? std::move(textFallback) : data.text; const auto context = Core::TextContext({ .session = &parent->history()->session(), .repaint = [parent] { parent->repaint(); }, }); pushText( std::move(title), st::giftBoxPreviewTitlePadding, {}, context); if (v::is(descriptor)) { const auto &stars = v::get(descriptor); if (const auto by = stars.info.releasedBy) { push(std::make_unique( tr::lng_gift_released_by( tr::now, lt_name, tr::link('@' + by->username()), tr::marked), st::giftBoxReleasedByMargin, st::uniqueGiftReleasedBy.style)); } } 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)); }; } 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::on_next([=](not_null view) { if (view == _item.get()) { update(); } }, lifetime()); _history->session().downloaderTaskFinished() | rpl::on_next([=] { update(); }, lifetime()); prepare(std::move(details)); } void ShowSentToast( not_null window, const GiftDescriptor &descriptor, const GiftSendDetails &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, tr::rich); }, [&](const GiftTypeStars &gift) { if (gift.info.perUserTotal && gift.info.perUserRemains < 2) { return tr::lng_gift_sent_finished( tr::now, lt_count, gift.info.perUserTotal, tr::rich); } else if (gift.info.perUserTotal) { return tr::lng_gift_sent_remains( tr::now, lt_count, gift.info.perUserRemains - 1, tr::rich); } const auto amount = gift.info.stars + (details.upgraded ? gift.info.starsToUpgrade : 0); return tr::lng_gift_sent_about( tr::now, lt_count, amount, tr::rich); }); 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::on_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::on_next([=] { preview->update(); }, preview->lifetime()); } PreviewWrap::~PreviewWrap() { _item = {}; } void PreviewWrap::prepare(rpl::producer details) { std::move(details) | rpl::on_next([=](GiftSendDetails details) { const auto &descriptor = details.descriptor; const auto cost = v::match(descriptor, [&](GiftTypePremium data) { const auto stars = (details.byStars && data.stars) ? data.stars : (data.currency == kCreditsCurrency) ? data.cost : 0; return stars ? tr::lng_gift_stars_title(tr::now, lt_count, stars) : 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::chatGiftPreviewWidth, .service = true, })); _item = std::move(owned); if (width() >= st::msgMinWidth) { resizeTo(width()); } update(); }, lifetime()); widthValue( ) | rpl::filter([=](int width) { return width >= st::msgMinWidth; }) | rpl::on_next([=](int width) { resizeTo(width); }, lifetime()); _history->owner().itemResizeRequest( ) | rpl::on_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::on_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) { if (option.currency != kCreditsCurrency) { list.push_back({ .cost = option.cost, .currency = option.currency, .months = option.months, }); if (!minMonthsGift.months || option.months < minMonthsGift.months) { minMonthsGift = list.back(); } } } for (const auto &option : options) { if (option.currency == kCreditsCurrency) { const auto i = ranges::find( list, option.months, &GiftTypePremium::months); if (i != end(list)) { i->stars = option.cost; } } } 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 || list.empty()) { map.last = PremiumGiftsDescriptor{ std::move(list), api, }; consumer.put_next_copy(map.last); } }, lifetime); return lifetime; }; } [[nodiscard]] Text::String TabTextForPrice(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 == kPriceTabMy) { return simple(tr::lng_gift_stars_tabs_my(tr::now)); } else if (price == kPriceTabCollectibles) { return simple(tr::lng_gift_stars_tabs_collectibles(tr::now)); } return {}; } struct GiftPriceTabs { rpl::producer priceTab; object_ptr widget; }; [[nodiscard]] GiftPriceTabs MakeGiftsPriceTabs( not_null peer, rpl::producer> gifts, bool hasMyUnique) { 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