From c5d1739e9557829bac86b496bc8738f93a1a896c Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 8 Nov 2023 13:31:01 +0400 Subject: [PATCH] Implement multiboost reassign box. --- Telegram/CMakeLists.txt | 2 + Telegram/Resources/langs/lang.strings | 13 +- Telegram/SourceFiles/boxes/passcode_box.cpp | 10 +- Telegram/SourceFiles/boxes/peer_list_box.cpp | 10 + Telegram/SourceFiles/boxes/peer_list_box.h | 3 + .../boxes/peers/edit_peer_color_box.cpp | 3 +- .../boxes/peers/edit_peer_invite_link.cpp | 2 +- .../boxes/peers/replace_boost_box.cpp | 613 ++++++++++++++++++ .../boxes/peers/replace_boost_box.h | 53 ++ Telegram/SourceFiles/ui/boxes/boost_box.cpp | 50 +- Telegram/SourceFiles/ui/boxes/boost_box.h | 10 + Telegram/SourceFiles/ui/boxes/confirm_box.cpp | 7 - Telegram/SourceFiles/ui/boxes/confirm_box.h | 22 +- .../ui/controls/userpic_button.cpp | 53 -- .../SourceFiles/ui/controls/userpic_button.h | 5 - Telegram/SourceFiles/ui/effects/premium.style | 8 +- .../window/window_session_controller.cpp | 181 ++---- .../window/window_session_controller.h | 12 +- 18 files changed, 844 insertions(+), 213 deletions(-) create mode 100644 Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp create mode 100644 Telegram/SourceFiles/boxes/peers/replace_boost_box.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 03382f711d..109fecbb34 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -221,6 +221,8 @@ PRIVATE boxes/peers/peer_short_info_box.h boxes/peers/prepare_short_info_box.cpp boxes/peers/prepare_short_info_box.h + boxes/peers/replace_boost_box.cpp + boxes/peers/replace_boost_box.h boxes/about_box.cpp boxes/about_box.h boxes/about_sponsored_box.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index a02afaf8fe..4edc3388b3 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2052,6 +2052,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_boost_channel_post_stories#other" = "post **{count} stories** per day"; "lng_boost_error_gifted_title" = "Can't boost with gifted Premium!"; "lng_boost_error_gifted_text" = "Because your **Telegram Premium** subscription was gifted to you, you can't use it to boost channels."; +"lng_boost_need_more" = "More boosts needed"; +"lng_boost_need_more_text#one" = "To boost {channel}, gift **Telegram Premium** to a friend and get **{count}** boosts."; +"lng_boost_need_more_text#other" = "To boost {channel}, gift **Telegram Premium** to a friend and get **{count}** boosts."; +"lng_boost_need_more_again#one" = "To boost {channel} again, gift **Telegram Premium** to a friend and get **{count}** additional boost."; +"lng_boost_need_more_again#other" = "To boost {channel} again, gift **Telegram Premium** to a friend and get **{count}** additional boosts."; "lng_boost_error_already_title" = "Already Boosted!"; "lng_boost_error_already_text" = "You are already boosting this channel."; "lng_boost_error_premium_title" = "Premium needed!"; @@ -2062,12 +2067,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_boost_now_instead" = "You currently boost {channel}. Do you want to boost {other} instead?"; "lng_boost_now_replace" = "Replace"; "lng_boost_reassign_title" = "Reassign boost"; -"lng_boost_reassign_text" = "To boost {channel}, reassign a previous boost from another channel."; +"lng_boost_reassign_text" = "To boost {channel}, reassign a previous boost or {gift}."; +"lng_boost_reassign_gift#one" = "gift **Telegram Premium** to a friend to get **{count}** additional boost"; +"lng_boost_reassign_gift#other" = "gift **Telegram Premium** to a friend to get **{count}** additional boosts"; "lng_boost_remove_title" = "Remove your boost from"; "lng_boost_reassign_button" = "Reassign"; "lng_boost_available_in" = "available in {duration}"; "lng_boost_available_in_toast#one" = "Wait until the boost is available or get **{count}** more boost by gifting a **Telegram Premium** subscription."; "lng_boost_available_in_toast#other" = "Wait until the boost is available or get **{count}** more boosts by gifting a **Telegram Premium** subscription."; +"lng_boost_reassign_done#one" = "{count} boost is reassigned from {channels}."; +"lng_boost_reassign_done#other" = "{count} boosts are reassigned from {channels}."; +"lng_boost_reassign_channels#one" = "{count} channel"; +"lng_boost_reassign_channels#other" = "{count} channels"; "lng_boost_channel_title_color" = "Enable colors"; "lng_boost_channel_needs_level_color#one" = "Your channel needs to reach **Level {count}** to change channel color."; diff --git a/Telegram/SourceFiles/boxes/passcode_box.cpp b/Telegram/SourceFiles/boxes/passcode_box.cpp index dd165ac611..e320b434ff 100644 --- a/Telegram/SourceFiles/boxes/passcode_box.cpp +++ b/Telegram/SourceFiles/boxes/passcode_box.cpp @@ -567,12 +567,10 @@ void PasscodeBox::validateEmail( } else if (error.type() == u"EMAIL_HASH_EXPIRED"_q) { const auto weak = Ui::MakeWeak(this); _clearUnconfirmedPassword.fire({}); - if (weak) { - auto box = Ui::MakeInformBox({ - Lang::Hard::EmailConfirmationExpired() - }); - weak->getDelegate()->show( - std::move(box), + if (const auto strong = weak.data()) { + strong->getDelegate()->show( + Ui::MakeInformBox( + Lang::Hard::EmailConfirmationExpired()), Ui::LayerOption::CloseOther); } } else { diff --git a/Telegram/SourceFiles/boxes/peer_list_box.cpp b/Telegram/SourceFiles/boxes/peer_list_box.cpp index 189a132155..9f12a539e8 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_box.cpp @@ -1682,9 +1682,19 @@ crl::time PeerListContent::paintRow( return refreshStatusIn; } + const auto opacity = row->opacity(); const auto &bg = selected ? _st.item.button.textBgOver : _st.item.button.textBg; + if (opacity < 1.) { + p.setOpacity(opacity); + } + const auto guard = gsl::finally([&] { + if (opacity < 1.) { + p.setOpacity(1.); + } + }); + p.fillRect(0, 0, outerWidth, _rowHeight, bg); row->paintRipple(p, 0, 0, outerWidth); row->paintUserpic( diff --git a/Telegram/SourceFiles/boxes/peer_list_box.h b/Telegram/SourceFiles/boxes/peer_list_box.h index 1a53254e12..e99469b9ea 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.h +++ b/Telegram/SourceFiles/boxes/peer_list_box.h @@ -141,6 +141,9 @@ public: } virtual void rightActionStopLastRipple() { } + [[nodiscard]] virtual float64 opacity() { + return 1.; + } // By default elements code falls back to a simple right action code. virtual int elementsCount() const; diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp index 7c12c4661b..1913675afb 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_color_box.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "apiwrap.h" #include "base/unixtime.h" +#include "boxes/peers/replace_boost_box.h" #include "chat_helpers/compose/compose_show.h" #include "data/data_changes.h" #include "data/data_channel.h" @@ -520,7 +521,7 @@ void Apply( controller->showSection(Info::Boosts::Make(peer)); } }; - auto counters = Window::ParseBoostCounters(result); + auto counters = ParseBoostCounters(result); counters.mine = 0; // Don't show current level as just-reached. show->show(Box(Ui::AskBoostBox, Ui::AskBoostBoxData{ .link = qs(data.vboost_url()), diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp index f43112511d..1a32c9978e 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp @@ -1335,7 +1335,7 @@ object_ptr ShowInviteLinkBox( auto data = rpl::single(link) | rpl::then(std::move(updates)); auto initBox = [=, data = rpl::duplicate(data)]( - not_null box) { + not_null box) { rpl::duplicate( data ) | rpl::start_with_next([=](const LinkData &link) { diff --git a/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp b/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp new file mode 100644 index 0000000000..c629fe9488 --- /dev/null +++ b/Telegram/SourceFiles/boxes/peers/replace_boost_box.cpp @@ -0,0 +1,613 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "boxes/peers/replace_boost_box.h" + +#include "base/event_filter.h" +#include "base/unixtime.h" +#include "boxes/peer_list_box.h" +#include "data/data_channel.h" +#include "data/data_session.h" +#include "lang/lang_keys.h" +#include "main/main_account.h" +#include "main/main_app_config.h" +#include "main/main_session.h" +#include "main/session/session_show.h" +#include "ui/boxes/boost_box.h" +#include "ui/boxes/confirm_box.h" +#include "ui/chat/chat_style.h" +#include "ui/controls/userpic_button.h" +#include "ui/effects/premium_graphics.h" +#include "ui/layers/generic_box.h" +#include "ui/text/text_utilities.h" +#include "ui/toast/toast.h" +#include "ui/widgets/labels.h" +#include "ui/wrap/padding_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/empty_userpic.h" +#include "ui/painter.h" +#include "styles/style_boxes.h" +#include "styles/style_premium.h" + +namespace { + +constexpr auto kWaitingOpacity = 0.5; + +class Row final : public PeerListRow { +public: + Row( + not_null session, + TakenBoostSlot slot, + TimeId unixtimeNow, + crl::time preciseNow); + + void updateStatus(TimeId unixtimeNow, crl::time preciseNow); + [[nodiscard]] TakenBoostSlot data() const { + return _data; + } + [[nodiscard]] bool waiting() const { + return _waiting; + } + + QString generateName() override; + QString generateShortName() override; + PaintRoundImageCallback generatePaintUserpicCallback( + bool forceRound) override; + float64 opacity() override; + +private: + [[nodiscard]]PaintRoundImageCallback peerPaintUserpicCallback(); + + TakenBoostSlot _data; + PeerData *_peer = nullptr; + std::shared_ptr _empty; + Ui::PeerUserpicView _userpic; + crl::time _startPreciseTime = 0; + TimeId _startUnixtime = 0; + bool _waiting = false; + +}; + +class Controller final : public PeerListController { +public: + Controller(not_null to, std::vector from); + + [[nodiscard]] rpl::producer> selectedValue() const { + return _selected.value(); + } + + Main::Session &session() const override; + void prepare() override; + void rowClicked(not_null row) override; + bool trackSelectedList() override { + return false; + } + +private: + void updateWaitingState(); + + not_null _to; + std::vector _from; + rpl::variable> _selected; + rpl::variable>> _selectedPeers; + base::Timer _waitingTimer; + bool _hasWaitingRows = false; + +}; + +Row::Row( + not_null session, + TakenBoostSlot slot, + TimeId unixtimeNow, + crl::time preciseNow) +: PeerListRow(PeerListRowId(slot.id)) +, _data(slot) +, _peer(session->data().peerLoaded(_data.peerId)) +, _startPreciseTime(preciseNow) +, _startUnixtime(unixtimeNow) { + updateStatus(unixtimeNow, preciseNow); +} + +void Row::updateStatus(TimeId unixtimeNow, crl::time preciseNow) { + _waiting = (_data.cooldown > unixtimeNow); + if (_waiting) { + const auto initial = crl::time(_data.cooldown - _startUnixtime); + const auto elapsed = (preciseNow + 500 - _startPreciseTime) / 1000; + const auto seconds = initial + - std::clamp(elapsed, crl::time(), initial); + const auto hours = seconds / 3600; + const auto minutes = seconds / 60; + const auto duration = (hours > 0) + ? 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')); + setCustomStatus( + tr::lng_boost_available_in(tr::now, lt_duration, duration)); + } else { + const auto date = base::unixtime::parse(_data.expires); + setCustomStatus(tr::lng_boosts_list_status( + tr::now, + lt_date, + langDayOfMonth(date.date()))); + } +} + +QString Row::generateName() { + return _peer ? _peer->name() : u" "_q; +} + +QString Row::generateShortName() { + return _peer ? _peer->shortName() : generateName(); +} + +PaintRoundImageCallback Row::generatePaintUserpicCallback( + bool forceRound) { + if (_peer) { + return (forceRound && _peer->isForum()) + ? ForceRoundUserpicCallback(_peer) + : peerPaintUserpicCallback(); + } else if (!_empty) { + const auto colorIndex = _data.id % Ui::kColorIndexCount; + _empty = std::make_shared( + Ui::EmptyUserpic::UserpicColor(colorIndex), + u" "_q); + } + const auto empty = _empty; + return [=](Painter &p, int x, int y, int outerWidth, int size) { + empty->paintCircle(p, x, y, outerWidth, size); + }; +} + +float64 Row::opacity() { + return _waiting ? kWaitingOpacity : 1.; +} + +PaintRoundImageCallback Row::peerPaintUserpicCallback() { + const auto peer = _peer; + if (!_userpic.cloud && peer->hasUserpic()) { + _userpic = peer->createUserpicView(); + } + auto userpic = _userpic; + return [=](Painter &p, int x, int y, int outerWidth, int size) mutable { + peer->paintUserpicLeft(p, userpic, x, y, outerWidth, size); + }; +} + +Controller::Controller( + not_null to, + std::vector from) +: _to(to) +, _from(std::move(from)) +, _waitingTimer([=] { updateWaitingState(); }) { +} + +Main::Session &Controller::session() const { + return _to->session(); +} + +void Controller::prepare() { + delegate()->peerListSetTitle(tr::lng_boost_reassign_title()); + + const auto session = &_to->session(); + auto above = object_ptr((QWidget*)nullptr); + above->add( + CreateBoostReplaceUserpics( + above.data(), + _selectedPeers.value(), + _to), + st::boxRowPadding + st::boostReplaceUserpicsPadding); + above->add( + object_ptr( + above.data(), + tr::lng_boost_reassign_text( + lt_channel, + rpl::single(Ui::Text::Bold(_to->name())), + lt_gift, + tr::lng_boost_reassign_gift( + lt_count, + rpl::single(1. * BoostsForGift(session)), + Ui::Text::RichLangValue), + Ui::Text::RichLangValue), + st::boostReassignText), + st::boxRowPadding); + delegate()->peerListSetAboveWidget(std::move(above)); + + const auto now = base::unixtime::now(); + const auto precise = crl::now(); + ranges::stable_sort(_from, ranges::less(), [&](TakenBoostSlot slot) { + return (slot.cooldown > now) ? slot.cooldown : -slot.cooldown; + }); + for (const auto &slot : _from) { + auto row = std::make_unique(session, slot, now, precise); + if (row->waiting()) { + _hasWaitingRows = true; + } + delegate()->peerListAppendRow(std::move(row)); + } + + if (_hasWaitingRows) { + _waitingTimer.callEach(1000); + } + + delegate()->peerListRefreshRows(); +} + +void Controller::updateWaitingState() { + _hasWaitingRows = false; + const auto now = base::unixtime::now(); + const auto precise = crl::now(); + const auto count = delegate()->peerListFullRowsCount(); + for (auto i = 0; i != count; ++i) { + const auto bare = delegate()->peerListRowAt(i); + const auto row = static_cast(bare.get()); + if (row->waiting()) { + row->updateStatus(now, precise); + delegate()->peerListUpdateRow(row); + if (row->waiting()) { + _hasWaitingRows = true; + } + } + } + if (!_hasWaitingRows) { + _waitingTimer.cancel(); + } +} + +void Controller::rowClicked(not_null row) { + const auto slot = static_cast(row.get())->data(); + if (slot.cooldown > base::unixtime::now()) { + delegate()->peerListUiShow()->showToast({ + .text = tr::lng_boost_available_in_toast( + tr::now, + lt_count, + BoostsForGift(&session()), + Ui::Text::RichLangValue), + .adaptive = true, + }); + return; + } + auto now = _selected.current(); + const auto id = slot.id; + const auto checked = !row->checked(); + delegate()->peerListSetRowChecked(row, checked); + const auto peer = slot.peerId + ? _to->owner().peerLoaded(slot.peerId) + : nullptr; + auto peerRemoved = false; + if (checked) { + now.push_back(id); + } else { + now.erase(ranges::remove(now, id), end(now)); + + peerRemoved = true; + for (const auto left : now) { + const auto i = ranges::find(_from, left, &TakenBoostSlot::id); + Assert(i != end(_from)); + if (i->peerId == slot.peerId) { + peerRemoved = false; + break; + } + } + } + _selected = std::move(now); + + if (peer) { + auto selectedPeers = _selectedPeers.current(); + const auto i = ranges::find(selectedPeers, not_null(peer)); + if (peerRemoved) { + Assert(i != end(selectedPeers)); + selectedPeers.erase(i); + _selectedPeers = std::move(selectedPeers); + } else if (i == end(selectedPeers) && checked) { + selectedPeers.insert(begin(selectedPeers), peer); + _selectedPeers = std::move(selectedPeers); + } + } +} + +object_ptr ReassignBoostFloodBox(int seconds) { + const auto days = seconds / 86400; + const auto hours = seconds / 3600; + const auto minutes = seconds / 60; + return Ui::MakeInformBox({ + .text = tr::lng_boost_error_flood_text( + lt_left, + rpl::single(Ui::Text::Bold((days > 1) + ? tr::lng_days(tr::now, lt_count, days) + : (hours > 1) + ? tr::lng_hours(tr::now, lt_count, hours) + : (minutes > 1) + ? tr::lng_minutes(tr::now, lt_count, minutes) + : tr::lng_seconds(tr::now, lt_count, seconds))), + Ui::Text::RichLangValue), + .title = tr::lng_boost_error_flood_title(), + }); +} + +object_ptr ReassignBoostSingleBox( + not_null to, + TakenBoostSlot from, + Fn slots, int sources)> reassign, + Fn cancel) { + const auto reassigned = std::make_shared(); + const auto slot = from.id; + const auto peer = to->owner().peer(from.peerId); + const auto confirmed = [=](Fn close) { + *reassigned = true; + reassign({ slot }, 1); + close(); + }; + + auto result = Box([=](not_null box) { + Ui::ConfirmBox(box, { + .text = tr::lng_boost_now_instead( + lt_channel, + rpl::single(Ui::Text::Bold(peer->name())), + lt_other, + rpl::single(Ui::Text::Bold(to->name())), + Ui::Text::WithEntities), + .confirmed = confirmed, + .confirmText = tr::lng_boost_now_replace(), + .labelPadding = st::boxRowPadding, + }); + box->verticalLayout()->insert( + 0, + CreateBoostReplaceUserpics( + box, + rpl::single(std::vector{ peer }), + to), + st::boxRowPadding + st::boostReplaceUserpicsPadding); + }); + + result->boxClosing() | rpl::filter([=] { + return !*reassigned; + }) | rpl::start_with_next(cancel, result->lifetime()); + + return result; +} + +} // namespace + +ForChannelBoostSlots ParseForChannelBoostSlots( + not_null channel, + const QVector &boosts) { + auto result = ForChannelBoostSlots(); + const auto now = base::unixtime::now(); + for (const auto &my : boosts) { + const auto &data = my.data(); + const auto id = data.vslot().v; + const auto cooldown = data.vcooldown_until_date().value_or(0); + const auto peerId = data.vpeer() + ? peerFromMTP(*data.vpeer()) + : PeerId(); + if (!peerId && cooldown <= now) { + result.free.push_back(id); + } else if (peerId == channel->id) { + result.already.push_back(id); + } else { + result.other.push_back({ + .id = id, + .expires = data.vexpires().v, + .peerId = peerId, + .cooldown = cooldown, + }); + } + } + return result; +} + +Ui::BoostCounters ParseBoostCounters( + const MTPpremium_BoostsStatus &status) { + const auto &data = status.data(); + const auto slots = data.vmy_boost_slots(); + return { + .level = data.vlevel().v, + .boosts = data.vboosts().v, + .thisLevelBoosts = data.vcurrent_level_boosts().v, + .nextLevelBoosts = data.vnext_level_boosts().value_or_empty(), + .mine = slots ? slots->v.size() : 0, + }; +} + +int BoostsForGift(not_null session) { + const auto key = u"boosts_per_sent_gift"_q; + return session->account().appConfig().get(key, 0); +} + +[[nodiscard]] int SourcesCount( + const std::vector &from, + const std::vector &slots) { + auto checked = base::flat_set(); + checked.reserve(slots.size()); + for (const auto slot : slots) { + const auto i = ranges::find(from, slot, &TakenBoostSlot::id); + Assert(i != end(from)); + checked.emplace(i->peerId); + } + return checked.size(); +} + +object_ptr ReassignBoostsBox( + not_null to, + std::vector from, + Fn slots, int sources)> reassign, + Fn cancel) { + Expects(!from.empty()); + + const auto now = base::unixtime::now(); + if (from.size() == 1 && from.front().cooldown > now) { + cancel(); + return ReassignBoostFloodBox(from.front().cooldown - now); + } else if (from.size() == 1 && from.front().peerId) { + return ReassignBoostSingleBox(to, from.front(), reassign, cancel); + } + const auto reassigned = std::make_shared(); + auto controller = std::make_unique(to, from); + const auto raw = controller.get(); + auto initBox = [=](not_null box) { + raw->selectedValue( + ) | rpl::start_with_next([=](std::vector slots) { + box->clearButtons(); + if (!slots.empty()) { + const auto sources = SourcesCount(from, slots); + box->addButton(tr::lng_boost_reassign_button(), [=] { + *reassigned = true; + reassign(slots, sources); + }); + } + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); + }, box->lifetime()); + + box->boxClosing() | rpl::filter([=] { + return !*reassigned; + }) | rpl::start_with_next(cancel, box->lifetime()); + }; + return Box(std::move(controller), std::move(initBox)); +} + +object_ptr CreateBoostReplaceUserpics( + not_null parent, + rpl::producer>> from, + not_null to) { + struct State { + std::vector> from; + std::vector> buttons; + QImage layer; + rpl::variable count = 0; + bool painting = false; + }; + const auto full = st::boostReplaceUserpic.size.height() + + st::boostReplaceIconAdd.y() + + st::lineWidth; + auto result = object_ptr(parent, full); + const auto raw = result.data(); + const auto &st = st::boostReplaceUserpic; + const auto right = CreateChild(raw, to, st); + const auto overlay = CreateChild(raw); + + const auto state = raw->lifetime().make_state(); + std::move( + from + ) | rpl::start_with_next([=]( + const std::vector> &list) { + const auto &st = st::boostReplaceUserpic; + auto was = base::take(state->from); + auto buttons = base::take(state->buttons); + state->from.reserve(list.size()); + state->buttons.reserve(list.size()); + for (const auto &peer : list) { + state->from.push_back(peer); + const auto i = ranges::find(was, peer); + if (i != end(was)) { + const auto index = int(i - begin(was)); + Assert(buttons[index] != nullptr); + state->buttons.push_back(std::move(buttons[index])); + } else { + state->buttons.push_back( + std::make_unique(raw, peer, st)); + const auto raw = state->buttons.back().get(); + base::install_event_filter(raw, [=](not_null e) { + return (e->type() == QEvent::Paint && !state->painting) + ? base::EventFilterResult::Cancel + : base::EventFilterResult::Continue; + }); + } + } + state->count.force_assign(int(list.size())); + overlay->update(); + }, raw->lifetime()); + + rpl::combine( + raw->widthValue(), + state->count.value() + ) | rpl::start_with_next([=](int width, int count) { + const auto skip = st::boostReplaceUserpicsSkip; + const auto left = width - 2 * right->width() - skip; + const auto shift = std::min( + st::boostReplaceUserpicsShift, + (count > 1 ? (left / (count - 1)) : width)); + const auto total = right->width() + + (count ? (skip + right->width() + (count - 1) * shift) : 0); + auto x = (width - total) / 2; + for (const auto &single : state->buttons) { + single->moveToLeft(x, 0); + x += shift; + } + if (count) { + x += right->width() - shift + skip; + } + right->moveToLeft(x, 0); + overlay->setGeometry(QRect(0, 0, width, raw->height())); + }, raw->lifetime()); + + overlay->paintRequest( + ) | rpl::filter([=] { + return !state->buttons.empty(); + }) | rpl::start_with_next([=] { + const auto outerw = overlay->width(); + const auto ratio = style::DevicePixelRatio(); + if (state->layer.size() != QSize(outerw, full) * ratio) { + state->layer = QImage( + QSize(outerw, full) * ratio, + QImage::Format_ARGB32_Premultiplied); + state->layer.setDevicePixelRatio(ratio); + } + state->layer.fill(Qt::transparent); + + auto q = QPainter(&state->layer); + auto hq = PainterHighQualityEnabler(q); + const auto stroke = st::boostReplaceIconOutline; + const auto half = stroke / 2.; + auto pen = st::windowBg->p; + pen.setWidthF(stroke * 2.); + state->painting = true; + for (const auto &button : state->buttons) { + q.setPen(pen); + q.setBrush(Qt::NoBrush); + q.drawEllipse(button->geometry()); + const auto position = button->pos(); + button->render(&q, position, QRegion(), QWidget::DrawChildren); + } + state->painting = false; + const auto last = state->buttons.back().get(); + const auto add = st::boostReplaceIconAdd; + const auto skip = st::boostReplaceIconSkip; + const auto w = st::boostReplaceIcon.width() + 2 * skip; + const auto h = st::boostReplaceIcon.height() + 2 * skip; + const auto x = last->x() + last->width() - w + add.x(); + const auto y = last->y() + last->height() - h + add.y(); + + auto brush = QLinearGradient(QPointF(x + w, y + h), QPointF(x, y)); + brush.setStops(Ui::Premium::ButtonGradientStops()); + q.setBrush(brush); + pen.setWidthF(stroke); + q.setPen(pen); + q.drawEllipse(x - half, y - half, w + stroke, h + stroke); + st::boostReplaceIcon.paint(q, x + skip, y + skip, outerw); + + const auto size = st::boostReplaceArrow.size(); + st::boostReplaceArrow.paint( + q, + (last->x() + + last->width() + + (st::boostReplaceUserpicsSkip - size.width()) / 2), + (last->height() - size.height()) / 2, + outerw); + + q.end(); + + auto p = QPainter(overlay); + p.drawImage(0, 0, state->layer); + }, overlay->lifetime()); + return result; +} diff --git a/Telegram/SourceFiles/boxes/peers/replace_boost_box.h b/Telegram/SourceFiles/boxes/peers/replace_boost_box.h new file mode 100644 index 0000000000..f15cf0b148 --- /dev/null +++ b/Telegram/SourceFiles/boxes/peers/replace_boost_box.h @@ -0,0 +1,53 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "base/object_ptr.h" + +namespace Main { +class Session; +} // namespace Main + +namespace Ui { +struct BoostCounters; +class BoxContent; +class RpWidget; +} // namespace Ui + +struct TakenBoostSlot { + int id = 0; + TimeId expires = 0; + PeerId peerId = 0; + TimeId cooldown = 0; +}; + +struct ForChannelBoostSlots { + std::vector free; + std::vector already; + std::vector other; +}; + +[[nodiscard]] ForChannelBoostSlots ParseForChannelBoostSlots( + not_null channel, + const QVector &boosts); + +[[nodiscard]] Ui::BoostCounters ParseBoostCounters( + const MTPpremium_BoostsStatus &status); + +[[nodiscard]] int BoostsForGift(not_null session); + +object_ptr ReassignBoostsBox( + not_null to, + std::vector from, + Fn slots, int sources)> reassign, + Fn cancel); + +[[nodiscard]] object_ptr CreateBoostReplaceUserpics( + not_null parent, + rpl::producer>> from, + not_null to); diff --git a/Telegram/SourceFiles/ui/boxes/boost_box.cpp b/Telegram/SourceFiles/ui/boxes/boost_box.cpp index 197bcc0adb..68010c530f 100644 --- a/Telegram/SourceFiles/ui/boxes/boost_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/boost_box.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/boxes/boost_box.h" #include "lang/lang_keys.h" +#include "ui/boxes/confirm_box.h" #include "ui/effects/fireworks_animation.h" #include "ui/effects/premium_graphics.h" #include "ui/layers/generic_box.h" @@ -177,9 +178,10 @@ void BoostBox( (st::boxRowPadding + QMargins(0, st::boostTextSkip, 0, st::boostBottomSkip))); + const auto allowMulti = data.allowMulti; auto submit = state->data.value( ) | rpl::map([=](BoostCounters counters) { - return !counters.nextLevelBoosts + return (!counters.nextLevelBoosts || (counters.mine && !allowMulti)) ? tr::lng_box_ok() : (counters.mine > 0) ? tr::lng_boost_again_button() @@ -189,7 +191,8 @@ void BoostBox( const auto button = box->addButton(rpl::duplicate(submit), [=] { if (state->submitted) { return; - } else if (state->data.current().nextLevelBoosts > 0) { + } else if (state->data.current().nextLevelBoosts > 0 + && (allowMulti || !state->data.current().mine)) { state->submitted = true; const auto was = state->data.current().mine; @@ -346,6 +349,49 @@ object_ptr MakeLinkLabel( return result; } +void BoostBoxAlready(not_null box) { + ConfirmBox(box, { + .text = tr::lng_boost_error_already_text(Text::RichLangValue), + .title = tr::lng_boost_error_already_title(), + .inform = true, + }); +} + +void GiftForBoostsBox( + not_null box, + QString channel, + int receive, + bool again) { + ConfirmBox(box, { + .text = (again + ? tr::lng_boost_need_more_again + : tr::lng_boost_need_more_text)( + lt_count, + rpl::single(receive) | tr::to_count(), + lt_channel, + rpl::single(TextWithEntities{ channel }), + Text::RichLangValue), + .title = tr::lng_boost_need_more(), + .inform = true, + }); +} + +void GiftedNoBoostsBox(not_null box) { + InformBox(box, { + .text = tr::lng_boost_error_gifted_text(Text::RichLangValue), + .title = tr::lng_boost_error_gifted_title(), + }); +} + +void PremiumForBoostsBox(not_null box, Fn buyPremium) { + ConfirmBox(box, { + .text = tr::lng_boost_error_premium_text(Text::RichLangValue), + .confirmed = buyPremium, + .confirmText = tr::lng_boost_error_premium_yes(), + .title = tr::lng_boost_error_premium_title(), + }); +} + void AskBoostBox( not_null box, AskBoostBoxData data, diff --git a/Telegram/SourceFiles/ui/boxes/boost_box.h b/Telegram/SourceFiles/ui/boxes/boost_box.h index e8ef31993e..875a1405e6 100644 --- a/Telegram/SourceFiles/ui/boxes/boost_box.h +++ b/Telegram/SourceFiles/ui/boxes/boost_box.h @@ -33,6 +33,7 @@ struct BoostCounters { struct BoostBoxData { QString name; BoostCounters boost; + bool allowMulti = false; }; void BoostBox( @@ -40,6 +41,15 @@ void BoostBox( BoostBoxData data, Fn)> boost); +void BoostBoxAlready(not_null box); +void GiftForBoostsBox( + not_null box, + QString channel, + int receive, + bool again); +void GiftedNoBoostsBox(not_null box); +void PremiumForBoostsBox(not_null box, Fn buyPremium); + struct AskBoostBoxData { QString link; BoostCounters boost; diff --git a/Telegram/SourceFiles/ui/boxes/confirm_box.cpp b/Telegram/SourceFiles/ui/boxes/confirm_box.cpp index 22b2b39875..5af3f3b281 100644 --- a/Telegram/SourceFiles/ui/boxes/confirm_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/confirm_box.cpp @@ -100,11 +100,4 @@ object_ptr MakeConfirmBox(ConfirmBoxArgs &&args) { return Box(ConfirmBox, std::move(args)); } -object_ptr MakeInformBox(v::text::data text) { - return MakeConfirmBox({ - .text = std::move(text), - .inform = true, - }); -} - } // namespace Ui diff --git a/Telegram/SourceFiles/ui/boxes/confirm_box.h b/Telegram/SourceFiles/ui/boxes/confirm_box.h index b65a04361f..c33192e294 100644 --- a/Telegram/SourceFiles/ui/boxes/confirm_box.h +++ b/Telegram/SourceFiles/ui/boxes/confirm_box.h @@ -40,10 +40,24 @@ struct ConfirmBoxArgs { bool strictCancel = false; }; -void ConfirmBox(not_null box, ConfirmBoxArgs &&args); +void ConfirmBox(not_null box, ConfirmBoxArgs &&args); -[[nodiscard]] object_ptr MakeConfirmBox( - ConfirmBoxArgs &&args); -[[nodiscard]] object_ptr MakeInformBox(v::text::data text); +inline void InformBox(not_null box, ConfirmBoxArgs &&args) { + args.inform = true; + ConfirmBox(box, std::move(args)); +} + +[[nodiscard]] object_ptr MakeConfirmBox(ConfirmBoxArgs &&args); + +[[nodiscard]] inline object_ptr MakeInformBox( + ConfirmBoxArgs &&args) { + args.inform = true; + return MakeConfirmBox(std::move(args)); +} + +[[nodiscard]] inline object_ptr MakeInformBox( + v::text::data text) { + return MakeInformBox({ .text = std::move(text) }); +} } // namespace Ui diff --git a/Telegram/SourceFiles/ui/controls/userpic_button.cpp b/Telegram/SourceFiles/ui/controls/userpic_button.cpp index bad612d203..67717c5177 100644 --- a/Telegram/SourceFiles/ui/controls/userpic_button.cpp +++ b/Telegram/SourceFiles/ui/controls/userpic_button.cpp @@ -1108,57 +1108,4 @@ not_null CreateUploadSubButton( return upload; } -object_ptr CreateBoostReplaceUserpics( - not_null parent, - not_null from, - not_null to) { - const auto full = st::boostReplaceUserpic.size.height() - + st::boostReplaceIconAdd.y() - + st::lineWidth; - auto result = object_ptr(parent, full); - const auto raw = result.data(); - const auto &st = st::boostReplaceUserpic; - const auto left = CreateChild(raw, from, st); - const auto right = CreateChild(raw, to, st); - const auto overlay = CreateChild(raw); - raw->widthValue( - ) | rpl::start_with_next([=](int width) { - const auto skip = st::boostReplaceUserpicsSkip; - const auto total = left->width() + skip + right->width(); - left->moveToLeft((width - total) / 2, 0); - right->moveToLeft(left->x() + left->width() + skip, 0); - overlay->setGeometry(QRect(0, 0, width, raw->height())); - }, raw->lifetime()); - overlay->paintRequest( - ) | rpl::start_with_next([=] { - const auto outerw = overlay->width(); - const auto add = st::boostReplaceIconAdd; - const auto skip = st::boostReplaceIconSkip; - const auto w = st::boostReplaceIcon.width() + 2 * skip; - const auto h = st::boostReplaceIcon.height() + 2 * skip; - const auto x = left->x() + left->width() - w + add.x(); - const auto y = left->y() + left->height() - h + add.y(); - const auto stroke = st::boostReplaceIconOutline; - const auto half = stroke / 2.; - auto p = QPainter(overlay); - auto hq = PainterHighQualityEnabler(p); - auto pen = st::windowBg->p; - pen.setWidthF(stroke); - p.setPen(pen); - auto brush = QLinearGradient(QPointF(x + w, y + h), QPointF(x, y)); - brush.setStops(Premium::ButtonGradientStops()); - p.setBrush(brush); - p.drawEllipse(x - half, y - half, w + stroke, h + stroke); - st::boostReplaceIcon.paint(p, x + skip, y + skip, outerw); - - const auto size = st::boostReplaceArrow.size(); - st::boostReplaceArrow.paint( - p, - (outerw - size.width()) / 2, - (left->height() - size.height()) / 2, - outerw); - }, overlay->lifetime()); - return result; -} - } // namespace Ui diff --git a/Telegram/SourceFiles/ui/controls/userpic_button.h b/Telegram/SourceFiles/ui/controls/userpic_button.h index db3cdd3fdd..8d2eb2adf9 100644 --- a/Telegram/SourceFiles/ui/controls/userpic_button.h +++ b/Telegram/SourceFiles/ui/controls/userpic_button.h @@ -204,9 +204,4 @@ private: not_null contact, not_null controller); -[[nodiscard]] object_ptr CreateBoostReplaceUserpics( - not_null parent, - not_null from, - not_null to); - } // namespace Ui diff --git a/Telegram/SourceFiles/ui/effects/premium.style b/Telegram/SourceFiles/ui/effects/premium.style index ef364e0fe0..9c49a6c152 100644 --- a/Telegram/SourceFiles/ui/effects/premium.style +++ b/Telegram/SourceFiles/ui/effects/premium.style @@ -247,7 +247,6 @@ boostTitleSkip: 32px; boostTitle: FlatLabel(defaultFlatLabel) { minWidth: 40px; textFg: windowBoldFg; - align: align(top); maxHeight: 24px; style: TextStyle(boxTextStyle) { font: font(17px semibold); @@ -258,6 +257,10 @@ boostText: FlatLabel(defaultFlatLabel) { minWidth: 40px; align: align(top); } +boostReassignText: FlatLabel(defaultFlatLabel) { + minWidth: 40px; + align: align(top); +} boostBottomSkip: 6px; boostBox: Box(premiumPreviewDoubledLimitsBox) { buttonPadding: margins(22px, 22px, 22px, 22px); @@ -271,11 +274,12 @@ boostBox: Box(premiumPreviewDoubledLimitsBox) { boostReplaceUserpicsPadding: margins(0px, 18px, 0px, 20px); boostReplaceUserpicsSkip: 42px; +boostReplaceUserpicsShift: 24px; boostReplaceUserpic: UserpicButton(defaultUserpicButton) { size: size(60px, 60px); photoSize: 60px; } -boostReplaceIcon: icon{{ "stories/boost_mini", windowBg }}; +boostReplaceIcon: icon{{ "stories/boost_mini", premiumButtonFg }}; boostReplaceIconSkip: 3px; boostReplaceIconOutline: 2px; boostReplaceIconAdd: point(4px, 2px); diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 2cb64bcca7..89fac17eff 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/add_contact_box.h" #include "boxes/peers/add_bot_to_chat_box.h" #include "boxes/peers/edit_peer_info_box.h" +#include "boxes/peers/replace_boost_box.h" #include "boxes/delete_messages_box.h" #include "window/window_adaptive.h" #include "window/window_controller.h" @@ -267,19 +268,6 @@ Fn PausedIn( return [=] { return IsPaused(controller, level); }; } -Ui::BoostCounters ParseBoostCounters( - const MTPpremium_BoostsStatus &status) { - const auto &data = status.data(); - const auto slots = data.vmy_boost_slots(); - return { - .level = data.vlevel().v, - .boosts = data.vboosts().v, - .thisLevelBoosts = data.vcurrent_level_boosts().v, - .nextLevelBoosts = data.vnext_level_boosts().value_or_empty(), - .mine = slots ? slots->v.size() : 0, - }; -} - bool operator==(const PeerThemeOverride &a, const PeerThemeOverride &b) { return (a.peer == b.peer) && (a.theme == b.theme); } @@ -648,6 +636,7 @@ void SessionNavigation::resolveBoostState(not_null channel) { uiShow()->show(Box(Ui::BoostBox, Ui::BoostBoxData{ .name = channel->name(), .boost = ParseBoostCounters(result), + .allowMulti = (BoostsForGift(_session) > 0), }, submit)); }).fail([=](const MTP::Error &error) { _boostStateResolving = nullptr; @@ -663,86 +652,66 @@ void SessionNavigation::applyBoost( const auto &data = result.data(); _session->data().processUsers(data.vusers()); _session->data().processChats(data.vchats()); - const auto &list = data.vmy_boosts().v; - if (list.isEmpty()) { - if (!_session->premium()) { - const auto jumpToPremium = [=] { + const auto slots = ParseForChannelBoostSlots( + channel, + data.vmy_boosts().v); + if (!slots.free.empty()) { + applyBoostsChecked(channel, { slots.free.front() }, done); + } else if (slots.other.empty()) { + if (!slots.already.empty()) { + if (const auto receive = BoostsForGift(_session)) { + const auto again = true; + const auto name = channel->name(); + uiShow()->show( + Box(Ui::GiftForBoostsBox, name, receive, again)); + } else { + uiShow()->show(Box(Ui::BoostBoxAlready)); + } + } else if (!_session->premium()) { + uiShow()->show(Box(Ui::PremiumForBoostsBox, [=] { const auto id = peerToChannel(channel->id).bare; Settings::ShowPremium( parentController(), "channel_boost__" + QString::number(id)); - }; - uiShow()->show(Ui::MakeConfirmBox({ - .text = tr::lng_boost_error_premium_text( - Ui::Text::RichLangValue), - .confirmed = jumpToPremium, - .confirmText = tr::lng_boost_error_premium_yes(), - .title = tr::lng_boost_error_premium_title(), })); + } else if (const auto receive = BoostsForGift(_session)) { + const auto again = false; + const auto name = channel->name(); + uiShow()->show( + Box(Ui::GiftForBoostsBox, name, receive, again)); } else { - uiShow()->show(Ui::MakeConfirmBox({ - .text = tr::lng_boost_error_gifted_text( - Ui::Text::RichLangValue), - .title = tr::lng_boost_error_gifted_title(), - .inform = true, - })); + uiShow()->show(Box(Ui::GiftedNoBoostsBox)); } done({}); - return; - } - auto slot = int(); - auto different = PeerId(); - auto earliest = TimeId(-1); - const auto now = base::unixtime::now(); - for (const auto &my : list) { - const auto &data = my.data(); - const auto cooldown = data.vcooldown_until_date().value_or(0); - const auto peerId = data.vpeer() - ? peerFromMTP(*data.vpeer()) - : PeerId(); - if (!peerId && cooldown <= now) { - applyBoostChecked(channel, data.vslot().v, done); - return; - } else if (peerId != channel->id - && (earliest < 0 || cooldown < earliest)) { - slot = data.vslot().v; - different = peerId; - earliest = cooldown; - } - } - if (different) { - if (earliest > now) { - const auto seconds = earliest - now; - const auto days = seconds / 86400; - const auto hours = seconds / 3600; - const auto minutes = seconds / 60; - uiShow()->show(Ui::MakeConfirmBox({ - .text = tr::lng_boost_error_flood_text( - lt_left, - rpl::single(Ui::Text::Bold((days > 1) - ? tr::lng_days(tr::now, lt_count, days) - : (hours > 1) - ? tr::lng_hours(tr::now, lt_count, hours) - : (minutes > 1) - ? tr::lng_minutes(tr::now, lt_count, minutes) - : tr::lng_seconds(tr::now, lt_count, seconds))), - Ui::Text::RichLangValue), - .title = tr::lng_boost_error_flood_title(), - .inform = true, - })); - done({}); - } else { - const auto peer = _session->data().peer(different); - replaceBoostConfirm(peer, channel, slot, done); - } } else { - uiShow()->show(Ui::MakeConfirmBox({ - .text = tr::lng_boost_error_already_text( - Ui::Text::RichLangValue), - .title = tr::lng_boost_error_already_title(), - .inform = true, - })); - done({}); + const auto weak = std::make_shared>(); + const auto reassign = [=](std::vector slots, int sources) { + const auto count = int(slots.size()); + const auto callback = [=](Ui::BoostCounters counters) { + if (const auto strong = weak->data()) { + strong->closeBox(); + } + done(counters); + uiShow()->showToast(tr::lng_boost_reassign_done( + tr::now, + lt_count, + count, + lt_channels, + tr::lng_boost_reassign_channels( + tr::now, + lt_count, + sources))); + }; + applyBoostsChecked( + channel, + slots, + crl::guard(this, callback)); + }; + *weak = uiShow()->show(ReassignBoostsBox( + channel, + slots.other, + reassign, + [=] { done({}); })); } }).fail([=](const MTP::Error &error) { const auto type = error.type(); @@ -751,48 +720,18 @@ void SessionNavigation::applyBoost( }).handleFloodErrors().send(); } -void SessionNavigation::replaceBoostConfirm( - not_null from, +void SessionNavigation::applyBoostsChecked( not_null channel, - int slot, + std::vector slots, Fn done) { - const auto forwarded = std::make_shared(false); - const auto confirmed = [=](Fn close) { - *forwarded = true; - applyBoostChecked(channel, slot, done); - close(); - }; - const auto box = uiShow()->show(Box([=](not_null box) { - Ui::ConfirmBox(box, { - .text = tr::lng_boost_now_instead( - lt_channel, - rpl::single(Ui::Text::Bold(from->name())), - lt_other, - rpl::single(Ui::Text::Bold(channel->name())), - Ui::Text::WithEntities), - .confirmed = confirmed, - .confirmText = tr::lng_boost_now_replace(), - .labelPadding = st::boxRowPadding, - }); - box->verticalLayout()->insert( - 0, - Ui::CreateBoostReplaceUserpics(box, from, channel), - st::boxRowPadding + st::boostReplaceUserpicsPadding); + auto mtp = MTP_vector_from_range(ranges::views::all( + slots + ) | ranges::views::transform([](int slot) { + return MTP_int(slot); })); - box->boxClosing() | rpl::filter([=] { - return !*forwarded; - }) | rpl::start_with_next([=] { - done({}); - }, box->lifetime()); -} - -void SessionNavigation::applyBoostChecked( - not_null channel, - int slot, - Fn done) { _api.request(MTPpremium_ApplyBoost( MTP_flags(MTPpremium_ApplyBoost::Flag::f_slots), - MTP_vector({ MTP_int(slot) }), + std::move(mtp), channel->input )).done([=](const MTPpremium_MyBoosts &result) { _api.request(MTPpremium_GetBoostsStatus( diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index aecd4150d0..0a5fa22252 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -318,14 +318,9 @@ private: void applyBoost( not_null channel, Fn done); - void replaceBoostConfirm( - not_null from, + void applyBoostsChecked( not_null channel, - int slot, - Fn done); - void applyBoostChecked( - not_null channel, - int slot, + std::vector slots, Fn done); const not_null _session; @@ -755,7 +750,4 @@ void ActivateWindow(not_null controller); not_null controller, GifPauseReason level); -[[nodiscard]] Ui::BoostCounters ParseBoostCounters( - const MTPpremium_BoostsStatus &status); - } // namespace Window