diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 822f2ba065..1f6cd9ff8d 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -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"; diff --git a/Telegram/SourceFiles/api/api_premium.cpp b/Telegram/SourceFiles/api/api_premium.cpp index f2447c9d58..562bdfa866 100644 --- a/Telegram/SourceFiles/api/api_premium.cpp +++ b/Telegram/SourceFiles/api/api_premium.cpp @@ -915,11 +915,15 @@ std::optional 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()), diff --git a/Telegram/SourceFiles/boxes/gift_premium_box.cpp b/Telegram/SourceFiles/boxes/gift_premium_box.cpp index 415855b5ea..0a1a8333e3 100644 --- a/Telegram/SourceFiles/boxes/gift_premium_box.cpp +++ b/Telegram/SourceFiles/boxes/gift_premium_box.cpp @@ -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>(); + const auto handleChange = [=]( + not_null 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, diff --git a/Telegram/SourceFiles/boxes/star_gift_box.cpp b/Telegram/SourceFiles/boxes/star_gift_box.cpp index fc64f6371e..b9e8d0d098 100644 --- a/Telegram/SourceFiles/boxes/star_gift_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_box.cpp @@ -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 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( cover, - std::move(subtitleText), + released->subtitleText.value(), released->st); if (released->by) { const auto button = CreateChild(cover); diff --git a/Telegram/SourceFiles/boxes/transfer_gift_box.cpp b/Telegram/SourceFiles/boxes/transfer_gift_box.cpp index 0252113896..c9916bb7a4 100644 --- a/Telegram/SourceFiles/boxes/transfer_gift_box.cpp +++ b/Telegram/SourceFiles/boxes/transfer_gift_box.cpp @@ -949,3 +949,19 @@ bool ShowTransferGiftLater( }); return true; } + +void ShowActionLocked( + std::shared_ptr 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 close) { open(); close(); }, + .confirmText = tr::lng_gift_transfer_confirm_button(), + .title = tr::lng_gift_transfer_locked_title(), + })); +} diff --git a/Telegram/SourceFiles/boxes/transfer_gift_box.h b/Telegram/SourceFiles/boxes/transfer_gift_box.h index a1253e0247..ec074e4d70 100644 --- a/Telegram/SourceFiles/boxes/transfer_gift_box.h +++ b/Telegram/SourceFiles/boxes/transfer_gift_box.h @@ -64,3 +64,7 @@ void SetPeerTheme( not_null peer, const QString &token, const std::shared_ptr &theme); + +void ShowActionLocked( + std::shared_ptr show, + const QString &slug); diff --git a/Telegram/SourceFiles/data/data_credits.h b/Telegram/SourceFiles/data/data_credits.h index eb653a4df6..3b1e810eb2 100644 --- a/Telegram/SourceFiles/data/data_credits.h +++ b/Telegram/SourceFiles/data/data_credits.h @@ -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; diff --git a/Telegram/SourceFiles/data/data_star_gift.cpp b/Telegram/SourceFiles/data/data_star_gift.cpp index efe2b3ad8c..5a5f0c1440 100644 --- a/Telegram/SourceFiles/data/data_star_gift.cpp +++ b/Telegram/SourceFiles/data/data_star_gift.cpp @@ -120,6 +120,7 @@ GiftAttributeId IdFor(const UniqueGiftPattern &value) { rpl::producer MyUniqueGiftsSlice( not_null session, + MyUniqueType type, QString offset) { return [=](auto consumer) { using Flag = MTPpayments_GetSavedStarGifts::Flag; @@ -128,7 +129,10 @@ rpl::producer 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), diff --git a/Telegram/SourceFiles/data/data_star_gift.h b/Telegram/SourceFiles/data/data_star_gift.h index ce7f4c9ace..0f57cdfcef 100644 --- a/Telegram/SourceFiles/data/data_star_gift.h +++ b/Telegram/SourceFiles/data/data_star_gift.h @@ -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 MyUniqueGiftsSlice( not_null session, + MyUniqueType type, QString offset = QString()); struct ResaleGiftsDescriptor { diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 23c46e2c6c..62ffcdd035 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -1461,7 +1461,8 @@ void History::newItemAdded(not_null 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(); } } diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 77dbed3eac..f93741c974 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -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( diff --git a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp index 770078a94d..75f4881600 100644 --- a/Telegram/SourceFiles/settings/settings_credits_graphics.cpp +++ b/Telegram/SourceFiles/settings/settings_credits_graphics.cpp @@ -968,11 +968,29 @@ void ProcessReceivedSubscriptions( } } +[[nodiscard]] bool ShowResellButton( + not_null 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 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( + 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( - 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 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), diff --git a/Telegram/SourceFiles/ui/controls/sub_tabs.cpp b/Telegram/SourceFiles/ui/controls/sub_tabs.cpp index f9f4c4abc4..5a76c7e503 100644 --- a/Telegram/SourceFiles/ui/controls/sub_tabs.cpp +++ b/Telegram/SourceFiles/ui/controls/sub_tabs.cpp @@ -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)