Support hosted unique gifts.

This commit is contained in:
John Preston
2025-09-29 19:00:29 +04:00
parent d832a0a186
commit ec4deb87c6
13 changed files with 192 additions and 53 deletions
+4
View File
@@ -2231,6 +2231,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_action_gift_transferred_unknown" = "Someone transferred you a gift";
"lng_action_gift_transferred_unknown_channel" = "Someone transferred a gift to {channel}";
"lng_action_gift_transferred_self" = "You transferred a unique collectible";
"lng_action_gift_displayed_self" = "You've started displaying {name} on your Telegram profile page.";
"lng_action_gift_transferred_self_channel" = "You transferred a gift to {channel}";
"lng_action_gift_transferred_mine" = "You transferred a gift to {user}";
"lng_action_gift_received_anonymous" = "Unknown user sent you a gift for {cost}";
@@ -3684,6 +3685,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_released_by" = "released by {name}";
"lng_gift_unique_owner" = "Owner";
"lng_gift_unique_address_copied" = "Address copied to clipboard.";
"lng_gift_unique_telegram" = "Telegram";
"lng_gift_unique_status" = "Status";
"lng_gift_unique_status_non" = "Non-Unique";
"lng_gift_unique_upgrade" = "Upgrade";
@@ -3822,6 +3824,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_gift_transfer_sell" = "Sell";
"lng_gift_transfer_update" = "Change Price";
"lng_gift_transfer_unlist" = "Unlist";
"lng_gift_transfer_locked_title" = "Action Locked";
"lng_gift_transfer_locked_text" = "Transfer this gift to your Telegram account on Fragment to unlock this action.";
"lng_gift_sell_unlist_title" = "Unlist {name}";
"lng_gift_sell_unlist_sure" = "Are you sure you want to unlist your gift?";
"lng_gift_sell_title" = "Price in Stars";
+4
View File
@@ -915,11 +915,15 @@ std::optional<Data::StarGift> FromTL(
.initialGiftId = data.vgift_id().v,
.slug = qs(data.vslug()),
.title = qs(data.vtitle()),
.giftAddress = qs(data.vgift_address().value_or_empty()),
.ownerAddress = qs(data.vowner_address().value_or_empty()),
.ownerName = qs(data.vowner_name().value_or_empty()),
.ownerId = (data.vowner_id()
? peerFromMTP(*data.vowner_id())
: PeerId()),
.hostId = (data.vhost_id()
? peerFromMTP(*data.vhost_id())
: PeerId()),
.releasedBy = releasedBy,
.themeUser = themeUser,
.nanoTonForResale = FindTonForResale(data.vresell_amount()),
@@ -1618,6 +1618,38 @@ void AddStarGiftTable(
tr::lng_gift_unique_owner(),
std::move(label));
}
if (const auto hostId = PeerId(entry.bareGiftHostId)) {
const auto was = std::make_shared<std::optional<CollectibleId>>();
const auto handleChange = [=](
not_null<Ui::RpWidget*> badge,
EmojiStatusId emojiStatusId) {
const auto id = emojiStatusId.collectible
? emojiStatusId.collectible->id
: 0;
const auto show = [&](const auto &phrase) {
showTooltip(badge, phrase(
lt_name,
rpl::single(Ui::Text::Bold(UniqueGiftName(*unique))),
Ui::Text::WithEntities));
};
if (!*was || *was == id) {
*was = id;
return;
} else if (*was == unique->id) {
show(tr::lng_gift_wear_end_toast);
} else if (id == unique->id) {
show(tr::lng_gift_wear_start_toast);
}
*was = id;
};
AddTableRow(
table,
tr::lng_gift_unique_telegram(),
MakePeerWithStatusValue(table, show, hostId, handleChange),
st::giveawayGiftCodePeerMargin);
}
} else if (giftToChannel) {
AddTableRow(
table,
+10 -3
View File
@@ -2807,6 +2807,7 @@ void AddBlock(
&& !state->myLoading) {
state->myLoading = Data::MyUniqueGiftsSlice(
&peer->session(),
Data::MyUniqueType::OnlyOwned,
state->my.offset
) | rpl::start_with_next([=](MyGiftsDescriptor &&descriptor) {
state->myLoading.destroy();
@@ -3631,7 +3632,8 @@ void ShowStarGiftBox(
}, i->second.lifetime);
Data::MyUniqueGiftsSlice(
session
session,
Data::MyUniqueType::OnlyOwned
) | rpl::start_with_next([=](MyGiftsDescriptor &&gifts) {
auto &entry = Map[session];
entry.my = std::move(gifts);
@@ -3714,6 +3716,7 @@ void AddUniqueGiftCover(
Released() : white(QColor(255, 255, 255)) {
}
rpl::variable<TextWithEntities> subtitleText;
style::owned_color white;
style::FlatLabel st;
PeerData *by = nullptr;
@@ -3743,7 +3746,7 @@ void AddUniqueGiftCover(
) | rpl::map([](const Data::UniqueGift &now) { return now.title; }),
st::uniqueGiftTitle);
title->setTextColorOverride(QColor(255, 255, 255));
auto subtitleText = subtitleOverride
released->subtitleText = subtitleOverride
? std::move(
subtitleOverride
) | Ui::Text::ToWithEntities() | rpl::type_erased()
@@ -3764,9 +3767,13 @@ void AddUniqueGiftCover(
TextWithEntities{ QString::number(gift.number) },
Ui::Text::WithEntities);
});
if (!released->by) {
released->st = st::uniqueGiftSubtitle;
released->st.palette.linkFg = released->white.color();
}
const auto subtitle = CreateChild<FlatLabel>(
cover,
std::move(subtitleText),
released->subtitleText.value(),
released->st);
if (released->by) {
const auto button = CreateChild<AbstractButton>(cover);
@@ -949,3 +949,19 @@ bool ShowTransferGiftLater(
});
return true;
}
void ShowActionLocked(
std::shared_ptr<ChatHelpers::Show> show,
const QString &slug) {
const auto open = [=] {
UrlClickHandler::Open(u"https://fragment.com/gift/"_q
+ slug
+ u"?collection=my"_q);
};
show->show(Ui::MakeConfirmBox({
.text = tr::lng_gift_transfer_locked_text(),
.confirmed = [=](Fn<void()> close) { open(); close(); },
.confirmText = tr::lng_gift_transfer_confirm_button(),
.title = tr::lng_gift_transfer_locked_title(),
}));
}
@@ -64,3 +64,7 @@ void SetPeerTheme(
not_null<PeerData*> peer,
const QString &token,
const std::shared_ptr<Ui::ChatTheme> &theme);
void ShowActionLocked(
std::shared_ptr<ChatHelpers::Show> show,
const QString &slug);
+1
View File
@@ -66,6 +66,7 @@ struct CreditsHistoryEntry final {
uint64 bareGiveawayMsgId = 0;
uint64 bareGiftStickerId = 0;
uint64 bareGiftOwnerId = 0;
uint64 bareGiftHostId = 0;
uint64 bareGiftReleasedById = 0;
uint64 bareGiftResaleRecipientId = 0;
uint64 bareActorId = 0;
+5 -1
View File
@@ -120,6 +120,7 @@ GiftAttributeId IdFor(const UniqueGiftPattern &value) {
rpl::producer<MyGiftsDescriptor> MyUniqueGiftsSlice(
not_null<Main::Session*> session,
MyUniqueType type,
QString offset) {
return [=](auto consumer) {
using Flag = MTPpayments_GetSavedStarGifts::Flag;
@@ -128,7 +129,10 @@ rpl::producer<MyGiftsDescriptor> MyUniqueGiftsSlice(
MTPpayments_GetSavedStarGifts(
MTP_flags(Flag::f_exclude_upgradable
| Flag::f_exclude_unupgradable
| Flag::f_exclude_unlimited),
| Flag::f_exclude_unlimited
| ((type == MyUniqueType::OnlyOwned)
? Flag::f_exclude_hosted
: Flag())),
user->input,
MTP_int(0), // collection_id
MTP_string(offset),
@@ -66,9 +66,11 @@ struct UniqueGift {
uint64 initialGiftId = 0;
QString slug;
QString title;
QString giftAddress;
QString ownerAddress;
QString ownerName;
PeerId ownerId = 0;
PeerId hostId = 0;
PeerData *releasedBy = nullptr;
PeerData *themeUser = nullptr;
int64 nanoTonForResale = -1;
@@ -253,8 +255,14 @@ struct MyGiftsDescriptor {
QString offset;
};
enum class MyUniqueType {
OwnedAndHosted,
OnlyOwned,
};
[[nodiscard]] rpl::producer<MyGiftsDescriptor> MyUniqueGiftsSlice(
not_null<Main::Session*> session,
MyUniqueType type,
QString offset = QString());
struct ResaleGiftsDescriptor {
+2 -1
View File
@@ -1461,7 +1461,8 @@ void History::newItemAdded(not_null<HistoryItem*> item) {
if (const auto media = item->media()) {
if (const auto gift = media->gift()) {
if (const auto unique = gift->unique.get()) {
if (unique->ownerId == session().userPeerId()) {
if (unique->ownerId == session().userPeerId()
|| unique->hostId == session().userPeerId()) {
owner().emojiStatuses().refreshCollectibles();
}
}
@@ -6231,6 +6231,18 @@ void HistoryItem::setServiceMessageByAction(const MTPmessageAction &action) {
lt_user,
Ui::Text::Link(from->shortName(), 1),
Ui::Text::WithEntities);
} else if (action.is_assigned()) {
const auto gift = Api::FromTL(
&history()->session(),
action.vgift());
result.text = tr::lng_action_gift_displayed_self(
tr::now,
lt_name,
TextWithEntities{ (gift && gift->unique)
? Data::UniqueGiftName(*gift->unique)
: QString(),
},
Ui::Text::WithEntities);
} else {
result.text = resale
? tr::lng_action_gift_self_bought(
@@ -968,11 +968,29 @@ void ProcessReceivedSubscriptions(
}
}
[[nodiscard]] bool ShowResellButton(
not_null<Main::Session*> session,
const Data::CreditsHistoryEntry &e) {
const auto unique = e.uniqueGift.get();
const auto host = (unique && unique->hostId)
? session->data().peer(unique->hostId).get()
: (unique && unique->ownerId)
? session->data().peer(unique->ownerId).get()
: nullptr;
return !host
? false
: host->isSelf()
? e.in
: false;
// Currently we're not reselling channel gifts.
// (host->isChannel() && host->asChannel()->canTransferGifts());
}
[[nodiscard]] bool CanResellGift(
not_null<Main::Session*> session,
const Data::CreditsHistoryEntry &e) {
const auto unique = e.uniqueGift.get();
const auto owner = unique
const auto owner = (unique && unique->ownerId)
? session->data().peer(unique->ownerId).get()
: nullptr;
return !owner
@@ -1101,45 +1119,60 @@ void FillUniqueGiftMenu(
}
}, st.theme ? st.theme : &st::menuIconChangeColors);
}
const auto owner = unique->ownerId
? show->session().data().peer(unique->ownerId).get()
: (PeerData*)nullptr;
const auto host = unique->hostId
? show->session().data().peer(unique->hostId).get()
: owner;
if (!host) {
return;
}
const auto transfer = savedId
&& (savedId.isUser() ? e.in : savedId.chat()->canTransferGifts())
&& (unique->starsForTransfer >= 0);
if (transfer) {
menu->addAction(tr::lng_gift_transfer_button(tr::now), [=] {
if (const auto window = show->resolveWindow()) {
if (!owner) {
ShowActionLocked(show, unique->slug);
} else if (const auto window = show->resolveWindow()) {
ShowTransferGiftBox(window, unique, savedId);
}
}, st.transfer ? st.transfer : &st::menuIconReplace);
}
const auto owner = show->session().data().peer(unique->ownerId);
const auto wear = owner->isSelf()
const auto wear = host->isSelf()
? e.in
: (owner->isChannel() && owner->asChannel()->canEditEmoji());
: (host->isChannel() && host->asChannel()->canEditEmoji());
if (wear) {
const auto name = UniqueGiftName(*unique);
const auto now = owner->emojiStatusId().collectible;
const auto now = host->emojiStatusId().collectible;
if (now && unique->slug == now->slug) {
menu->addAction(tr::lng_gift_transfer_take_off(tr::now), [=] {
show->session().data().emojiStatuses().set(owner, {});
show->session().data().emojiStatuses().set(host, {});
}, st.takeoff ? st.takeoff : &st::menuIconNftTakeOff);
} else {
menu->addAction(tr::lng_gift_transfer_wear(tr::now), [=] {
ShowUniqueGiftWearBox(show, owner, *unique, st.giftWearBox
ShowUniqueGiftWearBox(show, host, *unique, st.giftWearBox
? *st.giftWearBox
: GiftWearBoxStyleOverride());
}, st.wear ? st.wear : &st::menuIconNftWear);
}
}
if (CanResellGift(&show->session(), e)) {
if (ShowResellButton(&show->session(), e)) {
const auto can = CanResellGift(&show->session(), e);
const auto inResale = (unique->starsForResale > 0);
const auto editPrice = (inResale
? tr::lng_gift_transfer_update
: tr::lng_gift_transfer_sell)(tr::now);
menu->addAction(editPrice, [=] {
const auto style = st.giftWearBox
? *st.giftWearBox
: GiftWearBoxStyleOverride();
ShowUniqueGiftSellBox(show, unique, savedId, style);
if (!can) {
ShowActionLocked(show, unique->slug);
} else {
const auto style = st.giftWearBox
? *st.giftWearBox
: GiftWearBoxStyleOverride();
ShowUniqueGiftSellBox(show, unique, savedId, style);
}
}, st.resell ? st.resell : &st::menuIconTagSell);
if (inResale) {
menu->addAction(tr::lng_gift_transfer_unlist(tr::now), [=] {
@@ -1807,6 +1840,40 @@ void GenericCreditsEntryBox(
}
Ui::AddSkip(content);
const auto addGiftLinkTON = [&] {
if (!uniqueGift) {
return;
}
const auto address = !uniqueGift->giftAddress.isEmpty()
? uniqueGift->giftAddress
: uniqueGift->ownerAddress;
if (address.isEmpty()) {
return;
}
const auto label = box->addRow(
object_ptr<Ui::FlatLabel>(
box,
tr::lng_gift_in_blockchain(
lt_link,
tr::lng_gift_in_blockchain_link_arrow(
lt_arrow,
rpl::single(arrow),
Ui::Text::WithEntities
) | Ui::Text::ToLink(),
Ui::Text::WithEntities),
st::creditsBoxAboutDivider),
style::al_top);
label->setClickHandlerFilter([=](const auto &...) {
UrlClickHandler::Open(TonAddressUrl(session, address));
return false;
});
};
if (starGiftCanManage) {
addGiftLinkTON();
}
Ui::AddSkip(content);
struct State final {
@@ -2040,25 +2107,8 @@ void GenericCreditsEntryBox(
toggleVisibility(!e.savedToProfile);
return false;
});
} else if (uniqueGift && !uniqueGift->ownerAddress.isEmpty()) {
const auto label = box->addRow(
object_ptr<Ui::FlatLabel>(
box,
tr::lng_gift_in_blockchain(
lt_link,
tr::lng_gift_in_blockchain_link_arrow(
lt_arrow,
rpl::single(arrow),
Ui::Text::WithEntities
) | Ui::Text::ToLink(),
Ui::Text::WithEntities),
st::creditsBoxAboutDivider),
style::al_top);
label->setClickHandlerFilter([=](const auto &...) {
UrlClickHandler::Open(
TonAddressUrl(session, uniqueGift->ownerAddress));
return false;
});
} else {
addGiftLinkTON();
}
if (s) {
const auto user = peer ? peer->asUser() : nullptr;
@@ -2531,6 +2581,7 @@ void GlobalStarGiftBox(
CreditsEntryBoxStyleOverrides st) {
const auto selfId = show->session().userPeerId();
const auto ownerId = data.unique ? data.unique->ownerId.value : 0;
const auto hostId = data.unique ? data.unique->hostId.value : 0;
Settings::GenericCreditsEntryBox(
box,
show,
@@ -2538,6 +2589,7 @@ void GlobalStarGiftBox(
.credits = CreditsAmount(data.stars),
.bareGiftStickerId = data.document->id,
.bareGiftOwnerId = ownerId,
.bareGiftHostId = hostId,
.bareGiftResaleRecipientId = ((resale.recipientId != selfId)
? resale.recipientId.value
: 0),
@@ -2560,6 +2612,12 @@ Data::CreditsHistoryEntry SavedStarGiftEntry(
not_null<PeerData*> owner,
const Data::SavedStarGift &data) {
const auto chatGiftPeer = data.manageId.chat();
const auto ownerId = data.info.unique
? data.info.unique->ownerId
: owner->id;
const auto hostId = data.info.unique
? data.info.unique->hostId
: PeerId();
return {
.description = data.message,
.date = base::unixtime::parse(data.date),
@@ -2567,7 +2625,8 @@ Data::CreditsHistoryEntry SavedStarGiftEntry(
.bareMsgId = uint64(data.manageId.userMessageId().bare),
.barePeerId = data.fromId.value,
.bareGiftStickerId = data.info.document->id,
.bareGiftOwnerId = owner->id.value,
.bareGiftOwnerId = ownerId.value,
.bareGiftHostId = hostId.value,
.bareActorId = data.fromId.value,
.bareEntryOwnerId = chatGiftPeer ? chatGiftPeer->id.value : 0,
.giftChannelSavedId = data.manageId.chatSavedId(),
@@ -2647,6 +2706,7 @@ void ShowStarGiftViewBox(
const auto fromId = incoming ? peer->id : peer->session().userPeerId();
const auto toId = incoming ? peer->session().userPeerId() : peer->id;
const auto ownerId = data.unique ? data.unique->ownerId : toId;
const auto hostId = data.unique ? data.unique->hostId : PeerId();
const auto nextToUpgradeStickerId = upgradeNext
? upgradeNext->info.document->id
: uint64();
@@ -2667,6 +2727,7 @@ void ShowStarGiftViewBox(
.barePeerId = fromId.value,
.bareGiftStickerId = data.document ? data.document->id : 0,
.bareGiftOwnerId = ownerId.value,
.bareGiftHostId = hostId.value,
.bareGiftReleasedById = (data.stargiftReleasedBy
? data.stargiftReleasedBy->id.value
: 0),
@@ -592,26 +592,11 @@ void SubTabs::shakeTransform(
/ float64(kShakeXDuration);
const auto pY = (diff % kShakeYDuration)
/ float64(kShakeYDuration);
const auto pA = (diff % kShakeADuration)
/ float64(kShakeADuration);
constexpr auto kMaxA = 2.;
constexpr auto kMaxTranslation = .5;
constexpr auto kAStep = 1. / 5;
constexpr auto kXStep = 1. / 5;
constexpr auto kYStep = 1. / 4;
// 0, -kMaxA, 0, kMaxA, 0.
const auto angle = (pA < kAStep)
? anim::interpolateF(0., -kMaxA, pA / kAStep)
: (pA < kAStep * 2.)
? anim::interpolateF(-kMaxA, 0, (pA - kAStep) / kAStep)
: (pA < kAStep * 3.)
? anim::interpolateF(0, kMaxA, (pA - kAStep * 2.) / kAStep)
: (pA < kAStep * 4.)
? anim::interpolateF(kMaxA, 0, (pA - kAStep * 3.) / kAStep)
: anim::interpolateF(0, 0., (pA - kAStep * 4.) / kAStep);
// 0, kMaxTranslation, 0, -kMaxTranslation, 0.
const auto x = (pX < kXStep)
? anim::interpolateF(0., kMaxTranslation, pX / kXStep)