AyuGramDesktop/Telegram/SourceFiles/boxes/star_gift_box.cpp
2025-02-10 10:46:48 +04:00

2976 lines
84 KiB
C++

/*
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 <QtWidgets/QApplication>
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<GiftTypePremium> list;
std::shared_ptr<Api::PremiumGiftCodeOptions> api;
};
struct GiftsDescriptor {
std::vector<GiftDescriptor> list;
std::shared_ptr<Api::PremiumGiftCodeOptions> 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<QWidget*> parent,
not_null<ChatStyle*> st,
Fn<void()> update);
bool elementAnimationsPaused() override;
not_null<PathShiftGradient*> elementPathShiftGradient() override;
Context elementContext() override;
private:
const not_null<QWidget*> _parent;
const std::unique_ptr<PathShiftGradient> _pathGradient;
};
class PreviewWrap final : public RpWidget {
public:
PreviewWrap(
not_null<QWidget*> parent,
not_null<PeerData*> recipient,
rpl::producer<GiftDetails> details);
~PreviewWrap();
private:
void paintEvent(QPaintEvent *e) override;
void resizeTo(int width);
void prepare(rpl::producer<GiftDetails> details);
const not_null<History*> _history;
const not_null<PeerData*> _recipient;
const std::unique_ptr<ChatTheme> _theme;
const std::unique_ptr<ChatStyle> _style;
const std::unique_ptr<PreviewDelegate> _delegate;
AdminLog::OwnedItem _item;
QPoint _position;
};
[[nodiscard]] bool SortForBirthday(not_null<PeerData*> 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<QWidget*> parent,
not_null<ChatStyle*> st,
Fn<void()> update)
: _parent(parent)
, _pathGradient(MakePathShiftGradient(st, update)) {
}
bool PreviewDelegate::elementAnimationsPaused() {
return _parent->window()->isActiveWindow();
}
auto PreviewDelegate::elementPathShiftGradient()
-> not_null<PathShiftGradient*> {
return _pathGradient.get();
}
Context PreviewDelegate::elementContext() {
return Context::History;
}
auto GenerateGiftMedia(
not_null<Element*> parent,
Element *replacing,
not_null<PeerData*> recipient,
const GiftDetails &data)
-> Fn<void(
not_null<MediaGeneric*>,
Fn<void(std::unique_ptr<MediaGenericPart>)>)> {
return [=](
not_null<MediaGeneric*> media,
Fn<void(std::unique_ptr<MediaGenericPart>)> push) {
const auto &descriptor = data.descriptor;
auto pushText = [&](
TextWithEntities text,
QMargins margins = {},
const base::flat_map<uint16, ClickHandlerPtr> &links = {},
const std::any &context = {}) {
if (text.empty()) {
return;
}
push(std::make_unique<MediaGenericTextPart>(
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<GiftTypePremium>(descriptor),
};
};
push(std::make_unique<StickerInBubblePart>(
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<Text::CustomEmoji*> 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<QWidget*> parent,
not_null<PeerData*> recipient,
rpl::producer<GiftDetails> details)
: RpWidget(parent)
, _history(recipient->owner().history(recipient->session().userPeerId()))
, _recipient(recipient)
, _theme(Window::Theme::DefaultChatThemeOn(lifetime()))
, _style(std::make_unique<ChatStyle>(
_history->session().colorIndicesValue()))
, _delegate(std::make_unique<PreviewDelegate>(
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<const Element*> 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::SessionController*> 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<RpWidget>(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::SinglePlayer>(
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<GiftDetails> 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<MediaGeneric>(
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<const HistoryItem*> 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<PremiumGiftsDescriptor> GiftsPremium(
not_null<Main::Session*> session,
not_null<PeerData*> peer) {
struct Session {
PremiumGiftsDescriptor last;
};
static auto Map = base::flat_map<not_null<Main::Session*>, 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<PremiumGiftCodeOptions>(peer);
api->request() | rpl::start_with_error_done([=](QString error) {
consumer.put_next({});
}, [=] {
const auto &options = api->optionsForPeer();
auto list = std::vector<GiftTypePremium>();
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<std::vector<GiftTypeStars>> GiftsStars(
not_null<Main::Session*> session,
not_null<PeerData*> peer) {
struct Session {
std::vector<GiftTypeStars> last;
};
static auto Map = base::flat_map<not_null<Main::Session*>, 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<PremiumGiftCodeOptions>(peer);
api->requestStarGifts(
) | rpl::start_with_error_done([=](QString error) {
consumer.put_next({});
}, [=] {
auto list = std::vector<GiftTypeStars>();
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<Main::Session*> 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<int> priceTab;
object_ptr<RpWidget> widget;
};
[[nodiscard]] GiftPriceTabs MakeGiftsPriceTabs(
not_null<Window::SessionController*> window,
not_null<PeerData*> peer,
rpl::producer<std::vector<GiftTypeStars>> gifts) {
auto widget = object_ptr<RpWidget>((QWidget*)nullptr);
const auto raw = widget.data();
struct Button {
QRect geometry;
Text::String text;
int price = 0;
bool active = false;
};
struct State {
rpl::variable<std::vector<int>> prices;
rpl::variable<int> priceTab = kPriceTabAll;
rpl::variable<int> fullWidth;
std::vector<Button> buttons;
int dragx = 0;
int pressx = 0;
float64 dragscroll = 0.;
float64 scroll = 0.;
int scrollMax = 0;
int selected = -1;
int pressed = -1;
int active = -1;
};
const auto state = raw->lifetime().make_state<State>();
const auto scroll = [=] {
return QPoint(int(base::SafeRound(state->scroll)), 0);
};
state->prices = std::move(
gifts
) | rpl::map([](const std::vector<GiftTypeStars> &gifts) {
auto result = std::vector<int>();
result.push_back(kPriceTabAll);
auto special = 1;
auto same = true;
auto sameKey = 0;
auto hasNonSoldOut = false;
auto hasSoldOut = false;
auto hasLimited = false;
for (const auto &gift : gifts) {
if (same) {
const auto key = gift.info.stars
* (gift.info.limitedCount ? -1 : 1);
if (!sameKey) {
sameKey = key;
} else if (sameKey != key) {
same = false;
}
}
if (IsSoldOut(gift.info)) {
hasSoldOut = true;
} else {
hasNonSoldOut = true;
}
if (gift.info.limitedCount) {
hasLimited = true;
}
if (!ranges::contains(result, gift.info.stars)) {
result.push_back(gift.info.stars);
}
}
if (same) {
return std::vector<int>();
}
if (hasSoldOut && hasNonSoldOut) {
result.insert(begin(result) + (special++), kPriceTabInStock);
}
if (hasLimited) {
result.insert(begin(result) + (special++), kPriceTabLimited);
}
ranges::sort(begin(result) + 1, end(result));
return result;
});
const auto setSelected = [=](int index) {
const auto was = (state->selected >= 0);
const auto now = (index >= 0);
state->selected = index;
if (was != now) {
raw->setCursor(now ? style::cur_pointer : style::cur_default);
}
};
const auto setActive = [=](int index) {
const auto was = state->active;
if (was == index) {
return;
}
if (was >= 0 && was < state->buttons.size()) {
state->buttons[was].active = false;
}
state->active = index;
state->buttons[index].active = true;
raw->update();
state->priceTab = state->buttons[index].price;
};
const auto session = &peer->session();
state->prices.value(
) | rpl::start_with_next([=](const std::vector<int> &prices) {
auto x = st::giftBoxTabsMargin.left();
auto y = st::giftBoxTabsMargin.top();
setSelected(-1);
state->buttons.resize(prices.size());
const auto padding = st::giftBoxTabPadding;
auto currentPrice = state->priceTab.current();
if (!ranges::contains(prices, currentPrice)) {
currentPrice = kPriceTabAll;
}
state->active = -1;
for (auto i = 0, count = int(prices.size()); i != count; ++i) {
const auto price = prices[i];
auto &button = state->buttons[i];
if (button.text.isEmpty() || button.price != price) {
button.price = price;
button.text = TabTextForPrice(session, price);
}
button.active = (price == currentPrice);
if (button.active) {
state->active = i;
}
const auto width = button.text.maxWidth();
const auto height = st::giftBoxTabStyle.font->height;
const auto r = QRect(0, 0, width, height).marginsAdded(padding);
button.geometry = QRect(QPoint(x, y), r.size());
x += r.width() + st::giftBoxTabSkip;
}
state->fullWidth = x
- st::giftBoxTabSkip
+ st::giftBoxTabsMargin.right();
const auto height = state->buttons.empty()
? 0
: (y
+ state->buttons.back().geometry.height()
+ st::giftBoxTabsMargin.bottom());
raw->resize(raw->width(), height);
raw->update();
}, raw->lifetime());
rpl::combine(
raw->widthValue(),
state->fullWidth.value()
) | rpl::start_with_next([=](int outer, int inner) {
state->scrollMax = std::max(0, inner - outer);
}, raw->lifetime());
raw->setMouseTracking(true);
raw->events() | rpl::start_with_next([=](not_null<QEvent*> e) {
const auto type = e->type();
switch (type) {
case QEvent::Leave: setSelected(-1); break;
case QEvent::MouseMove: {
const auto me = static_cast<QMouseEvent*>(e.get());
const auto mousex = me->pos().x();
const auto drag = QApplication::startDragDistance();
if (state->dragx > 0) {
state->scroll = std::clamp(
state->dragscroll + state->dragx - mousex,
0.,
state->scrollMax * 1.);
raw->update();
break;
} else if (state->pressx > 0
&& std::abs(state->pressx - mousex) > drag) {
state->dragx = state->pressx;
state->dragscroll = state->scroll;
}
const auto position = me->pos() + scroll();
for (auto i = 0, c = int(state->buttons.size()); i != c; ++i) {
if (state->buttons[i].geometry.contains(position)) {
setSelected(i);
break;
}
}
} break;
case QEvent::Wheel: {
const auto me = static_cast<QWheelEvent*>(e.get());
state->scroll = std::clamp(
state->scroll - ScrollDeltaF(me).x(),
0.,
state->scrollMax * 1.);
raw->update();
} break;
case QEvent::MouseButtonPress: {
const auto me = static_cast<QMouseEvent*>(e.get());
if (me->button() != Qt::LeftButton) {
break;
}
state->pressed = state->selected;
state->pressx = me->pos().x();
} break;
case QEvent::MouseButtonRelease: {
const auto me = static_cast<QMouseEvent*>(e.get());
if (me->button() != Qt::LeftButton) {
break;
}
const auto dragx = std::exchange(state->dragx, 0);
const auto pressed = std::exchange(state->pressed, -1);
state->pressx = 0;
if (!dragx && pressed >= 0 && state->selected == pressed) {
setActive(pressed);
}
} break;
}
}, raw->lifetime());
raw->paintRequest() | rpl::start_with_next([=] {
auto p = QPainter(raw);
auto hq = PainterHighQualityEnabler(p);
const auto padding = st::giftBoxTabPadding;
const auto shift = -scroll();
for (const auto &button : state->buttons) {
const auto geometry = button.geometry.translated(shift);
if (button.active) {
p.setBrush(st::giftBoxTabBgActive);
p.setPen(Qt::NoPen);
const auto radius = geometry.height() / 2.;
p.drawRoundedRect(geometry, radius, radius);
p.setPen(st::giftBoxTabFgActive);
} else {
p.setPen(st::giftBoxTabFg);
}
button.text.draw(p, {
.position = geometry.marginsRemoved(padding).topLeft(),
.availableWidth = button.text.maxWidth(),
});
}
{
const auto &icon = st::defaultEmojiSuggestions;
const auto w = icon.fadeRight.width();
const auto &c = st::boxDividerBg->c;
const auto r = QRect(0, 0, w, raw->height());
const auto s = std::abs(float64(shift.x()));
constexpr auto kF = 0.5;
const auto opacityRight = (state->scrollMax - s)
/ (icon.fadeRight.width() * kF);
p.setOpacity(std::clamp(std::abs(opacityRight), 0., 1.));
icon.fadeRight.fill(p, r.translated(raw->width() - w, 0), c);
const auto opacityLeft = s / (icon.fadeLeft.width() * kF);
p.setOpacity(std::clamp(std::abs(opacityLeft), 0., 1.));
icon.fadeLeft.fill(p, r, c);
}
}, raw->lifetime());
return {
.priceTab = state->priceTab.value(),
.widget = std::move(widget),
};
}
[[nodiscard]] int StarGiftMessageLimit(not_null<Main::Session*> session) {
return session->appConfig().get<int>(
u"stargifts_message_length_max"_q,
255);
}
[[nodiscard]] not_null<InputField*> AddPartInput(
not_null<Window::SessionController*> controller,
not_null<VerticalLayout*> container,
not_null<QWidget*> outer,
rpl::producer<QString> placeholder,
QString current,
int limit) {
const auto field = container->add(
object_ptr<InputField>(
container,
st::giftBoxTextField,
InputField::Mode::NoNewlines,
std::move(placeholder),
current),
st::giftBoxTextPadding);
field->setMaxLength(limit);
AddLengthLimitLabel(field, limit, std::nullopt, st::giftBoxLimitTop);
const auto toggle = CreateChild<EmojiButton>(
container,
st::defaultComposeFiles.emoji);
toggle->show();
field->geometryValue() | rpl::start_with_next([=](QRect r) {
toggle->move(
r.x() + r.width() - toggle->width(),
r.y() - st::giftBoxEmojiToggleTop);
}, toggle->lifetime());
using namespace ChatHelpers;
const auto panel = field->lifetime().make_state<TabbedPanel>(
outer,
controller,
object_ptr<TabbedSelector>(
nullptr,
controller->uiShow(),
Window::GifPauseReason::Layer,
TabbedSelector::Mode::EmojiOnly));
panel->setDesiredHeightValues(
1.,
st::emojiPanMinHeight / 2,
st::emojiPanMinHeight);
panel->hide();
panel->selector()->setAllowEmojiWithoutPremium(true);
panel->selector()->emojiChosen(
) | rpl::start_with_next([=](ChatHelpers::EmojiChosen data) {
InsertEmojiAtCursor(field->textCursor(), data.emoji);
}, field->lifetime());
panel->selector()->customEmojiChosen(
) | rpl::start_with_next([=](ChatHelpers::FileChosen data) {
Data::InsertCustomEmoji(field, data.document);
}, field->lifetime());
const auto updateEmojiPanelGeometry = [=] {
const auto parent = panel->parentWidget();
const auto global = toggle->mapToGlobal({ 0, 0 });
const auto local = parent->mapFromGlobal(global);
panel->moveBottomRight(
local.y(),
local.x() + toggle->width() * 3);
};
const auto filterCallback = [=](not_null<QEvent*> event) {
const auto type = event->type();
if (type == QEvent::Move || type == QEvent::Resize) {
// updateEmojiPanelGeometry uses not only container geometry, but
// also container children geometries that will be updated later.
crl::on_main(field, updateEmojiPanelGeometry);
}
return base::EventFilterResult::Continue;
};
for (auto widget = (QWidget*)field, end = (QWidget*)outer->parentWidget()
; widget && widget != end
; widget = widget->parentWidget()) {
base::install_event_filter(field, widget, filterCallback);
}
toggle->installEventFilter(panel);
toggle->addClickHandler([=] {
panel->toggleAnimated();
});
return field;
}
void SendGift(
not_null<Window::SessionController*> window,
not_null<PeerData*> peer,
std::shared_ptr<Api::PremiumGiftCodeOptions> api,
const GiftDetails &details,
Fn<void(Payments::CheckoutResult)> done) {
v::match(details.descriptor, [&](const GiftTypePremium &gift) {
auto invoice = api->invoice(1, gift.months);
invoice.purpose = Payments::InvoicePremiumGiftCodeUsers{
.users = { peer->asUser() },
.message = details.text,
};
Payments::CheckoutProcess::Start(std::move(invoice), done);
}, [&](const GiftTypeStars &gift) {
const auto processNonPanelPaymentFormFactory
= Payments::ProcessNonPanelPaymentFormFactory(window, done);
Payments::CheckoutProcess::Start(Payments::InvoiceStarGift{
.giftId = gift.info.id,
.randomId = details.randomId,
.message = details.text,
.recipient = peer,
.limitedCount = gift.info.limitedCount,
.anonymous = details.anonymous,
.upgraded = details.upgraded,
}, done, processNonPanelPaymentFormFactory);
});
}
[[nodiscard]] std::shared_ptr<Data::UniqueGift> FindUniqueGift(
not_null<Main::Session*> session,
const MTPUpdates &updates) {
auto result = std::shared_ptr<Data::UniqueGift>();
const auto checkAction = [&](const MTPMessageAction &action) {
action.match([&](const MTPDmessageActionStarGiftUnique &data) {
if (const auto gift = Api::FromTL(session, data.vgift())) {
result = gift->unique;
}
}, [](const auto &) {});
};
updates.match([&](const MTPDupdates &data) {
for (const auto &update : data.vupdates().v) {
update.match([&](const MTPDupdateNewMessage &data) {
data.vmessage().match([&](const MTPDmessageService &data) {
checkAction(data.vaction());
}, [](const auto &) {});
}, [](const auto &) {});
}
}, [](const auto &) {});
return result;
}
void ShowGiftUpgradedToast(
base::weak_ptr<Window::SessionController> weak,
not_null<Main::Session*> session,
const MTPUpdates &result) {
const auto gift = FindUniqueGift(session, result);
if (const auto strong = gift ? weak.get() : nullptr) {
strong->showToast({
.title = tr::lng_gift_upgraded_title(tr::now),
.text = tr::lng_gift_upgraded_about(
tr::now,
lt_name,
Text::Bold(Data::UniqueGiftName(*gift)),
Ui::Text::WithEntities),
.duration = kUpgradeDoneToastDuration,
});
}
}
void SendStarsFormRequest(
not_null<Window::SessionController*> controller,
Settings::SmallBalanceResult result,
uint64 formId,
MTPInputInvoice invoice,
Fn<void(Payments::CheckoutResult, const MTPUpdates *)> done) {
using BalanceResult = Settings::SmallBalanceResult;
const auto session = &controller->session();
if (result == BalanceResult::Success
|| result == BalanceResult::Already) {
const auto weak = base::make_weak(controller);
session->api().request(MTPpayments_SendStarsForm(
MTP_long(formId),
invoice
)).done([=](const MTPpayments_PaymentResult &result) {
result.match([&](const MTPDpayments_paymentResult &data) {
session->api().applyUpdates(data.vupdates());
done(Payments::CheckoutResult::Paid, &data.vupdates());
}, [&](const MTPDpayments_paymentVerificationNeeded &data) {
done(Payments::CheckoutResult::Failed, nullptr);
});
}).fail([=](const MTP::Error &error) {
if (const auto strong = weak.get()) {
strong->showToast(error.type());
}
done(Payments::CheckoutResult::Failed, nullptr);
}).send();
} else if (result == BalanceResult::Cancelled) {
done(Payments::CheckoutResult::Cancelled, nullptr);
} else {
done(Payments::CheckoutResult::Failed, nullptr);
}
}
void UpgradeGift(
not_null<Window::SessionController*> window,
Data::SavedStarGiftId savedId,
bool keepDetails,
int stars,
Fn<void(Payments::CheckoutResult)> done) {
const auto session = &window->session();
const auto weak = base::make_weak(window);
auto formDone = [=](
Payments::CheckoutResult result,
const MTPUpdates *updates) {
if (result == Payments::CheckoutResult::Paid && updates) {
if (const auto strong = weak.get()) {
ShowGiftUpgradedToast(strong, session, *updates);
}
}
done(result);
};
if (stars <= 0) {
using Flag = MTPpayments_UpgradeStarGift::Flag;
session->api().request(MTPpayments_UpgradeStarGift(
MTP_flags(keepDetails ? Flag::f_keep_original_details : Flag()),
Api::InputSavedStarGiftId(savedId)
)).done([=](const MTPUpdates &result) {
session->api().applyUpdates(result);
formDone(Payments::CheckoutResult::Paid, &result);
}).fail([=](const MTP::Error &error) {
if (const auto strong = weak.get()) {
strong->showToast(error.type());
}
formDone(Payments::CheckoutResult::Failed, nullptr);
}).send();
return;
}
using Flag = MTPDinputInvoiceStarGiftUpgrade::Flag;
RequestStarsFormAndSubmit(
window,
MTP_inputInvoiceStarGiftUpgrade(
MTP_flags(keepDetails ? Flag::f_keep_original_details : Flag()),
Api::InputSavedStarGiftId(savedId)),
std::move(formDone));
}
void SoldOutBox(
not_null<GenericBox*> box,
not_null<Window::SessionController*> window,
const GiftTypeStars &gift) {
Settings::ReceiptCreditsBox(
box,
window,
Data::CreditsHistoryEntry{
.firstSaleDate = base::unixtime::parse(gift.info.firstSaleDate),
.lastSaleDate = base::unixtime::parse(gift.info.lastSaleDate),
.credits = StarsAmount(gift.info.stars),
.bareGiftStickerId = gift.info.document->id,
.peerType = Data::CreditsHistoryEntry::PeerType::Peer,
.limitedCount = gift.info.limitedCount,
.limitedLeft = gift.info.limitedLeft,
.soldOutInfo = true,
.gift = true,
},
Data::SubscriptionEntry());
}
void AddUpgradeButton(
not_null<Ui::VerticalLayout*> container,
not_null<Main::Session*> session,
int cost,
not_null<PeerData*> peer,
Fn<void(bool)> toggled,
Fn<void()> preview) {
const auto button = container->add(
object_ptr<SettingsButton>(
container,
rpl::single(QString()),
st::settingsButtonNoIcon));
button->toggleOn(rpl::single(false))->toggledValue(
) | rpl::start_with_next(toggled, button->lifetime());
const auto makeContext = [session](Fn<void()> update) {
return Core::MarkedTextContext{
.session = session,
.customEmojiRepaint = std::move(update),
};
};
auto star = session->data().customEmojiManager().creditsEmoji();
const auto label = Ui::CreateChild<Ui::FlatLabel>(
button,
tr::lng_gift_send_unique(
lt_price,
rpl::single(star.append(' '
+ Lang::FormatStarsAmountDecimal(
StarsAmount{ cost }))),
Text::WithEntities),
st::boxLabel,
st::defaultPopupMenu,
std::move(makeContext));
label->show();
label->setAttribute(Qt::WA_TransparentForMouseEvents);
button->widthValue() | rpl::start_with_next([=](int outer) {
const auto padding = st::settingsButtonNoIcon.padding;
const auto inner = outer
- padding.left()
- padding.right()
- st::settingsButtonNoIcon.toggleSkip
- 2 * st::settingsButtonNoIcon.toggle.border
- 2 * st::settingsButtonNoIcon.toggle.diameter
- 2 * st::settingsButtonNoIcon.toggle.width;
label->resizeToWidth(inner);
label->moveToLeft(padding.left(), padding.top(), outer);
}, label->lifetime());
AddSkip(container);
const auto about = AddDividerText(
container,
(peer->isBroadcast()
? tr::lng_gift_send_unique_about_channel(
lt_name,
rpl::single(TextWithEntities{ peer->name() }),
lt_link,
tr::lng_gift_send_unique_link() | Text::ToLink(),
Text::WithEntities)
: tr::lng_gift_send_unique_about(
lt_user,
rpl::single(TextWithEntities{ peer->shortName() }),
lt_link,
tr::lng_gift_send_unique_link() | Text::ToLink(),
Text::WithEntities)));
about->setClickHandlerFilter([=](const auto &...) {
preview();
return false;
});
}
void AddSoldLeftSlider(
not_null<RoundButton*> button,
const GiftTypeStars &gift) {
const auto still = gift.info.limitedLeft;
const auto total = gift.info.limitedCount;
const auto slider = CreateChild<RpWidget>(button->parentWidget());
struct State {
Text::String still;
Text::String sold;
int height = 0;
};
const auto state = slider->lifetime().make_state<State>();
const auto sold = total - still;
state->still.setText(
st::semiboldTextStyle,
tr::lng_gift_send_limited_left(tr::now, lt_count_decimal, still));
state->sold.setText(
st::semiboldTextStyle,
tr::lng_gift_send_limited_sold(tr::now, lt_count_decimal, sold));
state->height = st::giftLimitedPadding.top()
+ st::semiboldFont->height
+ st::giftLimitedPadding.bottom();
button->geometryValue() | rpl::start_with_next([=](QRect geometry) {
const auto space = st::giftLimitedBox.buttonPadding.top();
const auto skip = (space - state->height) / 2;
slider->setGeometry(
geometry.x(),
geometry.y() - skip - state->height,
geometry.width(),
state->height);
}, slider->lifetime());
slider->paintRequest() | rpl::start_with_next([=] {
const auto &padding = st::giftLimitedPadding;
const auto left = (padding.left() * 2) + state->still.maxWidth();
const auto right = (padding.right() * 2) + state->sold.maxWidth();
const auto space = slider->width() - left - right;
if (space <= 0) {
return;
}
const auto edge = left + ((space * still) / total);
const auto radius = st::buttonRadius;
auto p = QPainter(slider);
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(st::windowBgOver);
p.drawRoundedRect(
edge - (radius * 3),
0,
slider->width() - (edge - (radius * 3)),
state->height,
radius,
radius);
p.setBrush(st::windowBgActive);
p.drawRoundedRect(0, 0, edge, state->height, radius, radius);
p.setPen(st::windowFgActive);
state->still.draw(p, {
.position = { padding.left(), padding.top() },
.availableWidth = left,
});
p.setPen(st::windowSubTextFg);
state->sold.draw(p, {
.position = { left + space + padding.right(), padding.top() },
.availableWidth = right,
});
}, slider->lifetime());
}
void SendGiftBox(
not_null<GenericBox*> box,
not_null<Window::SessionController*> window,
not_null<PeerData*> peer,
std::shared_ptr<Api::PremiumGiftCodeOptions> api,
const GiftDescriptor &descriptor) {
const auto stars = std::get_if<GiftTypeStars>(&descriptor);
const auto limited = stars
&& (stars->info.limitedCount > stars->info.limitedLeft)
&& (stars->info.limitedLeft > 0);
box->setStyle(limited ? st::giftLimitedBox : st::giftBox);
box->setWidth(st::boxWideWidth);
box->setTitle(tr::lng_gift_send_title());
box->addTopButton(st::boxTitleClose, [=] {
box->closeBox();
});
const auto session = &window->session();
struct State {
rpl::variable<GiftDetails> details;
std::shared_ptr<Data::DocumentMedia> media;
bool submitting = false;
};
const auto state = box->lifetime().make_state<State>();
state->details = GiftDetails{
.descriptor = descriptor,
.randomId = base::RandomValue<uint64>(),
};
auto cost = state->details.value(
) | rpl::map([session](const GiftDetails &details) {
return v::match(details.descriptor, [&](const GiftTypePremium &data) {
if (data.currency == kCreditsCurrency) {
return CreditsEmojiSmall(session).append(
Lang::FormatCountDecimal(std::abs(data.cost)));
}
return TextWithEntities{
FillAmountAndCurrency(data.cost, data.currency),
};
}, [&](const GiftTypeStars &data) {
const auto amount = std::abs(data.info.stars)
+ (details.upgraded ? data.info.starsToUpgrade : 0);
return CreditsEmojiSmall(session).append(
Lang::FormatCountDecimal(amount));
});
});
const auto document = LookupGiftSticker(session, descriptor);
if ((state->media = document ? document->createMediaView() : nullptr)) {
state->media->checkStickerLarge();
}
const auto container = box->verticalLayout();
container->add(object_ptr<PreviewWrap>(
container,
peer,
state->details.value()));
const auto limit = StarGiftMessageLimit(session);
const auto text = AddPartInput(
window,
container,
box->getDelegate()->outerContainer(),
tr::lng_gift_send_message(),
QString(),
limit);
text->changes() | rpl::start_with_next([=] {
auto now = state->details.current();
auto textWithTags = text->getTextWithAppliedMarkdown();
now.text = TextWithEntities{
std::move(textWithTags.text),
TextUtilities::ConvertTextTagsToEntities(textWithTags.tags)
};
state->details = std::move(now);
}, text->lifetime());
box->setFocusCallback([=] {
text->setFocusFast();
});
const auto allow = [=](not_null<DocumentData*> emoji) {
return true;
};
InitMessageFieldHandlers({
.session = session,
.show = window->uiShow(),
.field = text,
.customEmojiPaused = [=] {
using namespace Window;
return window->isGifPausedAtLeastFor(GifPauseReason::Layer);
},
.allowPremiumEmoji = allow,
.allowMarkdownTags = {
InputField::kTagBold,
InputField::kTagItalic,
InputField::kTagUnderline,
InputField::kTagStrikeOut,
InputField::kTagSpoiler,
}
});
Emoji::SuggestionsController::Init(
box->getDelegate()->outerContainer(),
text,
session,
{ .suggestCustomEmoji = true, .allowCustomWithoutPremium = allow });
if (stars) {
const auto cost = stars->info.starsToUpgrade;
if (cost > 0 && !peer->isSelf()) {
const auto id = stars->info.id;
const auto showing = std::make_shared<bool>();
AddDivider(container);
AddSkip(container);
AddUpgradeButton(container, session, cost, peer, [=](bool on) {
auto now = state->details.current();
now.upgraded = on;
state->details = std::move(now);
}, [=] {
if (*showing) {
return;
}
*showing = true;
ShowStarGiftUpgradeBox({
.controller = window,
.stargiftId = id,
.ready = [=](bool) { *showing = false; },
.peer = peer,
.cost = int(cost),
});
});
} else {
AddDivider(container);
}
AddSkip(container);
container->add(
object_ptr<SettingsButton>(
container,
tr::lng_gift_send_anonymous(),
st::settingsButtonNoIcon)
)->toggleOn(rpl::single(peer->isSelf()))->toggledValue(
) | rpl::start_with_next([=](bool toggled) {
auto now = state->details.current();
now.anonymous = toggled;
state->details = std::move(now);
}, container->lifetime());
AddSkip(container);
}
v::match(descriptor, [&](const GiftTypePremium &) {
AddDividerText(container, tr::lng_gift_send_premium_about(
lt_user,
rpl::single(peer->shortName())));
}, [&](const GiftTypeStars &) {
AddDividerText(container, peer->isSelf()
? tr::lng_gift_send_anonymous_self()
: peer->isBroadcast()
? tr::lng_gift_send_anonymous_about_channel()
: tr::lng_gift_send_anonymous_about(
lt_user,
rpl::single(peer->shortName()),
lt_recipient,
rpl::single(peer->shortName())));
});
const auto buttonWidth = st::boxWideWidth
- st::giftBox.buttonPadding.left()
- st::giftBox.buttonPadding.right();
const auto button = box->addButton(rpl::single(QString()), [=] {
if (state->submitting) {
return;
}
state->submitting = true;
const auto details = state->details.current();
const auto weak = MakeWeak(box);
const auto done = [=](Payments::CheckoutResult result) {
if (result == Payments::CheckoutResult::Paid) {
const auto copy = state->media;
window->showPeerHistory(peer);
ShowSentToast(window, descriptor, details);
}
if (const auto strong = weak.data()) {
box->closeBox();
}
};
SendGift(window, peer, api, details, done);
});
if (limited) {
AddSoldLeftSlider(button, *stars);
}
SetButtonMarkedLabel(
button,
(peer->isSelf()
? tr::lng_gift_send_button_self
: tr::lng_gift_send_button)(
lt_cost,
std::move(cost),
Text::WithEntities),
session,
st::creditsBoxButtonLabel,
&st::giftBox.button.textFg);
button->resizeToWidth(buttonWidth);
button->widthValue() | rpl::start_with_next([=](int width) {
if (width != buttonWidth) {
button->resizeToWidth(buttonWidth);
}
}, button->lifetime());
}
[[nodiscard]] object_ptr<RpWidget> MakeGiftsList(
not_null<Window::SessionController*> window,
not_null<PeerData*> peer,
rpl::producer<GiftsDescriptor> gifts) {
auto result = object_ptr<RpWidget>((QWidget*)nullptr);
const auto raw = result.data();
struct State {
Delegate delegate;
std::vector<std::unique_ptr<GiftButton>> buttons;
bool sending = false;
};
const auto state = raw->lifetime().make_state<State>(State{
.delegate = Delegate(window, GiftButtonMode::Full),
});
const auto single = state->delegate.buttonSize();
const auto shadow = st::defaultDropdownMenu.wrap.shadow;
const auto extend = shadow.extend;
auto &packs = window->session().giftBoxStickersPacks();
packs.updated() | rpl::start_with_next([=] {
for (const auto &button : state->buttons) {
button->update();
}
}, raw->lifetime());
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();
const auto count = int(gifts.list.size());
auto order = ranges::views::ints
| ranges::views::take(count)
| ranges::to_vector;
if (SortForBirthday(peer)) {
ranges::stable_partition(order, [&](int i) {
const auto &gift = gifts.list[i];
const auto stars = std::get_if<GiftTypeStars>(&gift);
return stars && stars->info.birthday;
});
}
auto x = padding.left();
auto y = padding.top();
state->buttons.resize(count);
for (auto &button : state->buttons) {
if (!button) {
button = std::make_unique<GiftButton>(raw, &state->delegate);
button->show();
}
}
const auto api = gifts.api;
for (auto i = 0; i != count; ++i) {
const auto button = state->buttons[i].get();
const auto &descriptor = gifts.list[order[i]];
button->setDescriptor(descriptor, GiftButton::Mode::Full);
const auto last = !((i + 1) % perRow);
if (last) {
x = padding.left() + available - single.width();
}
button->setGeometry(QRect(QPoint(x, y), single), extend);
if (last) {
x = padding.left();
y += single.height() + st::giftBoxGiftSkip.y();
} else {
x += single.width() + st::giftBoxGiftSkip.x();
}
button->setClickedCallback([=] {
const auto star = std::get_if<GiftTypeStars>(&descriptor);
if (star && IsSoldOut(star->info)) {
window->show(Box(SoldOutBox, window, *star));
} else {
window->show(
Box(SendGiftBox, window, peer, api, descriptor));
}
});
}
if (count % perRow) {
y += padding.bottom() + single.height();
} else {
y += padding.bottom() - st::giftBoxGiftSkip.y();
}
raw->resize(raw->width(), count ? y : 0);
}, raw->lifetime());
return result;
}
void FillBg(not_null<RpWidget*> box) {
box->paintRequest() | rpl::start_with_next([=] {
auto p = QPainter(box);
auto hq = PainterHighQualityEnabler(p);
const auto radius = st::boxRadius;
p.setPen(Qt::NoPen);
p.setBrush(st::boxDividerBg);
p.drawRoundedRect(
box->rect().marginsAdded({ 0, 0, 0, 2 * radius }),
radius,
radius);
}, box->lifetime());
}
struct AddBlockArgs {
rpl::producer<QString> subtitle;
rpl::producer<TextWithEntities> about;
Fn<bool(const ClickHandlerPtr&, Qt::MouseButton)> aboutFilter;
object_ptr<RpWidget> content;
};
void AddBlock(
not_null<VerticalLayout*> content,
not_null<Window::SessionController*> window,
AddBlockArgs &&args) {
content->add(
object_ptr<FlatLabel>(
content,
std::move(args.subtitle),
st::giftBoxSubtitle),
st::giftBoxSubtitleMargin);
const auto about = content->add(
object_ptr<FlatLabel>(
content,
std::move(args.about),
st::giftBoxAbout),
st::giftBoxAboutMargin);
about->setClickHandlerFilter(std::move(args.aboutFilter));
content->add(std::move(args.content));
}
[[nodiscard]] object_ptr<RpWidget> MakePremiumGifts(
not_null<Window::SessionController*> window,
not_null<PeerData*> peer) {
struct State {
rpl::variable<PremiumGiftsDescriptor> gifts;
};
auto state = std::make_unique<State>();
state->gifts = GiftsPremium(&window->session(), peer);
auto result = MakeGiftsList(window, peer, state->gifts.value(
) | rpl::map([=](const PremiumGiftsDescriptor &gifts) {
return GiftsDescriptor{
gifts.list | ranges::to<std::vector<GiftDescriptor>>,
gifts.api,
};
}));
result->lifetime().add([state = std::move(state)] {});
return result;
}
[[nodiscard]] object_ptr<RpWidget> MakeStarsGifts(
not_null<Window::SessionController*> window,
not_null<PeerData*> peer) {
auto result = object_ptr<VerticalLayout>((QWidget*)nullptr);
struct State {
rpl::variable<std::vector<GiftTypeStars>> gifts;
rpl::variable<int> priceTab = kPriceTabAll;
};
const auto state = result->lifetime().make_state<State>();
state->gifts = GiftsStars(&window->session(), peer);
auto tabs = MakeGiftsPriceTabs(window, peer, state->gifts.value());
state->priceTab = std::move(tabs.priceTab);
result->add(std::move(tabs.widget));
result->add(MakeGiftsList(window, peer, rpl::combine(
state->gifts.value(),
state->priceTab.value()
) | rpl::map([=](std::vector<GiftTypeStars> &&gifts, int price) {
gifts.erase(ranges::remove_if(gifts, [&](const GiftTypeStars &gift) {
return (price == kPriceTabLimited)
? (!gift.info.limitedCount)
: (price == kPriceTabInStock)
? IsSoldOut(gift.info)
: (price && gift.info.stars != price);
}), end(gifts));
return GiftsDescriptor{
gifts | ranges::to<std::vector<GiftDescriptor>>(),
};
})));
return result;
}
void GiftBox(
not_null<GenericBox*> box,
not_null<Window::SessionController*> window,
not_null<PeerData*> peer) {
box->setWidth(st::boxWideWidth);
box->setStyle(st::creditsGiftBox);
box->setNoContentMargin(true);
box->setCustomCornersFilling(RectPart::FullTop);
box->addButton(tr::lng_create_group_back(), [=] { box->closeBox(); });
FillBg(box);
const auto &stUser = st::premiumGiftsUserpicButton;
const auto content = box->verticalLayout();
AddSkip(content, st::defaultVerticalListSkip * 5);
content->add(
object_ptr<CenterWrap<>>(
content,
object_ptr<UserpicButton>(content, peer, stUser))
)->setAttribute(Qt::WA_TransparentForMouseEvents);
AddSkip(content);
AddSkip(content);
Settings::AddMiniStars(
content,
CreateChild<RpWidget>(content),
stUser.photoSize,
box->width(),
2.);
AddSkip(content);
AddSkip(box->verticalLayout());
const auto starsClickHandlerFilter = [=](const auto &...) {
window->showSettings(Settings::CreditsId());
return false;
};
if (peer->isUser() && !peer->isSelf()) {
const auto premiumClickHandlerFilter = [=](const auto &...) {
Settings::ShowPremium(window, u"gift_send"_q);
return false;
};
AddBlock(content, window, {
.subtitle = tr::lng_gift_premium_subtitle(),
.about = tr::lng_gift_premium_about(
lt_name,
rpl::single(Text::Bold(peer->shortName())),
lt_features,
tr::lng_gift_premium_features() | Text::ToLink(),
Text::WithEntities),
.aboutFilter = premiumClickHandlerFilter,
.content = MakePremiumGifts(window, peer),
});
}
AddBlock(content, window, {
.subtitle = (peer->isSelf()
? tr::lng_gift_self_title()
: peer->isBroadcast()
? tr::lng_gift_channel_title()
: tr::lng_gift_stars_subtitle()),
.about = (peer->isSelf()
? tr::lng_gift_self_about(Text::WithEntities)
: peer->isBroadcast()
? tr::lng_gift_channel_about(
lt_name,
rpl::single(Text::Bold(peer->name())),
Text::WithEntities)
: tr::lng_gift_stars_about(
lt_name,
rpl::single(Text::Bold(peer->shortName())),
lt_link,
tr::lng_gift_stars_link() | Text::ToLink(),
Text::WithEntities)),
.aboutFilter = starsClickHandlerFilter,
.content = MakeStarsGifts(window, peer),
});
}
struct SelfOption {
object_ptr<Ui::RpWidget> content = { nullptr };
Fn<bool(int, int, int)> overrideKey;
Fn<void()> activate;
};
class Controller final : public ContactsBoxController {
public:
Controller(
not_null<Main::Session*> session,
Fn<void(not_null<PeerData*>)> choose);
void noSearchSubmit();
bool overrideKeyboardNavigation(
int direction,
int fromIndex,
int toIndex) override;
private:
std::unique_ptr<PeerListRow> createRow(
not_null<UserData*> user) override;
void prepareViewHook() override;
void rowClicked(not_null<PeerListRow*> row) override;
const Fn<void(not_null<PeerData*>)> _choose;
SelfOption _selfOption;
};
[[nodiscard]] SelfOption MakeSelfOption(
not_null<Main::Session*> session,
Fn<void()> activate) {
class SelfController final : public PeerListController {
public:
SelfController(
not_null<Main::Session*> session,
Fn<void()> activate)
: _session(session)
, _activate(std::move(activate)) {
}
void prepare() override {
auto row = std::make_unique<PeerListRow>(_session->user());
row->setCustomStatus(tr::lng_gift_self_status(tr::now));
delegate()->peerListAppendRow(std::move(row));
delegate()->peerListRefreshRows();
}
void loadMoreRows() override {
}
void rowClicked(not_null<PeerListRow*> row) override {
_activate();
}
Main::Session &session() const override {
return *_session;
}
private:
const not_null<Main::Session*> _session;
Fn<void()> _activate;
};
auto result = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);
const auto container = result.data();
Ui::AddSkip(container);
const auto delegate = container->lifetime().make_state<
PeerListContentDelegateSimple
>();
const auto controller = container->lifetime().make_state<
SelfController
>(session, activate);
controller->setStyleOverrides(&st::peerListSingleRow);
const auto content = container->add(object_ptr<PeerListContent>(
container,
controller));
delegate->setContent(content);
controller->setDelegate(delegate);
Ui::AddSkip(container);
container->add(CreatePeerListSectionSubtitle(
container,
tr::lng_contacts_header()));
const auto overrideKey = [=](int direction, int from, int to) {
if (!content->isVisible()) {
return false;
} else if (direction > 0 && from < 0 && to >= 0) {
if (content->hasSelection()) {
const auto was = content->selectedIndex();
const auto now = content->selectSkip(1).reallyMovedTo;
if (was != now) {
return true;
}
content->clearSelection();
} else {
content->selectSkip(1);
return true;
}
} else if (direction < 0 && to < 0) {
if (!content->hasSelection()) {
content->selectLast();
} else if (from >= 0 || content->hasSelection()) {
content->selectSkip(-1);
}
}
return false;
};
return {
.content = std::move(result),
.overrideKey = overrideKey,
.activate = activate,
};
}
Controller::Controller(
not_null<Main::Session*> session,
Fn<void(not_null<PeerData*>)> choose)
: ContactsBoxController(session)
, _choose(std::move(choose))
, _selfOption(MakeSelfOption(session, [=] { _choose(session->user()); })) {
setStyleOverrides(&st::peerListSmallSkips);
}
void Controller::noSearchSubmit() {
if (const auto onstack = _selfOption.activate) {
onstack();
}
}
bool Controller::overrideKeyboardNavigation(
int direction,
int fromIndex,
int toIndex) {
return _selfOption.overrideKey
&& _selfOption.overrideKey(direction, fromIndex, toIndex);
}
std::unique_ptr<PeerListRow> Controller::createRow(
not_null<UserData*> user) {
if (user->isSelf()
|| user->isBot()
|| user->isServiceUser()
|| user->isInaccessible()) {
return nullptr;
}
return ContactsBoxController::createRow(user);
}
void Controller::prepareViewHook() {
delegate()->peerListSetAboveWidget(std::move(_selfOption.content));
}
void Controller::rowClicked(not_null<PeerListRow*> row) {
_choose(row->peer());
}
} // namespace
void ChooseStarGiftRecipient(
not_null<Window::SessionController*> window) {
auto controller = std::make_unique<Controller>(
&window->session(),
[=](not_null<PeerData*> peer) {
ShowStarGiftBox(window, peer);
});
const auto controllerRaw = controller.get();
auto initBox = [=](not_null<PeerListBox*> box) {
box->setTitle(tr::lng_gift_premium_or_stars());
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
box->noSearchSubmits() | rpl::start_with_next([=] {
controllerRaw->noSearchSubmit();
}, box->lifetime());
};
window->show(
Box<PeerListBox>(std::move(controller), std::move(initBox)),
LayerOption::KeepOther);
}
void ShowStarGiftBox(
not_null<Window::SessionController*> controller,
not_null<PeerData*> peer) {
controller->show(Box(GiftBox, controller, peer));
}
void AddUniqueGiftCover(
not_null<VerticalLayout*> container,
rpl::producer<Data::UniqueGift> data,
rpl::producer<QString> subtitleOverride) {
const auto cover = container->add(object_ptr<RpWidget>(container));
const auto title = CreateChild<FlatLabel>(
cover,
rpl::duplicate(
data
) | rpl::map([](const Data::UniqueGift &now) { return now.title; }),
st::uniqueGiftTitle);
title->setTextColorOverride(QColor(255, 255, 255));
auto subtitleText = subtitleOverride
? std::move(subtitleOverride)
: rpl::duplicate(data) | rpl::map([](const Data::UniqueGift &gift) {
return tr::lng_gift_unique_number(
tr::now,
lt_index,
QString::number(gift.number));
});
const auto subtitle = CreateChild<FlatLabel>(
cover,
std::move(subtitleText),
st::uniqueGiftSubtitle);
struct GiftView {
QImage gradient;
std::optional<Data::UniqueGift> gift;
std::shared_ptr<Data::DocumentMedia> media;
std::unique_ptr<Lottie::SinglePlayer> lottie;
std::unique_ptr<Text::CustomEmoji> emoji;
base::flat_map<float64, QImage> emojis;
rpl::lifetime lifetime;
};
struct State {
GiftView now;
GiftView next;
Animations::Simple crossfade;
bool animating = false;
};
const auto state = cover->lifetime().make_state<State>();
const auto lottieSize = st::creditsHistoryEntryStarGiftSize;
const auto updateColors = [=](float64 progress) {
subtitle->setTextColorOverride((progress == 0.)
? state->now.gift->backdrop.textColor
: (progress == 1.)
? state->next.gift->backdrop.textColor
: anim::color(
state->now.gift->backdrop.textColor,
state->next.gift->backdrop.textColor,
progress));
};
std::move(
data
) | rpl::start_with_next([=](const Data::UniqueGift &gift) {
const auto setup = [&](GiftView &to) {
to.gift = gift;
const auto document = gift.model.document;
to.media = document->createMediaView();
to.media->automaticLoad({}, nullptr);
rpl::single() | rpl::then(
document->session().downloaderTaskFinished()
) | rpl::filter([&to] {
return to.media->loaded();
}) | rpl::start_with_next([=, &to] {
const auto lottieSize = st::creditsHistoryEntryStarGiftSize;
to.lottie = ChatHelpers::LottiePlayerFromDocument(
to.media.get(),
ChatHelpers::StickerLottieSize::MessageHistory,
QSize(lottieSize, lottieSize),
Lottie::Quality::High);
to.lifetime.destroy();
const auto lottie = to.lottie.get();
lottie->updates() | rpl::start_with_next([=] {
if (state->now.lottie.get() == lottie
|| state->crossfade.animating()) {
cover->update();
}
}, to.lifetime);
}, to.lifetime);
to.emoji = document->owner().customEmojiManager().create(
gift.pattern.document,
[=] { cover->update(); },
Data::CustomEmojiSizeTag::Large);
[[maybe_unused]] const auto preload = to.emoji->ready();
};
if (!state->now.gift) {
setup(state->now);
cover->update();
updateColors(0.);
} else if (!state->next.gift) {
setup(state->next);
}
}, cover->lifetime());
cover->widthValue() | rpl::start_with_next([=](int width) {
const auto skip = st::uniqueGiftBottom;
if (width <= 3 * skip) {
return;
}
const auto available = width - 2 * skip;
title->resizeToWidth(available);
title->moveToLeft(skip, st::uniqueGiftTitleTop);
subtitle->resizeToWidth(available);
subtitle->moveToLeft(skip, st::uniqueGiftSubtitleTop);
cover->resize(width, subtitle->y() + subtitle->height() + skip);
}, cover->lifetime());
cover->paintRequest() | rpl::start_with_next([=] {
auto p = QPainter(cover);
auto progress = state->crossfade.value(state->animating ? 1. : 0.);
if (state->animating) {
updateColors(progress);
}
if (progress == 1.) {
state->animating = false;
state->now = base::take(state->next);
progress = 0.;
}
const auto paint = [&](GiftView &gift, float64 shown) {
Expects(gift.gift.has_value());
const auto width = cover->width();
const auto pointsHeight = st::uniqueGiftSubtitleTop;
const auto ratio = style::DevicePixelRatio();
if (gift.gradient.size() != cover->size() * ratio) {
gift.gradient = CreateGradient(cover->size(), *gift.gift);
}
p.drawImage(0, 0, gift.gradient);
PaintPoints(
p,
PatternPoints(),
gift.emojis,
gift.emoji.get(),
*gift.gift,
QRect(0, 0, width, pointsHeight),
shown);
const auto lottie = gift.lottie.get();
const auto factor = style::DevicePixelRatio();
const auto request = Lottie::FrameRequest{
.box = Size(lottieSize) * factor,
};
const auto frame = (lottie && lottie->ready())
? lottie->frameInfo(request)
: Lottie::Animation::FrameInfo();
if (frame.image.isNull()) {
return false;
}
const auto size = frame.image.size() / factor;
const auto left = (width - size.width()) / 2;
p.drawImage(
QRect(QPoint(left, st::uniqueGiftModelTop), size),
frame.image);
const auto count = lottie->framesCount();
const auto finished = lottie->frameIndex() == (count - 1);
lottie->markFrameShown();
return finished;
};
if (progress < 1.) {
const auto finished = paint(state->now, 1. - progress);
const auto next = finished ? state->next.lottie.get() : nullptr;
if (next && next->ready()) {
state->animating = true;
state->crossfade.start([=] {
cover->update();
}, 0., 1., kCrossfadeDuration);
}
}
if (progress > 0.) {
p.setOpacity(progress);
paint(state->next, progress);
}
}, cover->lifetime());
}
void AddWearGiftCover(
not_null<VerticalLayout*> container,
const Data::UniqueGift &data,
not_null<PeerData*> peer) {
const auto cover = container->add(object_ptr<RpWidget>(container));
const auto title = CreateChild<FlatLabel>(
cover,
rpl::single(peer->name()),
st::uniqueGiftTitle);
title->setTextColorOverride(QColor(255, 255, 255));
const auto subtitle = CreateChild<FlatLabel>(
cover,
(peer->isChannel()
? tr::lng_chat_status_subscribers(
lt_count,
rpl::single(peer->asChannel()->membersCount() * 1.))
: tr::lng_status_online()),
st::uniqueGiftSubtitle);
subtitle->setTextColorOverride(data.backdrop.textColor);
struct State {
QImage gradient;
Data::UniqueGift gift;
Ui::PeerUserpicView view;
std::unique_ptr<Text::CustomEmoji> emoji;
base::flat_map<float64, QImage> emojis;
rpl::lifetime lifetime;
};
const auto state = cover->lifetime().make_state<State>(State{
.gift = data,
});
state->emoji = peer->owner().customEmojiManager().create(
state->gift.pattern.document,
[=] { cover->update(); },
Data::CustomEmojiSizeTag::Large);
cover->widthValue() | rpl::start_with_next([=](int width) {
const auto skip = st::uniqueGiftBottom;
if (width <= 3 * skip) {
return;
}
const auto available = width - 2 * skip;
title->resizeToWidth(available);
title->moveToLeft(skip, st::uniqueGiftTitleTop);
subtitle->resizeToWidth(available);
subtitle->moveToLeft(skip, st::uniqueGiftSubtitleTop);
cover->resize(width, subtitle->y() + subtitle->height() + skip);
}, cover->lifetime());
cover->paintRequest() | rpl::start_with_next([=] {
auto p = Painter(cover);
const auto width = cover->width();
const auto pointsHeight = st::uniqueGiftSubtitleTop;
const auto ratio = style::DevicePixelRatio();
if (state->gradient.size() != cover->size() * ratio) {
state->gradient = CreateGradient(cover->size(), state->gift);
}
p.drawImage(0, 0, state->gradient);
PaintPoints(
p,
PatternPoints(),
state->emojis,
state->emoji.get(),
state->gift,
QRect(0, 0, width, pointsHeight),
1.);
peer->paintUserpic(
p,
state->view,
(width - st::uniqueGiftUserpicSize) / 2,
st::uniqueGiftUserpicTop,
st::uniqueGiftUserpicSize);
}, cover->lifetime());
}
void ShowUniqueGiftWearBox(
std::shared_ptr<ChatHelpers::Show> show,
not_null<PeerData*> peer,
const Data::UniqueGift &gift,
Settings::GiftWearBoxStyleOverride st) {
show->show(Box([=](not_null<Ui::GenericBox*> box) {
box->setNoContentMargin(true);
box->setWidth((st::boxWidth + st::boxWideWidth) / 2); // =)
box->setStyle(st.box ? *st.box : st::upgradeGiftBox);
const auto channel = peer->isChannel();
const auto content = box->verticalLayout();
AddWearGiftCover(content, gift, peer);
AddSkip(content, st::defaultVerticalListSkip * 2);
const auto infoRow = [&](
rpl::producer<QString> title,
rpl::producer<QString> text,
not_null<const style::icon*> icon) {
auto raw = content->add(
object_ptr<Ui::VerticalLayout>(content));
raw->add(
object_ptr<Ui::FlatLabel>(
raw,
std::move(title) | Ui::Text::ToBold(),
st.infoTitle ? *st.infoTitle : st::defaultFlatLabel),
st::settingsPremiumRowTitlePadding);
raw->add(
object_ptr<Ui::FlatLabel>(
raw,
std::move(text),
st.infoAbout ? *st.infoAbout : st::upgradeGiftSubtext),
st::settingsPremiumRowAboutPadding);
object_ptr<Info::Profile::FloatingIcon>(
raw,
*icon,
st::starrefInfoIconPosition);
};
content->add(
object_ptr<Ui::FlatLabel>(
content,
tr::lng_gift_wear_title(
lt_name,
rpl::single(UniqueGiftName(gift))),
st.title ? *st.title : st::uniqueGiftTitle),
st::settingsPremiumRowTitlePadding);
content->add(
object_ptr<Ui::FlatLabel>(
content,
tr::lng_gift_wear_about(),
st.subtitle ? *st.subtitle : st::uniqueGiftSubtitle),
st::settingsPremiumRowAboutPadding);
infoRow(
tr::lng_gift_wear_badge_title(),
(channel
? tr::lng_gift_wear_badge_about_channel()
: tr::lng_gift_wear_badge_about()),
st.radiantIcon ? st.radiantIcon : &st::menuIconUnique);
//infoRow(
// tr::lng_gift_wear_design_title(),
// tr::lng_gift_wear_design_about(),
// &st::menuIconUniqueProfile);
infoRow(
tr::lng_gift_wear_proof_title(),
(channel
? tr::lng_gift_wear_proof_about_channel()
: tr::lng_gift_wear_proof_about()),
st.proofIcon ? st.proofIcon : &st::menuIconFactcheck);
const auto session = &show->session();
const auto checking = std::make_shared<bool>();
const auto button = box->addButton(rpl::single(QString()), [=] {
const auto emojiStatuses = &session->data().emojiStatuses();
const auto id = emojiStatuses->fromUniqueGift(gift);
if (!peer->isSelf()) {
if (*checking) {
return;
}
*checking = true;
const auto weak = Ui::MakeWeak(box);
CheckBoostLevel(show, peer, [=](int level) {
const auto limits = Data::LevelLimits(&peer->session());
const auto wanted = limits.channelEmojiStatusLevelMin();
if (level >= wanted) {
if (const auto strong = weak.data()) {
strong->closeBox();
}
emojiStatuses->set(peer, id);
return std::optional<Ui::AskBoostReason>();
}
const auto reason = [&]() -> Ui::AskBoostReason {
return { Ui::AskBoostWearCollectible{
wanted
} };
}();
return std::make_optional(reason);
}, [=] { *checking = false; });
} else if (session->premium()) {
box->closeBox();
emojiStatuses->set(peer, id);
} else {
const auto link = Ui::Text::Bold(
tr::lng_send_as_premium_required_link(tr::now));
Settings::ShowPremiumPromoToast(
show,
tr::lng_gift_wear_subscribe(
tr::now,
lt_link,
Ui::Text::Link(link),
Ui::Text::WithEntities),
u"wear_collectibles"_q);
}
});
const auto lock = Ui::Text::SingleCustomEmoji(
session->data().customEmojiManager().registerInternalEmoji(
st::historySendDisabledIcon,
st::giftBoxLockMargins,
true));
auto label = rpl::combine(
tr::lng_gift_wear_start(),
Data::AmPremiumValue(&show->session())
) | rpl::map([=](const QString &text, bool premium) {
auto result = TextWithEntities();
if (!premium && peer->isSelf()) {
result.append(lock);
}
result.append(text);
return result;
});
SetButtonMarkedLabel(
button,
std::move(label),
session,
st::creditsBoxButtonLabel,
&st::giftBox.button.textFg);
rpl::combine(
box->widthValue(),
button->widthValue()
) | rpl::start_with_next([=](int outer, int inner) {
const auto padding = st::giftBox.buttonPadding;
const auto wanted = outer - padding.left() - padding.right();
if (inner != wanted) {
button->resizeToWidth(wanted);
button->moveToLeft(padding.left(), padding.top());
}
}, box->lifetime());
AddUniqueCloseButton(box, {});
}));
}
struct UpgradeArgs : StarGiftUpgradeArgs {
std::vector<Data::UniqueGiftModel> models;
std::vector<Data::UniqueGiftPattern> patterns;
std::vector<Data::UniqueGiftBackdrop> backdrops;
};
[[nodiscard]] rpl::producer<Data::UniqueGift> MakeUpgradeGiftStream(
const UpgradeArgs &args) {
if (args.models.empty()
|| args.patterns.empty()
|| args.backdrops.empty()) {
return rpl::never<Data::UniqueGift>();
}
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
struct State {
UpgradeArgs data;
std::vector<int> modelIndices;
std::vector<int> patternIndices;
std::vector<int> backdropIndices;
};
const auto state = lifetime.make_state<State>(State{
.data = args,
});
const auto put = [=] {
const auto index = [](std::vector<int> &indices, const auto &v) {
const auto fill = [&] {
if (!indices.empty()) {
return;
}
indices = ranges::views::ints(0) | ranges::views::take(
v.size()
) | ranges::to_vector;
ranges::shuffle(indices);
};
fill();
const auto result = indices.back();
indices.pop_back();
fill();
if (indices.back() == result) {
std::swap(indices.front(), indices.back());
}
return result;
};
auto &models = state->data.models;
auto &patterns = state->data.patterns;
auto &backdrops = state->data.backdrops;
consumer.put_next(Data::UniqueGift{
.title = (state->data.savedId
? tr::lng_gift_upgrade_title(tr::now)
: tr::lng_gift_upgrade_preview_title(tr::now)),
.model = models[index(state->modelIndices, models)],
.pattern = patterns[index(state->patternIndices, patterns)],
.backdrop = backdrops[index(state->backdropIndices, backdrops)],
});
};
put();
base::timer_each(
kSwitchUpgradeCoverInterval / 3
) | rpl::start_with_next(put, lifetime);
return lifetime;
};
}
void AddUpgradeGiftCover(
not_null<VerticalLayout*> container,
const UpgradeArgs &args) {
AddUniqueGiftCover(
container,
MakeUpgradeGiftStream(args),
(args.savedId
? tr::lng_gift_upgrade_about()
: (args.peer->isBroadcast()
? tr::lng_gift_upgrade_preview_about_channel
: tr::lng_gift_upgrade_preview_about)(
lt_name,
rpl::single(args.peer->shortName()))));
}
void UpgradeBox(
not_null<GenericBox*> box,
not_null<Window::SessionController*> controller,
UpgradeArgs &&args) {
box->setNoContentMargin(true);
const auto container = box->verticalLayout();
AddUpgradeGiftCover(container, args);
AddSkip(container, st::defaultVerticalListSkip * 2);
const auto infoRow = [&](
rpl::producer<QString> title,
rpl::producer<QString> text,
not_null<const style::icon*> icon) {
auto raw = container->add(
object_ptr<Ui::VerticalLayout>(container));
raw->add(
object_ptr<Ui::FlatLabel>(
raw,
std::move(title) | Ui::Text::ToBold(),
st::defaultFlatLabel),
st::settingsPremiumRowTitlePadding);
raw->add(
object_ptr<Ui::FlatLabel>(
raw,
std::move(text),
st::upgradeGiftSubtext),
st::settingsPremiumRowAboutPadding);
object_ptr<Info::Profile::FloatingIcon>(
raw,
*icon,
st::starrefInfoIconPosition);
};
infoRow(
tr::lng_gift_upgrade_unique_title(),
tr::lng_gift_upgrade_unique_about(),
&st::menuIconUnique);
infoRow(
tr::lng_gift_upgrade_transferable_title(),
tr::lng_gift_upgrade_transferable_about(),
&st::menuIconReplace);
infoRow(
tr::lng_gift_upgrade_tradable_title(),
tr::lng_gift_upgrade_tradable_about(),
&st::menuIconTradable);
struct State {
bool sent = false;
bool preserveDetails = false;
};
const auto state = std::make_shared<State>();
const auto preview = !args.savedId;
if (!preview) {
const auto skip = st::defaultVerticalListSkip;
container->add(
object_ptr<PlainShadow>(container),
st::boxRowPadding + QMargins(0, skip, 0, skip));
const auto checkbox = container->add(
object_ptr<CenterWrap<Checkbox>>(
container,
object_ptr<Checkbox>(
container,
(args.canAddComment
? tr::lng_gift_upgrade_add_comment(tr::now)
: args.canAddSender
? tr::lng_gift_upgrade_add_sender(tr::now)
: args.canAddMyComment
? tr::lng_gift_upgrade_add_my_comment(tr::now)
: tr::lng_gift_upgrade_add_my(tr::now)),
args.addDetailsDefault)),
st::defaultCheckbox.margin)->entity();
checkbox->checkedChanges() | rpl::start_with_next([=](bool checked) {
state->preserveDetails = checked;
}, checkbox->lifetime());
}
box->setStyle(preview ? st::giftBox : st::upgradeGiftBox);
const auto cost = args.cost;
const auto session = &controller->session();
auto buttonText = preview ? tr::lng_box_ok() : rpl::single(QString());
const auto button = box->addButton(std::move(buttonText), [=] {
if (preview) {
box->closeBox();
return;
} else if (state->sent) {
return;
}
state->sent = true;
const auto keepDetails = state->preserveDetails;
const auto weak = Ui::MakeWeak(box);
const auto done = [=](Payments::CheckoutResult result) {
if (result != Payments::CheckoutResult::Paid) {
state->sent = false;
} else {
controller->showPeerHistory(args.peer);
if (const auto strong = weak.data()) {
strong->closeBox();
}
}
};
UpgradeGift(controller, args.savedId, keepDetails, cost, done);
});
if (!preview) {
auto star = session->data().customEmojiManager().creditsEmoji();
SetButtonMarkedLabel(
button,
(cost
? tr::lng_gift_upgrade_button(
lt_price,
rpl::single(star.append(
' ' + Lang::FormatStarsAmountDecimal(
StarsAmount{ cost }))),
Ui::Text::WithEntities)
: tr::lng_gift_upgrade_confirm(Ui::Text::WithEntities)),
&controller->session(),
st::creditsBoxButtonLabel,
&st::giftBox.button.textFg);
}
rpl::combine(
box->widthValue(),
button->widthValue()
) | rpl::start_with_next([=](int outer, int inner) {
const auto padding = st::giftBox.buttonPadding;
const auto wanted = outer - padding.left() - padding.right();
if (inner != wanted) {
button->resizeToWidth(wanted);
button->moveToLeft(padding.left(), padding.top());
}
}, box->lifetime());
AddUniqueCloseButton(box, {});
}
const std::vector<PatternPoint> &PatternPoints() {
static const auto kSmall = 0.7;
static const auto kFaded = 0.2;
static const auto kLarge = 0.85;
static const auto kOpaque = 0.3;
static const auto result = std::vector<PatternPoint>{
{ { 0.5, 0.066 }, kSmall, kFaded },
{ { 0.177, 0.168 }, kSmall, kFaded },
{ { 0.822, 0.168 }, kSmall, kFaded },
{ { 0.37, 0.168 }, kLarge, kOpaque },
{ { 0.63, 0.168 }, kLarge, kOpaque },
{ { 0.277, 0.308 }, kSmall, kOpaque },
{ { 0.723, 0.308 }, kSmall, kOpaque },
{ { 0.13, 0.42 }, kSmall, kFaded },
{ { 0.87, 0.42 }, kSmall, kFaded },
{ { 0.27, 0.533 }, kLarge, kOpaque },
{ { 0.73, 0.533 }, kLarge, kOpaque },
{ { 0.2, 0.73 }, kSmall, kFaded },
{ { 0.8, 0.73 }, kSmall, kFaded },
{ { 0.302, 0.825 }, kLarge, kOpaque },
{ { 0.698, 0.825 }, kLarge, kOpaque },
{ { 0.5, 0.876 }, kLarge, kFaded },
{ { 0.144, 0.936 }, kSmall, kFaded },
{ { 0.856, 0.936 }, kSmall, kFaded },
};
return result;
}
const std::vector<PatternPoint> &PatternPointsSmall() {
static const auto kSmall = 0.45;
static const auto kFaded = 0.2;
static const auto kLarge = 0.55;
static const auto kOpaque = 0.3;
static const auto result = std::vector<PatternPoint>{
{ { 0.5, 0.066 }, kSmall, kFaded },
{ { 0.177, 0.168 }, kSmall, kFaded },
{ { 0.822, 0.168 }, kSmall, kFaded },
{ { 0.37, 0.168 }, kLarge, kOpaque },
{ { 0.63, 0.168 }, kLarge, kOpaque },
{ { 0.277, 0.308 }, kSmall, kOpaque },
{ { 0.723, 0.308 }, kSmall, kOpaque },
{ { 0.13, 0.42 }, kSmall, kFaded },
{ { 0.87, 0.42 }, kSmall, kFaded },
{ { 0.27, 0.533 }, kLarge, kOpaque },
{ { 0.73, 0.533 }, kLarge, kOpaque },
{ { 0.2, 0.73 }, kSmall, kFaded },
{ { 0.8, 0.73 }, kSmall, kFaded },
{ { 0.302, 0.825 }, kLarge, kOpaque },
{ { 0.698, 0.825 }, kLarge, kOpaque },
{ { 0.5, 0.876 }, kLarge, kFaded },
{ { 0.144, 0.936 }, kSmall, kFaded },
{ { 0.856, 0.936 }, kSmall, kFaded },
};
return result;
}
void PaintPoints(
QPainter &p,
const std::vector<PatternPoint> &points,
base::flat_map<float64, QImage> &cache,
not_null<Text::CustomEmoji*> emoji,
const Data::UniqueGift &gift,
const QRect &rect,
float64 shown) {
const auto origin = rect.topLeft();
const auto width = rect.width();
const auto height = rect.height();
const auto ratio = style::DevicePixelRatio();
const auto paintPoint = [&](const PatternPoint &point) {
const auto key = (1. + point.opacity) * 10. + point.scale;
auto &image = cache[key];
PrepareImage(image, emoji, point, gift);
if (!image.isNull()) {
const auto position = origin + QPoint(
int(point.position.x() * width),
int(point.position.y() * height));
if (shown < 1.) {
p.save();
p.translate(position);
p.scale(shown, shown);
p.translate(-position);
}
const auto size = image.size() / ratio;
p.drawImage(
position - QPoint(size.width() / 2, size.height() / 2),
image);
if (shown < 1.) {
p.restore();
}
}
};
for (const auto &point : points) {
paintPoint(point);
}
}
void ShowStarGiftUpgradeBox(StarGiftUpgradeArgs &&args) {
const auto weak = base::make_weak(args.controller);
const auto session = &args.peer->session();
session->api().request(MTPpayments_GetStarGiftUpgradePreview(
MTP_long(args.stargiftId)
)).done([=](const MTPpayments_StarGiftUpgradePreview &result) {
const auto strong = weak.get();
if (!strong) {
if (const auto onstack = args.ready) {
onstack(false);
}
return;
}
const auto &data = result.data();
auto upgrade = UpgradeArgs{ args };
for (const auto &attribute : data.vsample_attributes().v) {
attribute.match([&](const MTPDstarGiftAttributeModel &data) {
upgrade.models.push_back(Api::FromTL(session, data));
}, [&](const MTPDstarGiftAttributePattern &data) {
upgrade.patterns.push_back(Api::FromTL(session, data));
}, [&](const MTPDstarGiftAttributeBackdrop &data) {
upgrade.backdrops.push_back(Api::FromTL(data));
}, [](const auto &) {});
}
strong->show(Box(UpgradeBox, strong, std::move(upgrade)));
if (const auto onstack = args.ready) {
onstack(true);
}
}).fail([=](const MTP::Error &error) {
if (const auto strong = weak.get()) {
strong->showToast(error.type());
}
if (const auto onstack = args.ready) {
onstack(false);
}
}).send();
}
void AddUniqueCloseButton(
not_null<GenericBox*> box,
Settings::CreditsEntryBoxStyleOverrides st,
Fn<void(not_null<Ui::PopupMenu*>)> fillMenu) {
const auto close = Ui::CreateChild<IconButton>(
box,
st::uniqueCloseButton);
const auto menu = fillMenu
? Ui::CreateChild<IconButton>(box, st::uniqueMenuButton)
: nullptr;
close->show();
close->raise();
if (menu) {
menu->show();
menu->raise();
}
box->widthValue() | rpl::start_with_next([=](int width) {
close->moveToRight(0, 0, width);
close->raise();
if (menu) {
menu->moveToRight(close->width(), 0, width);
menu->raise();
}
}, close->lifetime());
close->setClickedCallback([=] {
box->closeBox();
});
if (menu) {
const auto state = menu->lifetime().make_state<
base::unique_qptr<Ui::PopupMenu>
>();
menu->setClickedCallback([=] {
if (*state) {
*state = nullptr;
return;
}
*state = base::make_unique_q<Ui::PopupMenu>(
menu,
st.menu ? *st.menu : st::popupMenuWithIcons);
fillMenu(state->get());
if (!(*state)->empty()) {
(*state)->popup(QCursor::pos());
}
});
}
}
void RequestStarsFormAndSubmit(
not_null<Window::SessionController*> window,
MTPInputInvoice invoice,
Fn<void(Payments::CheckoutResult, const MTPUpdates *)> done) {
const auto weak = base::make_weak(window);
window->session().api().request(MTPpayments_GetPaymentForm(
MTP_flags(0),
invoice,
MTPDataJSON() // theme_params
)).done([=](const MTPpayments_PaymentForm &result) {
result.match([&](const MTPDpayments_paymentFormStarGift &data) {
const auto formId = data.vform_id().v;
const auto prices = data.vinvoice().data().vprices().v;
const auto strong = weak.get();
if (!strong) {
done(Payments::CheckoutResult::Failed, nullptr);
return;
}
const auto ready = [=](Settings::SmallBalanceResult result) {
SendStarsFormRequest(strong, result, formId, invoice, done);
};
Settings::MaybeRequestBalanceIncrease(
strong->uiShow(),
prices.front().data().vamount().v,
Settings::SmallBalanceDeepLink{},
ready);
}, [&](const auto &) {
done(Payments::CheckoutResult::Failed, nullptr);
});
}).fail([=](const MTP::Error &error) {
const auto type = error.type();
if (type == u"STARGIFT_EXPORT_IN_PROGRESS"_q) {
done(Payments::CheckoutResult::Cancelled, nullptr);
} else {
if (const auto strong = weak.get()) {
strong->showToast(type);
}
done(Payments::CheckoutResult::Failed, nullptr);
}
}).send();
}
void ShowGiftTransferredToast(
base::weak_ptr<Window::SessionController> weak,
not_null<PeerData*> to,
const Data::UniqueGift &gift) {
if (const auto strong = weak.get()) {
strong->showToast({
.title = tr::lng_gift_transferred_title(tr::now),
.text = tr::lng_gift_transferred_about(
tr::now,
lt_name,
Text::Bold(Data::UniqueGiftName(gift)),
lt_recipient,
Text::Bold(to->shortName()),
Ui::Text::WithEntities),
.duration = kUpgradeDoneToastDuration,
});
}
}
} // namespace Ui