From 46e7b6d6df82b76815f7a3b230e2b10da9824de7 Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 19 Mar 2025 18:25:50 +0400 Subject: [PATCH] Implement "Choose gift to unpin" box feature. --- Telegram/Resources/langs/lang.strings | 4 + Telegram/SourceFiles/api/api_premium.cpp | 3 +- Telegram/SourceFiles/boxes/star_gift_box.cpp | 2 +- .../view/media/history_view_premium_gift.cpp | 3 +- .../peer_gifts/info_peer_gifts_common.cpp | 283 +++++++++++++++--- .../info/peer_gifts/info_peer_gifts_common.h | 32 +- .../peer_gifts/info_peer_gifts_widget.cpp | 68 +++-- .../settings/settings_credits_graphics.cpp | 116 +++++-- .../settings/settings_credits_graphics.h | 7 +- Telegram/SourceFiles/ui/effects/credits.style | 8 + 10 files changed, 419 insertions(+), 107 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 5d4370b535..af2a2d6abd 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -3429,7 +3429,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_display_done_channel" = "The gift is now shown in channel's Gifts."; "lng_gift_display_done_hide" = "The gift is now hidden from your profile page."; "lng_gift_display_done_hide_channel" = "The gift is now hidden from channel's Gifts."; +"lng_gift_pinned_done_title" = "{gift} pinned"; "lng_gift_pinned_done" = "The gift will always be shown on top."; +"lng_gift_pinned_done_replaced" = "replacing {gift}"; "lng_gift_got_stars#one" = "You got **{count} Star** for this gift."; "lng_gift_got_stars#other" = "You got **{count} Stars** for this gift."; "lng_gift_channel_got#one" = "Channel got **{count} Star** for this gift."; @@ -3502,6 +3504,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_wear_subscribe" = "Subscribe to {link} to wear collectibles."; "lng_gift_wear_start_toast" = "You put on {name}"; "lng_gift_wear_end_toast" = "You took off {name}"; +"lng_gift_many_pinned_title" = "Too Many Pinned Gifts"; +"lng_gift_many_pinned_choose" = "Select a gift to unpin below"; "lng_accounts_limit_title" = "Limit Reached"; "lng_accounts_limit1#one" = "You have reached the limit of **{count}** connected account."; diff --git a/Telegram/SourceFiles/api/api_premium.cpp b/Telegram/SourceFiles/api/api_premium.cpp index 61769069fa..be3ead5afc 100644 --- a/Telegram/SourceFiles/api/api_premium.cpp +++ b/Telegram/SourceFiles/api/api_premium.cpp @@ -883,6 +883,7 @@ std::optional FromTL( unique->exportAt = data.vcan_export_at().value_or_empty(); } using Id = Data::SavedStarGiftId; + const auto hasUnique = parsed->unique != nullptr; return Data::SavedStarGift{ .info = std::move(*parsed), .manageId = (to->isUser() @@ -905,7 +906,7 @@ std::optional FromTL( .date = data.vdate().v, .upgradable = data.is_can_upgrade(), .anonymous = data.is_name_hidden(), - .pinned = data.is_pinned_to_top(), + .pinned = data.is_pinned_to_top() && hasUnique, .hidden = data.is_unsaved(), .mine = to->isSelf(), }; diff --git a/Telegram/SourceFiles/boxes/star_gift_box.cpp b/Telegram/SourceFiles/boxes/star_gift_box.cpp index cb3fdf2520..59abb78468 100644 --- a/Telegram/SourceFiles/boxes/star_gift_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_box.cpp @@ -1725,7 +1725,7 @@ void SendGiftBox( bool sending = false; }; const auto state = raw->lifetime().make_state(State{ - .delegate = Delegate(window, GiftButtonMode::Full), + .delegate = Delegate(&window->session(), GiftButtonMode::Full), }); const auto single = state->delegate.buttonSize(); const auto shadow = st::defaultDropdownMenu.wrap.shadow; diff --git a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp index 700d6a1237..f9a9b031ca 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_premium_gift.cpp @@ -479,7 +479,8 @@ ClickHandlerPtr OpenStarGiftLink(not_null item) { Settings::SavedStarGiftBox, window, owner, - *parsed)); + *parsed, + nullptr)); } } }).fail([=](const MTP::Error &error) { diff --git a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp index 803e7c4330..4118649948 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp @@ -7,20 +7,25 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "info/peer_gifts/info_peer_gifts_common.h" +#include "boxes/send_credits_box.h" // SetButtonMarkedLabel #include "boxes/star_gift_box.h" #include "boxes/sticker_set_box.h" #include "chat_helpers/stickers_gift_box_pack.h" #include "chat_helpers/stickers_lottie.h" #include "core/ui_integration.h" #include "data/stickers/data_custom_emoji.h" +#include "data/data_credits.h" // CreditsHistoryEntry #include "data/data_document.h" #include "data/data_document_media.h" #include "data/data_session.h" #include "history/view/media/history_view_sticker_player.h" #include "lang/lang_keys.h" #include "main/main_session.h" +#include "settings/settings_credits_graphics.h" +#include "ui/layers/generic_box.h" #include "ui/text/format_values.h" #include "ui/text/text_utilities.h" +#include "ui/widgets/buttons.h" #include "ui/dynamic_image.h" #include "ui/dynamic_thumbnails.h" #include "ui/effects/premium_graphics.h" @@ -240,6 +245,76 @@ void GiftButton::setGeometry(QRect inner, QMargins extend) { AbstractButton::setGeometry(inner.marginsAdded(extend)); } +QMargins GiftButton::currentExtend() const { + const auto progress = _selectedAnimation.value(_selected ? 1. : 0.); + const auto added = anim::interpolate(0, st::giftBoxSelectSkip, progress); + return _extend + QMargins(added, added, added, added); +} + +void GiftButton::toggleSelected(bool selected) { + if (_selected == selected) { + return; + } + const auto duration = st::defaultRoundCheckbox.duration; + _selected = selected; + _selectedAnimation.start([=] { + update(); + }, selected ? 0. : 1., selected ? 1. : 0., duration, anim::easeOutCirc); +} + +void GiftButton::paintBackground(QPainter &p, const QImage &background) { + const auto removed = currentExtend() - _extend; + const auto x = removed.left(); + const auto y = removed.top(); + const auto width = this->width() - x - removed.right(); + const auto height = this->height() - y - removed.bottom(); + const auto dpr = int(background.devicePixelRatio()); + const auto bwidth = background.width() / dpr; + const auto bheight = background.height() / dpr; + const auto fillRow = [&](int yfrom, int ytill, int bfrom) { + const auto fill = [&](int xto, int wto, int xfrom, int wfrom = 0) { + const auto fheight = ytill - yfrom; + p.drawImage( + QRect(x + xto, y + yfrom, wto, fheight), + background, + QRect( + QPoint(xfrom, bfrom) * dpr, + QSize((wfrom ? wfrom : wto), fheight) * dpr)); + }; + if (width < bwidth) { + const auto xhalf = width / 2; + fill(0, xhalf, 0); + fill(xhalf, width - xhalf, bwidth - (width - xhalf)); + } else if (width == bwidth) { + fill(0, width, 0); + } else { + const auto half = bwidth / (2 * dpr); + fill(0, half, 0); + fill(width - half, half, bwidth - half); + fill(half, width - 2 * half, half, 1); + } + }; + if (height < bheight) { + fillRow(0, height / 2, 0); + fillRow(height / 2, height, bheight - (height - (height / 2))); + } else { + fillRow(0, height, 0); + } + + auto hq = PainterHighQualityEnabler(p); + const auto progress = _selectedAnimation.value(_selected ? 1. : 0.); + const auto pwidth = progress * st::defaultRoundCheckbox.width; + p.setPen(QPen(st::defaultRoundCheckbox.bgActive->c, pwidth)); + p.setBrush(Qt::NoBrush); + const auto rounded = rect().marginsRemoved(_extend); + const auto phalf = pwidth / 2.; + const auto extended = QRectF(rounded).marginsRemoved( + { phalf, phalf, phalf, phalf }); + const auto xradius = removed.left() + st::giftBoxGiftRadius - phalf; + const auto yradius = removed.top() + st::giftBoxGiftRadius - phalf; + p.drawRoundedRect(extended, xradius, yradius); +} + void GiftButton::resizeEvent(QResizeEvent *e) { if (!_button.isEmpty()) { _button.moveLeft((width() - _button.width()) / 2); @@ -267,9 +342,10 @@ void GiftButton::cacheUniqueBackground( [[maybe_unused]] const auto preload = _uniquePatternEmoji->ready(); } const auto outer = QRect(0, 0, width, height); + const auto extend = currentExtend(); const auto inner = outer.marginsRemoved( - _extend - ).translated(-_extend.left(), -_extend.top()); + extend + ).translated(-extend.left(), -extend.top()); const auto ratio = style::DevicePixelRatio(); if (_uniqueBackgroundCache.size() != inner.size() * ratio) { _uniqueBackgroundCache = QImage( @@ -313,32 +389,15 @@ void GiftButton::paintEvent(QPaintEvent *e) { : nullptr; const auto hidden = v::is(_descriptor) && v::get(_descriptor).hidden;; - const auto position = QPoint(_extend.left(), _extend.top()); + const auto extend = currentExtend(); + const auto position = QPoint(extend.left(), extend.top()); const auto background = _delegate->background(); - const auto dpr = int(background.devicePixelRatio()); const auto width = this->width(); - if (width * dpr <= background.width()) { - p.drawImage(0, 0, background); - } else { - const auto full = background.width(); - const auto half = ((full / 2) / dpr) * dpr; - const auto height = background.height(); - p.drawImage( - QRect(0, 0, half / dpr, height / dpr), - background, - QRect(0, 0, half, height)); - p.drawImage( - QRect(width - (half / dpr), 0, half / dpr, height / dpr), - background, - QRect(full - half, 0, half, height)); - p.drawImage( - QRect(half / dpr, 0, width - 2 * (half / dpr), height / dpr), - background, - QRect(half, 0, 1, height)); - } + const auto dpr = int(background.devicePixelRatio()); + paintBackground(p, background); if (unique) { cacheUniqueBackground(unique, width, background.height() / dpr); - p.drawImage(_extend.left(), _extend.top(), _uniqueBackgroundCache); + p.drawImage(extend.left(), extend.top(), _uniqueBackgroundCache); } if (_userpic) { @@ -348,7 +407,7 @@ void GiftButton::paintEvent(QPaintEvent *e) { } const auto image = _userpic->image(st::giftBoxUserpicSize); const auto skip = st::giftBoxUserpicSkip; - p.drawImage(_extend.left() + skip, _extend.top() + skip, image); + p.drawImage(extend.left() + skip, extend.top() + skip, image); } auto frame = QImage(); @@ -401,7 +460,7 @@ void GiftButton::paintEvent(QPaintEvent *e) { auto hq = PainterHighQualityEnabler(p); const auto premium = v::is(_descriptor); - const auto singlew = width - _extend.left() - _extend.right(); + const auto singlew = width - extend.left() - extend.right(); const auto font = st::semiboldFont; p.setFont(font); @@ -420,11 +479,17 @@ void GiftButton::paintEvent(QPaintEvent *e) { } return GiftBadge(); }, [&](const GiftTypeStars &data) { - if (const auto count = data.info.limitedCount) { - const auto soldOut = !data.userpic && !data.info.limitedLeft; + const auto count = data.info.limitedCount; + const auto pinned = data.pinned || data.pinnedSelection; + if (count || pinned) { + const auto soldOut = !pinned + && !data.userpic + && !data.info.limitedLeft; return GiftBadge{ .text = (soldOut ? tr::lng_gift_stars_sold_out(tr::now) + : (unique && pinned) + ? ('#' + QString::number(unique->number)) : (!data.userpic && !data.info.unique) ? tr::lng_gift_stars_limited(tr::now) : (count == 1) @@ -452,7 +517,7 @@ void GiftButton::paintEvent(QPaintEvent *e) { if (badge) { const auto rubberOut = st::lineWidth; - const auto inner = rect().marginsRemoved(_extend); + const auto inner = rect().marginsRemoved(extend); p.setClipRect(inner.marginsAdded( { rubberOut, rubberOut, rubberOut, rubberOut })); @@ -474,7 +539,7 @@ void GiftButton::paintEvent(QPaintEvent *e) { p.setPen(Qt::NoPen); p.setBrush(unique->backdrop.patternColor); const auto rect = QRect( - QPoint(_extend.left() + skip, _extend.top() + skip), + QPoint(extend.left() + skip, extend.top() + skip), QSize(icon.width() + 2 * add, icon.height() + 2 * add)); p.drawEllipse(rect); icon.paintInCenter(p, rect); @@ -547,12 +612,10 @@ void GiftButton::paintEvent(QPaintEvent *e) { } } -Delegate::Delegate( - not_null window, - GiftButtonMode mode) -: _window(window) +Delegate::Delegate(not_null session, GiftButtonMode mode) +: _session(session) , _hiddenMark(std::make_unique( - &window->session(), + _session, st::giftBoxHiddenMark, RectPart::Center)) , _mode(mode) { @@ -563,18 +626,17 @@ Delegate::Delegate(Delegate &&other) = default; Delegate::~Delegate() = default; TextWithEntities Delegate::star() { - const auto owner = &_window->session().data(); - return owner->customEmojiManager().creditsEmoji(); + return _session->data().customEmojiManager().creditsEmoji(); } TextWithEntities Delegate::ministar() { - const auto owner = &_window->session().data(); + const auto owner = &_session->data(); const auto top = st::giftBoxByStarsStarTop; return owner->customEmojiManager().ministarEmoji({ 0, top, 0, 0 }); } Ui::Text::MarkedContext Delegate::textContext() { - return Core::TextContext({ .session = &_window->session() }); + return Core::TextContext({ .session = _session }); } QSize Delegate::buttonSize() { @@ -601,7 +663,7 @@ auto Delegate::buttonPatternEmoji( not_null unique, Fn repaint) -> std::unique_ptr { - return _window->session().data().customEmojiManager().create( + return _session->data().customEmojiManager().create( unique->pattern.document, repaint, Data::CustomEmojiSizeTag::Large); @@ -658,7 +720,7 @@ QImage Delegate::background() { rpl::producer> Delegate::sticker( const GiftDescriptor &descriptor) { - return GiftStickerValue(&_window->session(), descriptor); + return GiftStickerValue(_session, descriptor); } not_null Delegate::hiddenMark() { @@ -784,4 +846,143 @@ QImage ValidateRotatedBadge(const GiftBadge &badge, int added) { return result; } +void SelectGiftToUnpin( + std::shared_ptr show, + const std::vector &pinned, + Fn chosen) { + show->show(Box([=](not_null box) { + struct State { + explicit State(not_null session) + : delegate(session, GiftButtonMode::Minimal) { + } + + Delegate delegate; + rpl::variable selected = -1; + std::vector> buttons; + }; + const auto session = &show->session(); + const auto state = box->lifetime().make_state(session); + + box->setStyle(st::giftTooManyPinnedBox); + box->setWidth(st::boxWideWidth); + + box->addRow( + object_ptr( + box, + tr::lng_gift_many_pinned_title(), + st::giftBoxSubtitle), + st::giftBoxSubtitleMargin); + box->addRow( + object_ptr( + box, + tr::lng_gift_many_pinned_choose(), + st::giftTooManyPinnedChoose), + st::giftBoxAboutMargin); + + const auto gifts = box->addRow( + object_ptr(box), + QMargins( + st::giftBoxPadding.left(), + st::giftTooManyPinnedBox.buttonPadding.top(), + st::giftBoxPadding.right(), + 0)); + for (const auto &entry : pinned) { + const auto index = int(state->buttons.size()); + state->buttons.push_back( + Ui::CreateChild(gifts, &state->delegate)); + const auto button = state->buttons.back(); + button->setDescriptor(GiftTypeStars{ + .info = { + .id = entry.stargiftId, + .unique = entry.uniqueGift, + .document = entry.uniqueGift->model.document, + }, + .pinnedSelection = true, + }, GiftButton::Mode::Minimal); + button->setClickedCallback([=] { + const auto now = state->selected.current(); + state->selected = (now == index) ? -1 : index; + }); + } + + state->selected.value( + ) | rpl::combine_previous( + ) | rpl::start_with_next([=](int old, int now) { + if (old >= 0) state->buttons[old]->toggleSelected(false); + if (now >= 0) state->buttons[now]->toggleSelected(true); + }, gifts->lifetime()); + + gifts->widthValue() | rpl::start_with_next([=](int width) { + const auto singleMin = state->delegate.buttonSize(); + if (width < singleMin.width()) { + return; + } + const auto count = int(state->buttons.size()); + const auto skipw = st::giftBoxGiftSkip.x(); + const auto skiph = st::giftBoxGiftSkip.y(); + const auto perRow = std::min( + (width + skipw) / (singleMin.width() + skipw), + std::max(count, 1)); + if (perRow <= 0) { + return; + } + const auto single = (width - (perRow - 1) * skipw) / perRow; + const auto height = singleMin.height(); + const auto rows = (count + perRow - 1) / perRow; + for (auto row = 0; row != rows; ++row) { + const auto y = row * (height + skiph); + for (auto column = 0; column != perRow; ++column) { + const auto index = row * perRow + column; + if (index >= count) { + break; + } + const auto &button = state->buttons[index]; + const auto x = column * (single + skipw); + button->setGeometry( + QRect(x, y, single, height), + state->delegate.buttonExtend()); + } + } + gifts->resize(width, rows * (height + skiph) - skiph); + }, gifts->lifetime()); + + const auto button = box->addButton(rpl::single(QString()), [=] { + const auto index = state->selected.current(); + if (index < 0) { + return; + } + Assert(index < int(pinned.size())); + const auto &entry = pinned[index]; + const auto weak = Ui::MakeWeak(box); + chosen(::Settings::EntryToSavedStarGiftId(session, entry)); + if (const auto strong = weak.data()) { + strong->closeBox(); + } + }); + const auto label = Ui::SetButtonMarkedLabel( + button, + tr::lng_context_unpin_from_top(Ui::Text::WithEntities), + &show->session(), + st::creditsBoxButtonLabel, + &st::giftTooManyPinnedBox.button.textFg); + + state->selected.value() | rpl::start_with_next([=](int value) { + const auto has = (value >= 0); + label->setOpacity(has ? 1. : 0.5); + button->setAttribute(Qt::WA_TransparentForMouseEvents, !has); + }, box->lifetime()); + + const auto buttonPadding = st::giftTooManyPinnedBox.buttonPadding; + const auto buttonWidth = st::boxWideWidth + - buttonPadding.left() + - buttonPadding.right(); + button->resizeToWidth(buttonWidth); + button->widthValue() | rpl::start_with_next([=](int width) { + if (width != buttonWidth) { + button->resizeToWidth(buttonWidth); + } + }, button->lifetime()); + })); +} + } // namespace Info::PeerGifts diff --git a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h index 0d93a8bf21..2c0ae5ef1c 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h @@ -15,8 +15,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL class StickerPremiumMark; +namespace ChatHelpers { +class Show; +} // namespace ChatHelpers + namespace Data { struct UniqueGift; +struct CreditsHistoryEntry; +class SavedStarGiftId; } // namespace Data namespace HistoryView { @@ -57,10 +63,11 @@ struct GiftTypeStars { Data::StarGift info; PeerData *from = nullptr; TimeId date = 0; - bool userpic = false; - bool pinned = false; - bool hidden = false; - bool mine = false; + bool pinnedSelection : 1 = false; + bool userpic : 1 = false; + bool pinned : 1 = false; + bool hidden : 1 = false; + bool mine : 1 = false; [[nodiscard]] friend inline bool operator==( const GiftTypeStars&, @@ -128,6 +135,8 @@ public: void setDescriptor(const GiftDescriptor &descriptor, Mode mode); void setGeometry(QRect inner, QMargins extend); + void toggleSelected(bool selected); + [[nodiscard]] rpl::producer contextMenuRequests() const { return _contextMenuRequests.events(); } @@ -137,6 +146,7 @@ private: void resizeEvent(QResizeEvent *e) override; void contextMenuEvent(QContextMenuEvent *e) override; + void paintBackground(QPainter &p, const QImage &background); void cacheUniqueBackground( not_null unique, int width, @@ -144,6 +154,7 @@ private: void setDocument(not_null document); [[nodiscard]] bool documentResolved() const; + [[nodiscard]] QMargins currentExtend() const; void unsubscribe(); @@ -159,8 +170,10 @@ private: std::unique_ptr _uniquePatternEmoji; base::flat_map _uniquePatternCache; std::optional _stars; + Ui::Animations::Simple _selectedAnimation; bool _subscribed = false; bool _patterned = false; + bool _selected = false; bool _small = false; QRect _button; @@ -173,9 +186,7 @@ private: class Delegate final : public GiftButtonDelegate { public: - Delegate( - not_null window, - GiftButtonMode mode); + Delegate(not_null session, GiftButtonMode mode); Delegate(Delegate &&other); ~Delegate(); @@ -195,7 +206,7 @@ public: QImage cachedBadge(const GiftBadge &badge) override; private: - const not_null _window; + const not_null _session; std::unique_ptr _hiddenMark; base::flat_map _badges; QSize _single; @@ -214,4 +225,9 @@ private: [[nodiscard]] QImage ValidateRotatedBadge(const GiftBadge &badge, int added); +void SelectGiftToUnpin( + std::shared_ptr show, + const std::vector &pinned, + Fn chosen); + } // namespace Info::PeerGifts diff --git a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp index 683501acf6..88745c6b92 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp @@ -112,6 +112,9 @@ private: int resizeGetHeight(int width) override; + [[nodiscard]] auto pinnedSavedGifts() + -> Fn()>; + const not_null _window; rpl::variable _filter; Delegate _delegate; @@ -152,7 +155,7 @@ InnerWidget::InnerWidget( : BoxContentDivider(parent) , _window(controller->parentController()) , _filter(std::move(filter)) -, _delegate(_window, GiftButtonMode::Minimal) +, _delegate(&_window->session(), GiftButtonMode::Minimal) , _controller(controller) , _peer(peer) , _totalCount(_peer->peerGiftsCount()) @@ -225,6 +228,9 @@ void InnerWidget::subscribeToUpdates() { return; } refreshButtons(); + if (update.action == Action::Pin) { + _scrollToTop.fire({}); + } }, lifetime()); } @@ -476,6 +482,41 @@ void InnerWidget::validateButtons() { std::swap(_views, views); } +auto InnerWidget::pinnedSavedGifts() +-> Fn()> { + struct Entry { + Data::SavedStarGiftId id; + std::shared_ptr unique; + }; + auto entries = std::vector(); + for (const auto &entry : _entries) { + if (entry.gift.pinned) { + Assert(entry.gift.info.unique != nullptr); + entries.push_back({ + entry.gift.manageId, + entry.gift.info.unique, + }); + } else { + break; + } + } + return [entries] { + auto result = std::vector(); + result.reserve(entries.size()); + for (const auto &entry : entries) { + const auto &id = entry.id; + result.push_back({ + .bareMsgId = uint64(id.userMessageId().bare), + .bareEntryOwnerId = id.chat() ? id.chat()->id.value : 0, + .giftChannelSavedId = id.chatSavedId(), + .uniqueGift = entry.unique, + .stargift = true, + }); + } + return result; + }; +} + void InnerWidget::showMenuFor(not_null button, QPoint point) { if (_menu) { return; @@ -495,27 +536,7 @@ void InnerWidget::showMenuFor(not_null button, QPoint point) { auto entry = ::Settings::SavedStarGiftEntry( _peer, _entries[index].gift); - auto pinnedIds = std::vector(); - for (const auto &entry : _entries) { - if (entry.gift.pinned) { - pinnedIds.push_back(entry.gift.manageId); - } else { - break; - } - } - entry.pinnedSavedGifts = [pinnedIds] { - auto result = std::vector(); - result.reserve(pinnedIds.size()); - for (const auto &id : pinnedIds) { - result.push_back({ - .bareMsgId = uint64(id.userMessageId().bare), - .bareEntryOwnerId = id.chat() ? id.chat()->id.value : 0, - .giftChannelSavedId = id.chatSavedId(), - .stargift = true, - }); - } - return result; - }; + entry.pinnedSavedGifts = pinnedSavedGifts(); _menu = base::make_unique_q(this, st::popupMenuWithIcons); ::Settings::FillSavedStarGiftMenu( _controller->uiShow(), @@ -535,7 +556,8 @@ void InnerWidget::showGift(int index) { ::Settings::SavedStarGiftBox, _window, _peer, - _entries[index].gift)); + _entries[index].gift, + pinnedSavedGifts())); } void InnerWidget::refreshAbout() { diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp index 4056c3bfb6..9aee9306f5 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp @@ -43,6 +43,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/bot/starref/info_bot_starref_common.h" #include "info/channel_statistics/boosts/giveaway/boost_badge.h" // InfiniteRadialAnimationWidget. #include "info/channel_statistics/earn/info_channel_earn_widget.h" // Info::ChannelEarn::Make. +#include "info/peer_gifts/info_peer_gifts_common.h" #include "info/settings/info_settings_widget.h" // SectionCustomTopBarData. #include "info/statistics/info_statistics_list_controllers.h" #include "info/info_controller.h" @@ -173,18 +174,6 @@ private: }; -[[nodiscard]] Data::SavedStarGiftId EntryToSavedStarGiftId( - not_null session, - const Data::CreditsHistoryEntry &entry) { - return !entry.stargift - ? Data::SavedStarGiftId() - : (entry.bareEntryOwnerId && entry.giftChannelSavedId) - ? Data::SavedStarGiftId::Chat( - session->data().peer(PeerId(entry.bareEntryOwnerId)), - entry.giftChannelSavedId) - : Data::SavedStarGiftId::User(MsgId(entry.bareMsgId)); -} - void ToggleStarGiftSaved( std::shared_ptr show, Data::SavedStarGiftId savedId, @@ -226,7 +215,8 @@ void ToggleStarGiftPinned( Data::SavedStarGiftId savedId, std::vector already, bool pinned, - Fn done = nullptr) { + std::shared_ptr uniqueData = nullptr, + std::shared_ptr replacingData = nullptr) { already.erase(ranges::remove(already, savedId), end(already)); if (pinned) { already.insert(begin(already), savedId); @@ -256,16 +246,29 @@ void ToggleStarGiftPinned( .action = (pinned ? GiftAction::Pin : GiftAction::Unpin), }); - if (const auto onstack = done) { - onstack(true); - } if (pinned) { - show->showToast(tr::lng_gift_pinned_done(tr::now)); + show->showToast({ + .title = (uniqueData + ? tr::lng_gift_pinned_done_title( + tr::now, + lt_gift, + Data::UniqueGiftName(*uniqueData)) + : QString()), + .text = (replacingData + ? tr::lng_gift_pinned_done_replaced( + tr::now, + lt_gift, + TextWithEntities{ + Data::UniqueGiftName(*replacingData), + }, + Ui::Text::WithEntities) + : tr::lng_gift_pinned_done( + tr::now, + Ui::Text::WithEntities)), + .duration = Ui::Toast::kDefaultDuration * 2, + }); } }).fail([=](const MTP::Error &error) { - if (const auto onstack = done) { - onstack(false); - } show->showToast(error.type()); }).send(); } @@ -920,25 +923,61 @@ void FillUniqueGiftMenu( if (unique && canToggle && e.savedToProfile - && type == SavedStarGiftMenuType::List) { - const auto already = [session, entries = e.pinnedSavedGifts] { - Expects(entries != nullptr); - - auto list = entries(); + && e.pinnedSavedGifts) { + const auto pinned = e.pinnedSavedGifts; + const auto ids = [session]( + const std::vector &pinned) { auto result = std::vector(); - result.reserve(list.size()); - for (const auto &entry : list) { + result.reserve(pinned.size()); + for (const auto &entry : pinned) { result.push_back(EntryToSavedStarGiftId(session, entry)); } return result; }; if (e.giftPinned) { menu->addAction(tr::lng_context_unpin_from_top(tr::now), [=] { - ToggleStarGiftPinned(show, savedId, already(), false); + ToggleStarGiftPinned(show, savedId, ids(pinned()), false); }, st.unpin ? st.unpin : &st::menuIconUnpin); } else { menu->addAction(tr::lng_context_pin_to_top(tr::now), [=] { - ToggleStarGiftPinned(show, savedId, already(), true); + const auto list = pinned(); + const auto &appConfig = show->session().appConfig(); + const auto limit = appConfig.pinnedGiftsLimit(); + auto already = ids(list); + if (list.size() >= limit) { + Info::PeerGifts::SelectGiftToUnpin(show, list, [=]( + Data::SavedStarGiftId id) { + auto copy = already; + const auto i = ranges::find(copy, id); + const auto replaced = (i != end(copy)) + ? list[i - begin(copy)].uniqueGift + : nullptr; + if (i != end(copy)) { + copy.erase(i); + } + + using GiftAction = Data::GiftUpdate::Action; + show->session().data().notifyGiftUpdate({ + .id = id, + .action = GiftAction::Unpin, + }); + + ToggleStarGiftPinned( + show, + savedId, + already, + true, + unique, + replaced); + }); + } else { + ToggleStarGiftPinned( + show, + savedId, + already, + true, + unique); + } }, st.pin ? st.pin : &st::menuIconPin); } } @@ -2031,15 +2070,30 @@ Data::CreditsHistoryEntry SavedStarGiftEntry( }; } +Data::SavedStarGiftId EntryToSavedStarGiftId( + not_null session, + const Data::CreditsHistoryEntry &entry) { + return !entry.stargift + ? Data::SavedStarGiftId() + : (entry.bareEntryOwnerId && entry.giftChannelSavedId) + ? Data::SavedStarGiftId::Chat( + session->data().peer(PeerId(entry.bareEntryOwnerId)), + entry.giftChannelSavedId) + : Data::SavedStarGiftId::User(MsgId(entry.bareMsgId)); +} + void SavedStarGiftBox( not_null box, not_null controller, not_null owner, - const Data::SavedStarGift &data) { + const Data::SavedStarGift &data, + Fn()> pinned) { + auto entry = SavedStarGiftEntry(owner, data); + entry.pinnedSavedGifts = std::move(pinned); Settings::ReceiptCreditsBox( box, controller, - SavedStarGiftEntry(owner, data), + std::move(entry), Data::SubscriptionEntry()); } diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.h b/Telegram/SourceFiles/settings/settings_credits_graphics.h index 8a1ee55bcd..aa43eff741 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.h +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.h @@ -24,6 +24,7 @@ struct SubscriptionEntry; struct GiftCode; struct CreditTopupOption; struct SavedStarGift; +class SavedStarGiftId; struct StarGift; } // namespace Data @@ -158,11 +159,15 @@ void GlobalStarGiftBox( [[nodiscard]] Data::CreditsHistoryEntry SavedStarGiftEntry( not_null owner, const Data::SavedStarGift &data); +[[nodiscard]] Data::SavedStarGiftId EntryToSavedStarGiftId( + not_null session, + const Data::CreditsHistoryEntry &entry); void SavedStarGiftBox( not_null box, not_null controller, not_null owner, - const Data::SavedStarGift &data); + const Data::SavedStarGift &data, + Fn()> pinned = nullptr); enum class SavedStarGiftMenuType { List, View, diff --git a/Telegram/SourceFiles/ui/effects/credits.style b/Telegram/SourceFiles/ui/effects/credits.style index 906f29c7da..ae44f06329 100644 --- a/Telegram/SourceFiles/ui/effects/credits.style +++ b/Telegram/SourceFiles/ui/effects/credits.style @@ -149,6 +149,7 @@ giftBoxStickerStarTop: 24px; giftBoxSmallStickerTop: 16px; giftBoxStickerTopByStars: -4px; giftBoxStickerSize: size(80px, 80px); +giftBoxSelectSkip: 5px; giftBoxUserpicSize: 24px; giftBoxUserpicSkip: 2px; giftBoxTextField: InputField(defaultInputField) { @@ -256,3 +257,10 @@ darkUpgradeGiftInfoTitle: FlatLabel(defaultFlatLabel) { darkUpgradeGiftInfoAbout: FlatLabel(upgradeGiftSubtext) { textFg: groupCallMemberNotJoinedStatus; } + +giftTooManyPinnedBox: Box(giftBox) { + buttonPadding: margins(11px, 11px, 11px, 11px); +} +giftTooManyPinnedChoose: FlatLabel(giftBoxAbout) { + textFg: windowSubTextFg; +}