From 0f74456f3030ef369aa380c981ce4577fcece189 Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 5 Mar 2025 14:57:17 +0400 Subject: [PATCH] Support gifts pinning. --- Telegram/Resources/langs/lang.strings | 1 + Telegram/SourceFiles/api/api_premium.cpp | 1 + Telegram/SourceFiles/data/data_credits.h | 2 + Telegram/SourceFiles/data/data_session.h | 2 + Telegram/SourceFiles/data/data_star_gift.h | 1 + .../boosts/giveaway/giveaway.style | 2 + .../peer_gifts/info_peer_gifts_common.cpp | 18 ++++ .../info/peer_gifts/info_peer_gifts_common.h | 2 + .../peer_gifts/info_peer_gifts_widget.cpp | 95 ++++++++++++++++++- Telegram/SourceFiles/main/main_app_config.cpp | 4 + Telegram/SourceFiles/main/main_app_config.h | 2 + .../settings/settings_credits_graphics.cpp | 95 +++++++++++++++++-- .../settings/settings_credits_graphics.h | 2 + Telegram/SourceFiles/ui/effects/credits.style | 1 + 14 files changed, 219 insertions(+), 9 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index e30c963fb..7a4a31c83 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -3428,6 +3428,7 @@ 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" = "The gift will always be shown on top."; "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."; diff --git a/Telegram/SourceFiles/api/api_premium.cpp b/Telegram/SourceFiles/api/api_premium.cpp index 313ec1bd1..d7dbcd37f 100644 --- a/Telegram/SourceFiles/api/api_premium.cpp +++ b/Telegram/SourceFiles/api/api_premium.cpp @@ -904,6 +904,7 @@ std::optional FromTL( .date = data.vdate().v, .upgradable = data.is_can_upgrade(), .anonymous = data.is_name_hidden(), + .pinned = data.is_pinned_to_top(), .hidden = data.is_unsaved(), .mine = to->isSelf(), }; diff --git a/Telegram/SourceFiles/data/data_credits.h b/Telegram/SourceFiles/data/data_credits.h index f569cdd97..af37f4e58 100644 --- a/Telegram/SourceFiles/data/data_credits.h +++ b/Telegram/SourceFiles/data/data_credits.h @@ -70,6 +70,7 @@ struct CreditsHistoryEntry final { uint64 giftChannelSavedId = 0; uint64 stargiftId = 0; std::shared_ptr uniqueGift; + Fn()> pinnedSavedGifts; StarsAmount starrefAmount; int starrefCommission = 0; uint64 starrefRecipientId = 0; @@ -93,6 +94,7 @@ struct CreditsHistoryEntry final { bool giftTransferred : 1 = false; bool giftRefunded : 1 = false; bool giftUpgraded : 1 = false; + bool giftPinned : 1 = false; bool savedToProfile : 1 = false; bool fromGiftsList : 1 = false; bool fromGiftSlug : 1 = false; diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index 3e5392173..14f272c6c 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -87,6 +87,8 @@ struct GiftUpdate { Convert, Transfer, Delete, + Pin, + Unpin, }; Data::SavedStarGiftId id; diff --git a/Telegram/SourceFiles/data/data_star_gift.h b/Telegram/SourceFiles/data/data_star_gift.h index e88cae6d1..1b9a8ca06 100644 --- a/Telegram/SourceFiles/data/data_star_gift.h +++ b/Telegram/SourceFiles/data/data_star_gift.h @@ -132,6 +132,7 @@ struct SavedStarGift { TimeId date = 0; bool upgradable = false; bool anonymous = false; + bool pinned = false; bool hidden = false; bool mine = false; }; diff --git a/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway.style b/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway.style index afcd0a34e..1558fb259 100644 --- a/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway.style +++ b/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway.style @@ -223,6 +223,8 @@ darkGiftNftWear: icon {{ "menu/nft_wear", groupCallMembersFg }}; darkGiftNftTakeOff: icon {{ "menu/nft_takeoff", groupCallMembersFg }}; darkGiftHide: icon {{ "menu/stealth", groupCallMembersFg }}; darkGiftShow: icon {{ "menu/show_in_chat", groupCallMembersFg }}; +darkGiftPin: icon {{ "menu/pin", groupCallMembersFg }}; +darkGiftUnpin: icon {{ "menu/unpin", groupCallMembersFg }}; darkGiftPalette: TextPalette(defaultTextPalette) { linkFg: mediaviewTextLinkFg; monoFg: groupCallMembersFg; 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 c48cb6f45..45d424fc0 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp @@ -463,6 +463,24 @@ void GiftButton::paintEvent(QPaintEvent *e) { position.y() - rubberOut, cached); } + + v::match(_descriptor, [](const GiftTypePremium &) { + }, [&](const GiftTypeStars &data) { + if (unique && data.pinned) { + auto hq = PainterHighQualityEnabler(p); + const auto &icon = st::giftBoxPinIcon; + const auto skip = st::giftBoxUserpicSkip; + const auto add = (st::giftBoxUserpicSize - icon.width()) / 2; + p.setPen(Qt::NoPen); + p.setBrush(unique->backdrop.patternColor); + const auto rect = QRect( + QPoint(_extend.left() + skip, _extend.top() + skip), + QSize(icon.width() + 2 * add, icon.height() + 2 * add)); + p.drawEllipse(rect); + icon.paintInCenter(p, rect); + } + }); + if (!_button.isEmpty()) { p.setBrush(unique ? QBrush(QColor(255, 255, 255, .2 * 255)) 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 02c4223b8..ddd800b3d 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h @@ -56,7 +56,9 @@ struct GiftTypePremium { struct GiftTypeStars { Data::StarGift info; PeerData *from = nullptr; + TimeId date = 0; bool userpic = false; + bool pinned = false; bool hidden = false; bool mine = false; 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 63f48a7a7..376b95c07 100644 --- a/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp +++ b/Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp @@ -26,6 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/wrap/slide_wrap.h" #include "ui/ui_utility.h" #include "lang/lang_keys.h" +#include "main/main_app_config.h" #include "main/main_session.h" #include "mtproto/sender.h" #include "window/window_session_controller.h" @@ -50,7 +51,9 @@ constexpr auto kPerPage = 50; .from = ((gift.anonymous || !gift.fromId) ? nullptr : to->owner().peer(gift.fromId).get()), + .date = gift.date, .userpic = !gift.info.unique, + .pinned = gift.pinned, .hidden = gift.hidden, .mine = to->isSelf(), }; @@ -104,6 +107,9 @@ private: void showMenuFor(not_null button, QPoint point); void refreshAbout(); + void markPinned(std::vector::iterator i); + void markUnpinned(std::vector::iterator i); + int resizeGetHeight(int width) override; const not_null _window; @@ -203,6 +209,13 @@ void InnerWidget::subscribeToUpdates() { view.manageId = {}; } } + } else if (update.action == Action::Pin + || update.action == Action::Unpin) { + if (update.action == Action::Pin) { + markPinned(i); + } else { + markUnpinned(i); + } } else { return; } @@ -210,6 +223,65 @@ void InnerWidget::subscribeToUpdates() { }, lifetime()); } +void InnerWidget::markPinned(std::vector::iterator i) { + const auto index = int(i - begin(_entries)); + + i->gift.pinned = true; + v::match(i->descriptor, [](const GiftTypePremium &) { + }, [&](GiftTypeStars &data) { + data.pinned = true; + }); + if (index) { + std::rotate(begin(_entries), i, i + 1); + } + auto unpin = end(_entries); + const auto session = &_window->session(); + const auto limit = session->appConfig().pinnedGiftsLimit(); + if (limit < _entries.size()) { + const auto j = begin(_entries) + limit; + if (j->gift.pinned) { + unpin = j; + } + } + for (auto &view : _views) { + if (view.index <= index) { + view.index = -1; + view.manageId = {}; + } + } + if (unpin != end(_entries)) { + markUnpinned(unpin); + } +} + +void InnerWidget::markUnpinned(std::vector::iterator i) { + const auto index = int(i - begin(_entries)); + + i->gift.pinned = false; + v::match(i->descriptor, [](const GiftTypePremium &) { + }, [&](GiftTypeStars &data) { + data.pinned = false; + }); + auto after = index + 1; + for (auto j = i + 1; j != end(_entries); ++j) { + if (!j->gift.pinned && j->gift.date <= i->gift.date) { + break; + } + ++after; + } + if (after == _entries.size()) { + _entries.erase(i); + } else if (after > index + 1) { + std::rotate(i, i + 1, begin(_entries) + after); + } + for (auto &view : _views) { + if (view.index >= index) { + view.index = -1; + view.manageId = {}; + } + } +} + void InnerWidget::visibleTopBottomUpdated( int visibleTop, int visibleBottom) { @@ -412,9 +484,30 @@ void InnerWidget::showMenuFor(not_null button, QPoint point) { return; } - const auto entry = ::Settings::SavedStarGiftEntry( + 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, peer = _peer] { + 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; + }; _menu = base::make_unique_q(this, st::popupMenuWithIcons); ::Settings::FillSavedStarGiftMenu( _controller->uiShow(), diff --git a/Telegram/SourceFiles/main/main_app_config.cpp b/Telegram/SourceFiles/main/main_app_config.cpp index d1f10b7e8..4326d7559 100644 --- a/Telegram/SourceFiles/main/main_app_config.cpp +++ b/Telegram/SourceFiles/main/main_app_config.cpp @@ -89,6 +89,10 @@ int AppConfig::paidMessageCommission() const { return get(u"stars_paid_message_commission_permille"_q, 850); } +int AppConfig::pinnedGiftsLimit() const { + return get(u"stargifts_pinned_to_top_limit"_q, 6); +} + void AppConfig::refresh(bool force) { if (_requestId || !_api) { if (force) { diff --git a/Telegram/SourceFiles/main/main_app_config.h b/Telegram/SourceFiles/main/main_app_config.h index 58a7da4de..308656bbe 100644 --- a/Telegram/SourceFiles/main/main_app_config.h +++ b/Telegram/SourceFiles/main/main_app_config.h @@ -77,6 +77,8 @@ public: [[nodiscard]] int paidMessageStarsMax() const; [[nodiscard]] int paidMessageCommission() const; + [[nodiscard]] int pinnedGiftsLimit() const; + void refresh(bool force = false); private: diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp index d680e7ef4..8e9c87bf3 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp @@ -221,6 +221,55 @@ void ToggleStarGiftSaved( }).send(); } +void ToggleStarGiftPinned( + std::shared_ptr show, + Data::SavedStarGiftId savedId, + std::vector already, + bool pinned, + Fn done = nullptr) { + already.erase(ranges::remove(already, savedId), end(already)); + if (pinned) { + already.insert(begin(already), savedId); + const auto limit = show->session().appConfig().pinnedGiftsLimit(); + if (already.size() > limit) { + already.erase(begin(already) + limit, end(already)); + } + } + + auto inputs = QVector(); + inputs.reserve(already.size()); + for (const auto &id : already) { + inputs.push_back(Api::InputSavedStarGiftId(id)); + } + + const auto api = &show->session().api(); + const auto peer = savedId.chat() + ? savedId.chat() + : show->session().user(); + api->request(MTPpayments_ToggleStarGiftsPinnedToTop( + peer->input, + MTP_vector(std::move(inputs)) + )).done([=] { + using GiftAction = Data::GiftUpdate::Action; + show->session().data().notifyGiftUpdate({ + .id = savedId, + .action = (pinned ? GiftAction::Pin : GiftAction::Unpin), + }); + + if (const auto onstack = done) { + onstack(true); + } + if (pinned) { + show->showToast(tr::lng_gift_pinned_done(tr::now)); + } + }).fail([=](const MTP::Error &error) { + if (const auto onstack = done) { + onstack(false); + } + show->showToast(error.type()); + }).send(); +} + void ConfirmConvertStarGift( std::shared_ptr show, rpl::producer confirmText, @@ -861,7 +910,41 @@ void FillUniqueGiftMenu( const Data::CreditsHistoryEntry &e, SavedStarGiftMenuType type, CreditsEntryBoxStyleOverrides st) { + const auto session = &show->session(); + const auto savedId = EntryToSavedStarGiftId(session, e); + const auto giftChannel = savedId.chat(); + const auto canToggle = savedId + && e.id.isEmpty() + && (e.in || (giftChannel && giftChannel->canManageGifts())) + && !e.giftTransferred + && !e.giftRefunded; + const auto unique = e.uniqueGift; + if (unique + && canToggle + && e.savedToProfile + && type == SavedStarGiftMenuType::List) { + const auto already = [session, entries = e.pinnedSavedGifts] { + Expects(entries != nullptr); + + auto list = entries(); + auto result = std::vector(); + result.reserve(list.size()); + for (const auto &entry : list) { + 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); + }, st.unpin ? st.unpin : &st::menuIconUnpin); + } else { + menu->addAction(tr::lng_context_pin_to_top(tr::now), [=] { + ToggleStarGiftPinned(show, savedId, already(), true); + }, st.pin ? st.pin : &st::menuIconPin); + } + } if (unique) { const auto local = u"nft/"_q + unique->slug; const auto url = show->session().createInternalLinkFull(local); @@ -879,14 +962,7 @@ void FillUniqueGiftMenu( }, st.share ? st.share : &st::menuIconShare); } - const auto savedId = EntryToSavedStarGiftId(&show->session(), e); - const auto giftChannel = savedId.chat(); - const auto canToggleVisibility = savedId - && e.id.isEmpty() - && (e.in || (giftChannel && giftChannel->canManageGifts())) - && !e.giftTransferred - && !e.giftRefunded; - if (canToggleVisibility && type == SavedStarGiftMenuType::List) { + if (canToggle && type == SavedStarGiftMenuType::List) { if (e.savedToProfile) { menu->addAction(tr::lng_gift_menu_hide(tr::now), [=] { ToggleStarGiftSaved(show, savedId, false); @@ -958,6 +1034,8 @@ CreditsEntryBoxStyleOverrides DarkCreditsEntryBoxStyle() { .takeoff = &st::darkGiftNftTakeOff, .show = &st::darkGiftShow, .hide = &st::darkGiftHide, + .pin = &st::darkGiftPin, + .unpin = &st::darkGiftUnpin, .shareBox = std::make_shared( DarkShareBoxStyle()), .giftWearBox = std::make_shared( @@ -1951,6 +2029,7 @@ Data::CreditsHistoryEntry SavedStarGiftEntry( .converted = false, .anonymous = data.anonymous, .stargift = true, + .giftPinned = data.pinned, .savedToProfile = !data.hidden, .fromGiftsList = true, .canUpgradeGift = data.upgradable, diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.h b/Telegram/SourceFiles/settings/settings_credits_graphics.h index 0ea66f359..8a1ee55bc 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.h +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.h @@ -115,6 +115,8 @@ struct CreditsEntryBoxStyleOverrides { const style::icon *takeoff = nullptr; const style::icon *show = nullptr; const style::icon *hide = nullptr; + const style::icon *pin = nullptr; + const style::icon *unpin = nullptr; std::shared_ptr shareBox; std::shared_ptr giftWearBox; }; diff --git a/Telegram/SourceFiles/ui/effects/credits.style b/Telegram/SourceFiles/ui/effects/credits.style index 8add6b1dc..e9b20d5bb 100644 --- a/Telegram/SourceFiles/ui/effects/credits.style +++ b/Telegram/SourceFiles/ui/effects/credits.style @@ -178,6 +178,7 @@ giftListAboutMargin: margins(12px, 24px, 12px, 24px); giftBoxEmojiToggleTop: 7px; giftBoxLimitTop: 28px; giftBoxLockMargins: margins(-2px, 1px, 0px, 0px); +giftBoxPinIcon: icon {{ "dialogs/dialogs_pinned", premiumButtonFg }}; creditsHistoryEntriesList: PeerList(defaultPeerList) { padding: margins(