Support gifts pinning.

This commit is contained in:
John Preston 2025-03-05 14:57:17 +04:00
parent 7840fa6d90
commit 0f74456f30
14 changed files with 219 additions and 9 deletions

View file

@ -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.";

View file

@ -904,6 +904,7 @@ std::optional<Data::SavedStarGift> 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(),
};

View file

@ -70,6 +70,7 @@ struct CreditsHistoryEntry final {
uint64 giftChannelSavedId = 0;
uint64 stargiftId = 0;
std::shared_ptr<UniqueGift> uniqueGift;
Fn<std::vector<CreditsHistoryEntry>()> 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;

View file

@ -87,6 +87,8 @@ struct GiftUpdate {
Convert,
Transfer,
Delete,
Pin,
Unpin,
};
Data::SavedStarGiftId id;

View file

@ -132,6 +132,7 @@ struct SavedStarGift {
TimeId date = 0;
bool upgradable = false;
bool anonymous = false;
bool pinned = false;
bool hidden = false;
bool mine = false;
};

View file

@ -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;

View file

@ -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))

View file

@ -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;

View file

@ -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<GiftButton*> button, QPoint point);
void refreshAbout();
void markPinned(std::vector<Entry>::iterator i);
void markUnpinned(std::vector<Entry>::iterator i);
int resizeGetHeight(int width) override;
const not_null<Window::SessionController*> _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<Entry>::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<Entry>::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<GiftButton*> button, QPoint point) {
return;
}
const auto entry = ::Settings::SavedStarGiftEntry(
auto entry = ::Settings::SavedStarGiftEntry(
_peer,
_entries[index].gift);
auto pinnedIds = std::vector<Data::SavedStarGiftId>();
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<Data::CreditsHistoryEntry>();
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<Ui::PopupMenu>(this, st::popupMenuWithIcons);
::Settings::FillSavedStarGiftMenu(
_controller->uiShow(),

View file

@ -89,6 +89,10 @@ int AppConfig::paidMessageCommission() const {
return get<int>(u"stars_paid_message_commission_permille"_q, 850);
}
int AppConfig::pinnedGiftsLimit() const {
return get<int>(u"stargifts_pinned_to_top_limit"_q, 6);
}
void AppConfig::refresh(bool force) {
if (_requestId || !_api) {
if (force) {

View file

@ -77,6 +77,8 @@ public:
[[nodiscard]] int paidMessageStarsMax() const;
[[nodiscard]] int paidMessageCommission() const;
[[nodiscard]] int pinnedGiftsLimit() const;
void refresh(bool force = false);
private:

View file

@ -221,6 +221,55 @@ void ToggleStarGiftSaved(
}).send();
}
void ToggleStarGiftPinned(
std::shared_ptr<ChatHelpers::Show> show,
Data::SavedStarGiftId savedId,
std::vector<Data::SavedStarGiftId> already,
bool pinned,
Fn<void(bool)> 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<MTPInputSavedStarGift>();
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<MTPInputSavedStarGift>(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<Ui::Show> show,
rpl::producer<TextWithEntities> 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<Data::SavedStarGiftId>();
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<ShareBoxStyleOverrides>(
DarkShareBoxStyle()),
.giftWearBox = std::make_shared<GiftWearBoxStyleOverride>(
@ -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,

View file

@ -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<ShareBoxStyleOverrides> shareBox;
std::shared_ptr<GiftWearBoxStyleOverride> giftWearBox;
};

View file

@ -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(