diff --git a/Telegram/Resources/icons/settings/button_auction.png b/Telegram/Resources/icons/settings/button_auction.png new file mode 100644 index 0000000000..6fbaa5e019 Binary files /dev/null and b/Telegram/Resources/icons/settings/button_auction.png differ diff --git a/Telegram/Resources/icons/settings/button_auction@2x.png b/Telegram/Resources/icons/settings/button_auction@2x.png new file mode 100644 index 0000000000..c4c3ef1d8f Binary files /dev/null and b/Telegram/Resources/icons/settings/button_auction@2x.png differ diff --git a/Telegram/Resources/icons/settings/button_auction@3x.png b/Telegram/Resources/icons/settings/button_auction@3x.png new file mode 100644 index 0000000000..c9c6b28561 Binary files /dev/null and b/Telegram/Resources/icons/settings/button_auction@3x.png differ diff --git a/Telegram/Resources/icons/settings/toast_auction.png b/Telegram/Resources/icons/settings/toast_auction.png new file mode 100644 index 0000000000..41b34d5fe1 Binary files /dev/null and b/Telegram/Resources/icons/settings/toast_auction.png differ diff --git a/Telegram/Resources/icons/settings/toast_auction@2x.png b/Telegram/Resources/icons/settings/toast_auction@2x.png new file mode 100644 index 0000000000..e03d6a987f Binary files /dev/null and b/Telegram/Resources/icons/settings/toast_auction@2x.png differ diff --git a/Telegram/Resources/icons/settings/toast_auction@3x.png b/Telegram/Resources/icons/settings/toast_auction@3x.png new file mode 100644 index 0000000000..8bf2db0ab2 Binary files /dev/null and b/Telegram/Resources/icons/settings/toast_auction@3x.png differ diff --git a/Telegram/SourceFiles/boxes/star_gift_auction_box.cpp b/Telegram/SourceFiles/boxes/star_gift_auction_box.cpp index 469e5462f5..24bb92a77a 100644 --- a/Telegram/SourceFiles/boxes/star_gift_auction_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_auction_box.cpp @@ -8,16 +8,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/star_gift_auction_box.h" #include "api/api_text_entities.h" +#include "base/timer_rpl.h" #include "base/unixtime.h" #include "boxes/peers/replace_boost_box.h" #include "boxes/send_credits_box.h" // CreditsEmojiSmall #include "boxes/share_box.h" #include "boxes/star_gift_box.h" #include "calls/group/calls_group_common.h" +#include "core/application.h" #include "core/credits_amount.h" #include "core/ui_integration.h" #include "data/components/credits.h" #include "data/components/gift_auctions.h" +#include "data/stickers/data_custom_emoji.h" +#include "data/data_document.h" #include "data/data_message_reactions.h" #include "data/data_session.h" #include "data/data_user.h" @@ -180,6 +184,17 @@ struct BidSliderValues { }; } +[[nodiscard]] QString NiceCountdownText(int seconds) { + const auto minutes = seconds / 60; + const auto hours = minutes / 60; + return hours + ? u"%1:%2:%3"_q + .arg(hours) + .arg((minutes % 60), 2, 10, QChar('0')) + .arg((seconds % 60), 2, 10, QChar('0')) + : u"%1:%2"_q.arg(minutes).arg((seconds % 60), 2, 10, QChar('0')); +} + [[nodiscard]] object_ptr MakeBidRow( not_null parent, std::shared_ptr show, @@ -380,17 +395,11 @@ object_ptr MakeAuctionInfoBlocks( auto untilTitle = rpl::duplicate( stateValue ) | rpl::map([=](const Data::GiftAuctionState &state) { - return SecondsLeftTillValue(state.nextRoundAt); - }) | rpl::flatten_latest() | rpl::map([=](int seconds) { - const auto minutes = seconds / 60; - const auto hours = minutes / 60; - return hours - ? u"%1:%2:%3"_q - .arg(hours) - .arg((minutes % 60), 2, 10, QChar('0')) - .arg((seconds % 60), 2, 10, QChar('0')) - : u"%1:%2"_q.arg(minutes).arg((seconds % 60), 2, 10, QChar('0')); - }) | Text::ToWithEntities(); + return SecondsLeftTillValue(state.nextRoundAt + ? state.nextRoundAt + : state.endDate); + }) | rpl::flatten_latest( + ) | rpl::map(NiceCountdownText) | rpl::map(tr::marked); auto leftTitle = rpl::duplicate( stateValue ) | rpl::map([=](const Data::GiftAuctionState &state) { @@ -653,7 +662,10 @@ void AuctionBidBox(not_null box, AuctionBidBoxArgs &&args) { box->closeBox(); if (const auto window = show->resolveWindow()) { - window->showPeer(update.to, ShowAtTheEndMsgId); + window->showPeerHistory( + update.to, + Window::SectionShow::Way::ClearStack, + ShowAtTheEndMsgId); } } }, box->lifetime()); @@ -801,6 +813,8 @@ void AuctionBidBox(not_null box, AuctionBidBoxArgs &&args) { lt_count, perRound, tr::rich), + .st = &st::auctionBidToast, + .attach = RectPart::Top, .duration = kBidPlacedToastDuration, }); } @@ -1561,4 +1575,285 @@ void AuctionAboutBox( ).append(' ').append(tr::lng_auction_about_understood(tr::now)))); } +TextWithEntities ActiveAuctionsTitle(const Data::ActiveAuctions &auctions) { + const auto &list = auctions.list; + if (list.size() == 1) { + const auto auction = list.front(); + return Data::SingleCustomEmoji( + auction->gift->document + ).append(' ').append(tr::lng_auction_bar_active(tr::now)); + } + auto result = tr::marked(); + for (const auto auction : list | ranges::views::take(3)) { + result.append(Data::SingleCustomEmoji(auction->gift->document)); + } + return result.append(' ').append( + tr::lng_auction_bar_active_many(tr::now, lt_count, list.size())); +} + +ManyAuctionsState ActiveAuctionsState(const Data::ActiveAuctions &auctions) { + const auto &list = auctions.list; + const auto winning = [](not_null auction) { + const auto position = MyAuctionPosition(*auction); + return (position <= auction->gift->auctionGiftsPerRound) + ? position + : 0; + }; + PROFILE_LOG(("GOT: POSITION %1, PERROUND: %2, WINNING: %3" + ).arg(MyAuctionPosition(*list.front()) + ).arg(list.front()->gift->auctionGiftsPerRound + ).arg(winning(list.front()))); + if (list.size() == 1) { + const auto auction = list.front(); + const auto position = winning(auction); + auto text = position + ? tr::lng_auction_bar_winning( + tr::now, + lt_count, + position, + tr::marked) + : tr::lng_auction_bar_outbid(tr::now, tr::marked); + return { std::move(text), !position }; + } + auto outbid = 0; + for (const auto auction : list) { + if (!winning(auction)) { + ++outbid; + } + } + auto text = (outbid == list.size()) + ? tr::lng_auction_bar_outbid_all(tr::now, tr::marked) + : outbid + ? tr::lng_auction_bar_outbid_some( + tr::now, + lt_count, + outbid, + tr::marked) + : tr::lng_auction_bar_winning_all(tr::now, tr::marked); + return { std::move(text), outbid != 0 }; +} + +rpl::producer ActiveAuctionsButton( + const Data::ActiveAuctions &auctions) { + const auto &list = auctions.list; + const auto withIcon = [](const QString &text) { + using namespace Ui::Text; + return IconEmoji(&st::auctionBidEmoji).append(' ').append(text); + }; + if (list.size() == 1) { + const auto auction = auctions.list.front(); + const auto end = auction->nextRoundAt + ? auction->nextRoundAt + : auction->endDate; + return SecondsLeftTillValue(end) + | rpl::map(NiceCountdownText) + | rpl::map(withIcon); + } + return tr::lng_auction_bar_view() | rpl::map(withIcon); +} + +struct Single { + QString slug; + not_null document; + int round = 0; + int total = 0; + int bid = 0; + int position = 0; + int winning = 0; + TimeId ends = 0; +}; + +object_ptr MakeActiveAuctionRow( + not_null parent, + not_null window, + not_null document, + const QString &slug, + rpl::producer value) { + auto result = object_ptr(parent); + const auto raw = result.data(); + + raw->add(object_ptr(raw)); + + auto title = rpl::duplicate(value) | rpl::map([=](const Single &fields) { + return tr::lng_auction_bar_round( + tr::now, + lt_n, + QString::number(fields.round + 1), + lt_amount, + QString::number(fields.total)); + }); + raw->add( + object_ptr( + raw, + std::move(title), + st::auctionListTitle), + st::auctionListTitlePadding); + + const auto tag = Data::CustomEmojiSizeTag::Isolated; + const auto sticker = std::shared_ptr( + document->owner().customEmojiManager().create( + document, + [=] { raw->update(); }, + tag)); + + raw->paintRequest( + ) | rpl::start_with_next([=] { + auto q = QPainter(raw); + sticker->paint(q, { + .textColor = st::windowFg->c, + .now = crl::now(), + .position = QPoint(), + }); + }, raw->lifetime()); + + auto helper = Ui::Text::CustomEmojiHelper(); + const auto star = helper.paletteDependent(Ui::Earn::IconCreditsEmoji()); + auto text = rpl::duplicate(value) | rpl::map([=](const Single &fields) { + const auto stars = tr::marked(star).append(' ').append( + Lang::FormatCountDecimal(fields.bid)); + const auto outbid = (fields.position > fields.winning); + return outbid + ? tr::lng_auction_bar_bid_outbid( + tr::now, + lt_stars, + stars, + tr::rich) + : tr::lng_auction_bar_bid_ranked( + tr::now, + lt_stars, + stars, + lt_n, + tr::marked(QString::number(fields.position)), + tr::rich); + }); + const auto subtitle = raw->add( + object_ptr( + raw, + std::move(text), + st::auctionListText, + st::defaultPopupMenu, + helper.context()), + st::auctionListTextPadding); + rpl::duplicate(value) | rpl::start_with_next([=](const Single &fields) { + const auto outbid = (fields.position > fields.winning); + subtitle->setTextColorOverride(outbid + ? st::attentionButtonFg->c + : std::optional()); + }, subtitle->lifetime()); + + const auto button = raw->add( + object_ptr( + raw, + rpl::single(QString()), + st::auctionListRaise), + st::auctionListRaisePadding); + button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + + auto secondsLeft = rpl::duplicate( + value + ) | rpl::map([=](const Single &fields) { + return SecondsLeftTillValue(fields.ends); + }) | rpl::flatten_latest(); + button->setText(rpl::combine( + std::move(secondsLeft), + tr::lng_auction_bar_raise_bid() + ) | rpl::map([=](int seconds, const QString &text) { + return Ui::Text::IconEmoji( + &st::auctionBidEmoji + ).append(' ').append(text).append(' ').append( + Ui::Text::Colorized(NiceCountdownText(seconds))); + })); + button->setClickedCallback([=] { + window->showStarGiftAuction(slug); + }); + button->setFullRadius(true); + raw->widthValue() | rpl::start_with_next([=](int width) { + button->setFullWidth(width); + }, button->lifetime()); + + return result; +} + +Fn ActiveAuctionsCallback( + not_null window, + const Data::ActiveAuctions &auctions) { + const auto &list = auctions.list; + const auto count = int(list.size()); + if (count == 1) { + const auto slug = list.front()->gift->auctionSlug; + return [=] { + window->showStarGiftAuction(slug); + }; + } + struct Auctions { + std::vector> list; + }; + const auto state = std::make_shared(); + const auto singleFrom = [](const Data::GiftAuctionState &state) { + return Single{ + .slug = state.gift->auctionSlug, + .document = state.gift->document, + .round = state.currentRound, + .total = state.totalRounds, + .bid = int(state.my.bid), + .position = MyAuctionPosition(state), + .winning = state.gift->auctionGiftsPerRound, + .ends = state.nextRoundAt ? state.nextRoundAt : state.endDate, + }; + }; + for (const auto auction : list) { + state->list.push_back(singleFrom(*auction)); + } + return [=] { + window->show(Box([=](not_null box) { + const auto rows = box->lifetime().make_state< + rpl::variable + >(count); + + box->setWidth(st::boxWideWidth); + box->setTitle(tr::lng_auction_bar_active_many( + lt_count, + rows->value() | tr::to_count())); + + const auto auctions = &window->session().giftAuctions(); + for (auto &entry : state->list) { + using Data::GiftAuctionState; + + const auto &now = entry.current(); + entry = auctions->state( + now.slug + ) | rpl::filter([=](const GiftAuctionState &state) { + return state.my.bid != 0; + }) | rpl::map(singleFrom); + + const auto skip = st::auctionListEntrySkip; + const auto row = box->addRow( + MakeActiveAuctionRow( + box, + window, + now.document, + now.slug, + entry.value()), + st::boxRowPadding + QMargins(0, skip, 0, skip)); + + auctions->state( + now.slug + ) | rpl::start_with_next([=](const GiftAuctionState &state) { + if (!state.my.bid) { + delete row; + if (const auto now = rows->current(); now > 1) { + *rows = (now - 1); + } else { + box->closeBox(); + } + } + }, row->lifetime()); + } + + box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); }); + box->addButton(tr::lng_box_ok(), [=] { box->closeBox(); }); + })); + }; +} + } // namespace Ui diff --git a/Telegram/SourceFiles/boxes/star_gift_auction_box.h b/Telegram/SourceFiles/boxes/star_gift_auction_box.h index eae0b32d8d..e79d1e394e 100644 --- a/Telegram/SourceFiles/boxes/star_gift_auction_box.h +++ b/Telegram/SourceFiles/boxes/star_gift_auction_box.h @@ -13,6 +13,7 @@ class Show; namespace Data { struct GiftAuctionState; +struct ActiveAuctions; } // namespace Data namespace Info::PeerGifts { @@ -60,4 +61,18 @@ void AuctionAboutBox( int giftsPerRound, Fn close)> understood); +[[nodiscard]] TextWithEntities ActiveAuctionsTitle( + const Data::ActiveAuctions &auctions); +struct ManyAuctionsState { + TextWithEntities text; + bool someOutbid = false; +}; +[[nodiscard]] ManyAuctionsState ActiveAuctionsState( + const Data::ActiveAuctions &auctions); +[[nodiscard]] rpl::producer ActiveAuctionsButton( + const Data::ActiveAuctions &auctions); +[[nodiscard]] Fn ActiveAuctionsCallback( + not_null window, + const Data::ActiveAuctions &auctions); + } // namespace Ui diff --git a/Telegram/SourceFiles/data/components/gift_auctions.cpp b/Telegram/SourceFiles/data/components/gift_auctions.cpp index 350975f999..11ac5a2d3c 100644 --- a/Telegram/SourceFiles/data/components/gift_auctions.cpp +++ b/Telegram/SourceFiles/data/components/gift_auctions.cpp @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "data/components/gift_auctions.h" +#include "api/api_hash.h" #include "api/api_premium.h" #include "api/api_text_entities.h" #include "apiwrap.h" @@ -18,6 +19,16 @@ namespace Data { GiftAuctions::GiftAuctions(not_null session) : _session(session) , _timer([=] { checkSubscriptions(); }) { + crl::on_main(_session, [=] { + rpl::merge( + _session->data().chatsListChanges(), + _session->data().chatsListLoadedEvents() + ) | rpl::filter( + !rpl::mappers::_1 + ) | rpl::take(1) | rpl::start_with_next([=] { + requestActive(); + }, _lifetime); + }); } GiftAuctions::~GiftAuctions() = default; @@ -50,15 +61,27 @@ rpl::producer GiftAuctions::state(const QString &slug) { void GiftAuctions::apply(const MTPDupdateStarGiftAuctionState &data) { if (const auto entry = find(data.vgift_id().v)) { + const auto was = myStateKey(entry->state); apply(entry, data.vstate()); entry->changes.fire({}); + if (was != myStateKey(entry->state)) { + _activeChanged.fire({}); + } + } else { + requestActive(); } } void GiftAuctions::apply(const MTPDupdateStarGiftAuctionUserState &data) { if (const auto entry = find(data.vgift_id().v)) { + const auto was = myStateKey(entry->state); apply(entry, data.vuser_state()); entry->changes.fire({}); + if (was != myStateKey(entry->state)) { + _activeChanged.fire({}); + } + } else { + requestActive(); } } @@ -106,6 +129,37 @@ void GiftAuctions::requestAcquired( }).send(); } +rpl::producer GiftAuctions::active() const { + return _activeChanged.events_starting_with_copy( + rpl::empty + ) | rpl::map([=] { + return collectActive(); + }); +} + +rpl::producer GiftAuctions::hasActiveChanges() const { + const auto has = hasActive(); + return _activeChanged.events( + ) | rpl::map([=] { + return hasActive(); + }) | rpl::combine_previous( + has + ) | rpl::filter([=](bool previous, bool current) { + return previous != current; + }) | rpl::map([=](bool previous, bool current) { + return current; + }); +} + +bool GiftAuctions::hasActive() const { + for (const auto &[slug, entry] : _map) { + if (myStateKey(entry->state)) { + return true; + } + } + return false; +} + void GiftAuctions::checkSubscriptions() { const auto now = crl::now(); auto next = crl::time(); @@ -128,6 +182,101 @@ void GiftAuctions::checkSubscriptions() { } } +auto GiftAuctions::myStateKey(const GiftAuctionState &state) const +-> MyStateKey { + if (!state.my.bid) { + return {}; + } + auto min = 0; + for (const auto &level : state.bidLevels) { + if (level.position > state.gift->auctionGiftsPerRound) { + break; + } else if (!min || min > level.amount) { + min = level.amount; + } + } + return { + .bid = int(state.my.bid), + .position = MyAuctionPosition(state), + .version = state.version, + }; +} + +ActiveAuctions GiftAuctions::collectActive() const { + auto result = ActiveAuctions(); + result.list.reserve(_map.size()); + for (const auto &[slug, entry] : _map) { + const auto raw = &entry->state; + if (raw->gift && raw->my.date) { + result.list.push_back(raw); + } + } + return result; +} + +uint64 GiftAuctions::countActiveHash() const { + auto result = Api::HashInit(); + for (const auto &active : collectActive().list) { + Api::HashUpdate(result, active->version); + Api::HashUpdate(result, active->my.date); + } + return Api::HashFinalize(result); +} + +void GiftAuctions::requestActive() { + if (_activeRequestId) { + return; + } + _activeRequestId = _session->api().request( + MTPpayments_GetStarGiftActiveAuctions(MTP_long(countActiveHash())) + ).done([=](const MTPpayments_StarGiftActiveAuctions &result) { + result.match([=](const MTPDpayments_starGiftActiveAuctions &data) { + const auto owner = &_session->data(); + owner->processUsers(data.vusers()); + + auto giftsFound = base::flat_set(); + const auto &list = data.vauctions().v; + giftsFound.reserve(list.size()); + for (const auto &auction : list) { + const auto &data = auction.data(); + auto gift = Api::FromTL(_session, data.vgift()); + const auto slug = gift ? gift->auctionSlug : QString(); + if (slug.isEmpty()) { + LOG(("Api Error: Bad auction gift.")); + continue; + } + auto &entry = _map[slug]; + if (!entry) { + entry = std::make_unique(); + } + const auto raw = entry.get(); + if (!raw->state.gift) { + raw->state.gift = std::move(gift); + } + apply(raw, data.vstate()); + apply(raw, data.vuser_state()); + giftsFound.emplace(slug); + } + for (const auto &[slug, entry] : _map) { + const auto my = &entry->state.my; + if (my->date && !giftsFound.contains(slug)) { + my->to = nullptr; + my->minBidAmount = 0; + my->bid = 0; + my->date = 0; + my->returned = false; + giftsFound.emplace(slug); + } + } + for (const auto &slug : giftsFound) { + _map[slug]->changes.fire({}); + } + _activeChanged.fire({}); + }, [](const MTPDpayments_starGiftActiveAuctionsNotModified &) { + }); + }).send(); +} + void GiftAuctions::request(const QString &slug) { auto &entry = _map[slug]; Assert(entry != nullptr); @@ -144,6 +293,8 @@ void GiftAuctions::request(const QString &slug) { raw->requested = false; const auto &data = result.data(); + _session->data().processUsers(data.vusers()); + raw->state.gift = Api::FromTL(_session, data.vgift()); if (!raw->state.gift) { return; @@ -152,8 +303,7 @@ void GiftAuctions::request(const QString &slug) { const auto ms = timeout * crl::time(1000); raw->state.subscribedTill = ms ? (crl::now() + ms) : -1; - _session->data().processUsers(data.vusers()); - + const auto was = myStateKey(raw->state); apply(raw, data.vstate()); apply(raw, data.vuser_state()); if (raw->changes.has_consumers()) { @@ -162,6 +312,9 @@ void GiftAuctions::request(const QString &slug) { _timer.callOnce(ms); } } + if (was != myStateKey(raw->state)) { + _activeChanged.fire({}); + } }).send(); } @@ -177,49 +330,54 @@ GiftAuctions::Entry *GiftAuctions::find(uint64 giftId) const { void GiftAuctions::apply( not_null entry, const MTPStarGiftAuctionState &state) { - Expects(entry->state.gift.has_value()); + apply(&entry->state, state); +} + +void GiftAuctions::apply( + not_null entry, + const MTPStarGiftAuctionState &state) { + Expects(entry->gift.has_value()); - const auto raw = &entry->state; state.match([&](const MTPDstarGiftAuctionState &data) { const auto version = data.vversion().v; - if (raw->version >= version) { + if (entry->version >= version) { return; } const auto owner = &_session->data(); - raw->startDate = data.vstart_date().v; - raw->endDate = data.vend_date().v; - raw->minBidAmount = data.vmin_bid_amount().v; + entry->startDate = data.vstart_date().v; + entry->endDate = data.vend_date().v; + entry->minBidAmount = data.vmin_bid_amount().v; const auto &levels = data.vbid_levels().v; - raw->bidLevels.clear(); - raw->bidLevels.reserve(levels.size()); + entry->bidLevels.clear(); + entry->bidLevels.reserve(levels.size()); for (const auto &level : levels) { - auto &entry = raw->bidLevels.emplace_back(); + auto &bid = entry->bidLevels.emplace_back(); const auto &data = level.data(); - entry.amount = data.vamount().v; - entry.position = data.vpos().v; - entry.date = data.vdate().v; + bid.amount = data.vamount().v; + bid.position = data.vpos().v; + bid.date = data.vdate().v; } const auto &top = data.vtop_bidders().v; - raw->topBidders.clear(); - raw->topBidders.reserve(top.size()); + entry->topBidders.clear(); + entry->topBidders.reserve(top.size()); for (const auto &user : top) { - raw->topBidders.push_back(owner->user(UserId(user.v))); + entry->topBidders.push_back(owner->user(UserId(user.v))); } - raw->nextRoundAt = data.vnext_round_at().v; - raw->giftsLeft = data.vgifts_left().v; - raw->currentRound = data.vcurrent_round().v; - raw->totalRounds = data.vtotal_rounds().v; - raw->averagePrice = 0; + entry->nextRoundAt = data.vnext_round_at().v; + entry->giftsLeft = data.vgifts_left().v; + entry->currentRound = data.vcurrent_round().v; + entry->totalRounds = data.vtotal_rounds().v; + entry->averagePrice = 0; }, [&](const MTPDstarGiftAuctionStateFinished &data) { - raw->averagePrice = data.vaverage_price().v; - raw->startDate = data.vstart_date().v; - raw->endDate = data.vend_date().v; - raw->minBidAmount = 0; - raw->nextRoundAt - = raw->currentRound - = raw->totalRounds - = raw->giftsLeft - = raw->version + entry->averagePrice = data.vaverage_price().v; + entry->startDate = data.vstart_date().v; + entry->endDate = data.vend_date().v; + entry->minBidAmount = 0; + entry->nextRoundAt + = entry->currentRound + = entry->totalRounds + = entry->giftsLeft + = entry->version = 0; }, [&](const MTPDstarGiftAuctionStateNotModified &data) { }); @@ -228,16 +386,32 @@ void GiftAuctions::apply( void GiftAuctions::apply( not_null entry, const MTPStarGiftAuctionUserState &state) { + apply(&entry->state.my, state); +} + +void GiftAuctions::apply( + not_null entry, + const MTPStarGiftAuctionUserState &state) { const auto &data = state.data(); - const auto raw = &entry->state.my; - raw->to = data.vbid_peer() + entry->to = data.vbid_peer() ? _session->data().peer(peerFromMTP(*data.vbid_peer())).get() : nullptr; - raw->minBidAmount = data.vmin_bid_amount().value_or(0); - raw->bid = data.vbid_amount().value_or(0); - raw->date = data.vbid_date().value_or(0); - raw->gotCount = data.vacquired_count().v; - raw->returned = data.is_returned(); + entry->minBidAmount = data.vmin_bid_amount().value_or(0); + entry->bid = data.vbid_amount().value_or(0); + entry->date = data.vbid_date().value_or(0); + entry->gotCount = data.vacquired_count().v; + entry->returned = data.is_returned(); +} + +int MyAuctionPosition(const GiftAuctionState &state) { + const auto &levels = state.bidLevels; + for (auto i = begin(levels), e = end(levels); i != e; ++i) { + if (i->amount < state.my.bid + || (i->amount == state.my.bid && i->date > state.my.date)) { + return i->position; + } + } + return (levels.empty() ? 0 : levels.back().position) + 1; } } // namespace Data diff --git a/Telegram/SourceFiles/data/components/gift_auctions.h b/Telegram/SourceFiles/data/components/gift_auctions.h index d86a342c32..ba8012707d 100644 --- a/Telegram/SourceFiles/data/components/gift_auctions.h +++ b/Telegram/SourceFiles/data/components/gift_auctions.h @@ -62,6 +62,10 @@ struct GiftAcquired { bool nameHidden = false; }; +struct ActiveAuctions { + std::vector> list; +}; + class GiftAuctions final { public: explicit GiftAuctions(not_null session); @@ -73,31 +77,63 @@ public: void apply(const MTPDupdateStarGiftAuctionUserState &data); void requestAcquired( - uint64 giftId, + uint64 giftId, Fn)> done); + [[nodiscard]] rpl::producer active() const; + [[nodiscard]] rpl::producer hasActiveChanges() const; + [[nodiscard]] bool hasActive() const; + private: struct Entry { GiftAuctionState state; rpl::event_stream<> changes; bool requested = false; }; + struct MyStateKey { + int bid = 0; + int position = 0; + int version = 0; + + explicit operator bool() const { + return bid != 0; + } + friend inline bool operator==(MyStateKey, MyStateKey) = default; + }; void request(const QString &slug); Entry *find(uint64 giftId) const; void apply( not_null entry, const MTPStarGiftAuctionState &state); + void apply( + not_null entry, + const MTPStarGiftAuctionState &state); void apply( not_null entry, const MTPStarGiftAuctionUserState &state); + void apply( + not_null entry, + const MTPStarGiftAuctionUserState &state); void checkSubscriptions(); + [[nodiscard]] MyStateKey myStateKey(const GiftAuctionState &state) const; + [[nodiscard]] ActiveAuctions collectActive() const; + [[nodiscard]] uint64 countActiveHash() const; + void requestActive(); + const not_null _session; base::Timer _timer; base::flat_map> _map; + rpl::event_stream<> _activeChanged; + mtpRequestId _activeRequestId = 0; + + rpl::lifetime _lifetime; + }; +[[nodiscard]] int MyAuctionPosition(const GiftAuctionState &state); + } // namespace Data diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style index fd9a620c3c..a85d68fc2e 100644 --- a/Telegram/SourceFiles/dialogs/dialogs.style +++ b/Telegram/SourceFiles/dialogs/dialogs.style @@ -129,6 +129,11 @@ dialogRowOpenBot: DialogRightButton { dialogRowOpenBotRecent: DialogRightButton(dialogRowOpenBot) { margin: margins(0px, 32px, 16px, 0px); } +dialogsTopBarRightButton: RoundButton(defaultActiveButton) { + width: -16px; + height: 22px; + textTop: 2px; +} forumDialogJumpArrow: icon{{ "dialogs/dialogs_topic_arrow", dialogsTextFg }}; forumDialogJumpArrowOver: icon{{ "dialogs/dialogs_topic_arrow", dialogsTextFgOver }}; diff --git a/Telegram/SourceFiles/dialogs/dialogs_top_bar_suggestion.cpp b/Telegram/SourceFiles/dialogs/dialogs_top_bar_suggestion.cpp index 8d25e709f7..91c4a177b3 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_top_bar_suggestion.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_top_bar_suggestion.cpp @@ -14,9 +14,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "base/call_delayed.h" #include "boxes/star_gift_box.h" // ShowStarGiftBox. +#include "boxes/star_gift_auction_box.h" #include "core/application.h" #include "core/click_handler_types.h" #include "core/ui_integration.h" +#include "data/components/gift_auctions.h" #include "data/components/promo_suggestions.h" #include "data/data_birthday.h" #include "data/data_changes.h" @@ -184,6 +186,7 @@ rpl::producer*> TopBarSuggestionValue( rpl::lifetime userpicLifetime; rpl::lifetime giftsLifetime; rpl::lifetime creditsLifetime; + rpl::lifetime auctionsLifetime; std::unique_ptr creditsHistory; }; @@ -229,6 +232,7 @@ rpl::producer*> TopBarSuggestionValue( state->userpicLifetime.destroy(); state->giftsLifetime.destroy(); state->creditsLifetime.destroy(); + state->auctionsLifetime.destroy(); if (!session->api().authorizations().unreviewed().empty()) { state->content = nullptr; @@ -273,7 +277,51 @@ rpl::producer*> TopBarSuggestionValue( const auto wrap = state->wrap.get(); using RightIcon = TopBarSuggestionContent::RightIcon; const auto promo = &session->promoSuggestions(); - if (const auto custom = promo->custom()) { + const auto auctions = &session->giftAuctions(); + if (auctions->hasActive()) { + using namespace Data; + struct Button { + rpl::variable text; + Fn callback; + base::has_weak_ptr guard; + }; + const auto button = state->auctionsLifetime.make_state< + Button + >(); + const auto window = FindSessionController(parent); + auctions->active( + ) | rpl::start_with_next([=](ActiveAuctions &&active) { + const auto empty = active.list.empty(); + state->desiredWrapToggle.force_assign( + Toggle{ !empty, anim::type::normal }); + if (empty) { + return; + } + + auto text = Ui::ActiveAuctionsState(active); + const auto textColorOverride = text.someOutbid + ? st::attentionButtonFg->c + : std::optional(); + content->setContent( + Ui::ActiveAuctionsTitle(active), + std::move(text.text), + Core::TextContext({ .session = session }), + textColorOverride); + button->text = Ui::ActiveAuctionsButton(active); + button->callback = Ui::ActiveAuctionsCallback( + window, + active); + }, state->auctionsLifetime); + const auto callback = crl::guard(&button->guard, [=] { + button->callback(); + }); + content->setRightButton(button->text.value(), callback); + content->setClickedCallback(callback); + content->setLeftPadding(state->leftPadding.value()); + state->desiredWrapToggle.force_assign( + Toggle{ true, anim::type::normal }); + return; + } else if (const auto custom = promo->custom()) { content->setRightIcon(RightIcon::Close); content->setLeftPadding(state->leftPadding.value()); content->setClickedCallback([=] { @@ -733,12 +781,14 @@ rpl::producer*> TopBarSuggestionValue( rpl::merge( session->promoSuggestions().value(), session->api().authorizations().unreviewedChanges(), - Data::AmPremiumValue(session) | rpl::skip(1) | rpl::to_empty + Data::AmPremiumValue(session) | rpl::skip(1) | rpl::to_empty, + session->giftAuctions().hasActiveChanges() | rpl::to_empty ) | rpl::start_with_next([=] { const auto was = state->wrap.get(); + const auto weak = base::make_weak(was); processCurrentSuggestion(processCurrentSuggestion); - if (was != state->wrap) { - consumer.put_next_copy(state->wrap); + if (was != state->wrap || (was && !weak)) { + consumer.put_next_copy(state->wrap.get()); } }, lifetime); diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_top_bar_suggestion_content.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_top_bar_suggestion_content.cpp index 04acfa5b88..e1e7386205 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_top_bar_suggestion_content.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_top_bar_suggestion_content.cpp @@ -165,6 +165,7 @@ TopBarSuggestionContent::TopBarSuggestionContent(not_null p) } void TopBarSuggestionContent::setRightIcon(RightIcon icon) { + _rightButton = nullptr; if (icon == _rightIcon) { return; } @@ -201,6 +202,35 @@ void TopBarSuggestionContent::setRightIcon(RightIcon icon) { } } +void TopBarSuggestionContent::setRightButton( + rpl::producer text, + Fn callback) { + _rightHide = nullptr; + _rightArrow = nullptr; + _rightIcon = RightIcon::None; + if (!text) { + _rightButton = nullptr; + return; + } + using namespace Ui; + _rightButton = base::make_unique_q( + this, + rpl::single(QString()), + st::dialogsTopBarRightButton); + _rightButton->setText(std::move(text)); + rpl::combine( + sizeValue(), + _rightButton->sizeValue() + ) | rpl::start_with_next([=](QSize outer, QSize inner) { + const auto top = (outer.height() - inner.height()) / 2; + _rightButton->moveToRight(top, top, outer.width()); + }, _rightButton->lifetime()); + _rightButton->setFullRadius(true); + _rightButton->setTextTransform(RoundButton::TextTransform::NoTransform); + _rightButton->setClickedCallback(std::move(callback)); + _rightButton->show(); +} + void TopBarSuggestionContent::draw(QPainter &p) { const auto kLinesForPhoto = 3; @@ -270,7 +300,7 @@ void TopBarSuggestionContent::draw(QPainter &p) { : availableWidth, }; }; - p.setPen(st::windowSubTextFg); + p.setPen(_descriptionColorOverride.value_or(st::windowSubTextFg->c)); _contentText.draw(p, { .position = QPoint(left, top), .outerWidth = availableWidth, @@ -288,7 +318,9 @@ void TopBarSuggestionContent::draw(QPainter &p) { void TopBarSuggestionContent::setContent( TextWithEntities title, TextWithEntities description, - std::optional context) { + std::optional context, + std::optional descriptionColorOverride) { + _descriptionColorOverride = descriptionColorOverride; if (context) { context->repaint = [=] { update(); }; _contentTitle.setMarkedText( @@ -305,6 +337,7 @@ void TopBarSuggestionContent::setContent( _contentTitle.setMarkedText(_contentTitleSt, std::move(title)); _contentText.setMarkedText(_contentTextSt, std::move(description)); } + update(); } void TopBarSuggestionContent::paintEvent(QPaintEvent *) { diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_top_bar_suggestion_content.h b/Telegram/SourceFiles/dialogs/ui/dialogs_top_bar_suggestion_content.h index 432f29fcc2..9a3a2efb8e 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_top_bar_suggestion_content.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_top_bar_suggestion_content.h @@ -45,12 +45,16 @@ public: void setContent( TextWithEntities title, TextWithEntities description, - std::optional context = std::nullopt); + std::optional context = std::nullopt, + std::optional descriptionColorOverride = std::nullopt); [[nodiscard]] rpl::producer desiredHeightValue() const override; void setHideCallback(Fn); void setRightIcon(RightIcon); + void setRightButton( + rpl::producer text, + Fn callback); void setLeftPadding(rpl::producer); [[nodiscard]] const style::TextStyle &contentTitleSt() const; @@ -69,9 +73,11 @@ private: Ui::Text::String _contentText; rpl::variable _lastPaintedContentLineAmount = 0; rpl::variable _lastPaintedContentTop = 0; + std::optional _descriptionColorOverride; base::unique_qptr _rightHide; base::unique_qptr _rightArrow; + base::unique_qptr _rightButton; Fn _hideCallback; int _leftPadding = 0; diff --git a/Telegram/SourceFiles/ui/effects/credits.style b/Telegram/SourceFiles/ui/effects/credits.style index 5cf630d3b1..adb5ffed15 100644 --- a/Telegram/SourceFiles/ui/effects/credits.style +++ b/Telegram/SourceFiles/ui/effects/credits.style @@ -444,6 +444,15 @@ videoStreamStarsCover: PremiumCover(creditsLowBalancePremiumCover) { auctionInfoPreviewMargin: margins(0px, 24px, 0px, 8px); auctionInfoSubtitleSkip: 8px; auctionInfoTableMargin: margins(0px, 12px, 0px, 12px); +auctionBidEmoji: IconEmoji { + icon: icon {{ "settings/button_auction", windowFg }}; + padding: margins(-4px, -1px, -4px, 0px); +} +auctionBidToast: Toast(defaultToast) { + padding: margins(54px, 13px, 19px, 12px); + icon: icon {{ "settings/toast_auction", toastFg }}; + iconPosition: point(18px, 18px); +} auctionAboutLogo: icon {{ "settings/large_auctions", windowFgActive }}; auctionAboutLogoPadding: margins(8px, 8px, 8px, 8px); auctionCenteredSubtitle: FlatLabel(defaultFlatLabel) { @@ -470,3 +479,19 @@ auctionBidStars: FlatLabel(defaultFlatLabel) { textFg: windowSubTextFg; } auctionBidSkip: 10px; + +auctionListEntrySkip: 12px; +auctionListTitle: FlatLabel(defaultFlatLabel) { + style: TextStyle(defaultTextStyle) { + font: font(15px semibold); + } + textFg: windowBoldFg; +} +auctionListTitlePadding: margins(50px, 0px, 0px, 0px); +auctionListText: FlatLabel(defaultFlatLabel) { +} +auctionListTextPadding: margins(50px, 4px, 0px, 0px); +auctionListRaise: RoundButton(defaultActiveButton) { + width: 0px; +} +auctionListRaisePadding: margins(0px, 8px, 0px, 0px); diff --git a/Telegram/lib_ui b/Telegram/lib_ui index 9fbbcfd466..be555efdb2 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit 9fbbcfd46679f9f61ff072beb8bf507166f852a2 +Subproject commit be555efdb20de610f55fde6dc8f670e2114e26fd