Improve paid peer-box multi-send.

This commit is contained in:
John Preston 2025-02-25 14:53:23 +04:00
parent ee9d0cfd99
commit fe2df96953
11 changed files with 226 additions and 46 deletions

View file

@ -873,6 +873,7 @@ void PeerListRow::paintUserpic(
} else if (const auto callback = generatePaintUserpicCallback(false)) {
callback(p, x, y, outerWidth, st.photoSize);
}
paintUserpicOverlay(p, st, x, y, outerWidth);
}
// Emulates Ui::RoundImageCheckbox::paint() in a checked state.

View file

@ -95,6 +95,13 @@ public:
[[nodiscard]] virtual QString generateShortName();
[[nodiscard]] virtual auto generatePaintUserpicCallback(
bool forceRound) -> PaintRoundImageCallback;
virtual void paintUserpicOverlay(
Painter &p,
const style::PeerListItem &st,
int x,
int y,
int outerWidth) {
}
[[nodiscard]] virtual auto generateNameFirstLetters() const
-> const base::flat_set<QChar> &;

View file

@ -311,24 +311,23 @@ void RecipientRow::setRestriction(Api::MessageMoneyRestriction restriction) {
_restriction->value = restriction;
}
PaintRoundImageCallback RecipientRow::generatePaintUserpicCallback(
bool forceRound) {
auto result = PeerListRow::generatePaintUserpicCallback(forceRound);
void RecipientRow::paintUserpicOverlay(
Painter &p,
const style::PeerListItem &st,
int x,
int y,
int outerWidth) {
if (const auto &r = _restriction) {
return [=](Painter &p, int x, int y, int outerWidth, int size) {
result(p, x, y, outerWidth, size);
PaintRestrictionBadge(
p,
_maybeLockedSt,
r->value.starsPerMessage,
r->cache,
x,
y,
outerWidth,
size);
};
PaintRestrictionBadge(
p,
_maybeLockedSt,
r->value.starsPerMessage,
r->cache,
x,
y,
outerWidth,
st.photoSize);
}
return result;
}
bool RecipientRow::refreshLock(

View file

@ -136,8 +136,12 @@ public:
[[nodiscard]] History *maybeHistory() const {
return _maybeHistory;
}
PaintRoundImageCallback generatePaintUserpicCallback(
bool forceRound) override;
void paintUserpicOverlay(
Painter &p,
const style::PeerListItem &st,
int x,
int y,
int outerWidth) override;
void preloadUserpic() override;

View file

@ -675,7 +675,7 @@ void ShareBox::submit(Api::SendOptions options) {
});
const auto alreadyApproved = options.starsApproved;
auto paid = std::vector<not_null<Data::Thread*>>();
auto paid = std::vector<not_null<PeerData*>>();
auto waiting = base::flat_set<not_null<PeerData*>>();
auto totalStars = 0;
for (const auto &thread : threads) {
@ -685,7 +685,7 @@ void ShareBox::submit(Api::SendOptions options) {
waiting.emplace(peer);
} else if (details->stars > 0) {
totalStars += details->stars;
paid.push_back(thread);
paid.push_back(peer);
}
}
if (!waiting.empty()) {
@ -963,7 +963,7 @@ void ShareBox::Inner::initChatRestriction(not_null<Chat*> chat) {
const auto restriction = Api::ResolveMessageMoneyRestrictions(
history->peer,
history);
if (restriction) {
if (restriction || restriction.known) {
chat->restriction = restriction;
}
}

View file

@ -267,7 +267,7 @@ void ShowSendPaidConfirm(
PaidConfirmStyles styles) {
ShowSendPaidConfirm(
std::move(show),
std::vector<not_null<Data::Thread*>>{ peer->owner().history(peer) },
std::vector<not_null<PeerData*>>{ peer },
details,
confirmed,
styles);
@ -275,15 +275,15 @@ void ShowSendPaidConfirm(
void ShowSendPaidConfirm(
std::shared_ptr<Main::SessionShow> show,
const std::vector<not_null<Data::Thread*>> &threads,
const std::vector<not_null<PeerData*>> &peers,
SendPaymentDetails details,
Fn<void()> confirmed,
PaidConfirmStyles styles) {
Expects(!threads.empty());
Expects(!peers.empty());
const auto singlePeer = (threads.size() > 1)
const auto singlePeer = (peers.size() > 1)
? (PeerData*)nullptr
: threads.front()->peer().get();
: peers.front().get();
const auto recipientId = singlePeer ? singlePeer->id : PeerId();
const auto check = [=] {
const auto required = details.stars;
@ -303,8 +303,8 @@ void ShowSendPaidConfirm(
done);
};
auto usersOnly = true;
for (const auto &thread : threads) {
if (!thread->peer()->isUser()) {
for (const auto &peer : peers) {
if (!peer->isUser()) {
usersOnly = false;
break;
}
@ -342,7 +342,7 @@ void ShowSendPaidConfirm(
: tr::lng_payment_confirm_chats)(
tr::now,
lt_count,
int(threads.size()),
int(peers.size()),
Ui::Text::RichLangValue)).append(' ').append(
tr::lng_payment_confirm_sure(
tr::now,

View file

@ -167,7 +167,7 @@ void ShowSendPaidConfirm(
PaidConfirmStyles styles = {});
void ShowSendPaidConfirm(
std::shared_ptr<Main::SessionShow> show,
const std::vector<not_null<Data::Thread*>> &threads,
const std::vector<not_null<PeerData*>> &peers,
SendPaymentDetails details,
Fn<void()> confirmed,
PaidConfirmStyles styles = {});

View file

@ -436,7 +436,7 @@ void BottomInfo::layoutDateText() {
auto marked = TextWithEntities{ full };
if (const auto count = _data.stars) {
marked.append(Ui::Text::IconEmoji(&st::starIconEmoji));
marked.append(QString::number(count));
marked.append(Lang::FormatCountToShort(count).string);
}
_authorEditedDate.setMarkedText(
st::msgDateTextStyle,

View file

@ -1809,11 +1809,15 @@ void WebViewInstance::botSendPreparedMessage(
QPointer<Ui::BoxContent> preview;
QPointer<Ui::BoxContent> choose;
rpl::event_stream<not_null<Data::Thread*>> recipient;
Fn<void(Api::SendOptions)> send;
SendPaymentHelper sendPayment;
bool sent = false;
};
const auto state = std::make_shared<State>();
auto recipient = state->recipient.events();
const auto send = [=](std::vector<not_null<Data::Thread*>> list) {
const auto send = [=](
std::vector<not_null<Data::Thread*>> list,
Api::SendOptions options) {
if (state->sent) {
return;
}
@ -1852,7 +1856,7 @@ void WebViewInstance::botSendPreparedMessage(
bot->session().api().sendInlineResult(
bot,
parsed.get(),
Api::SendAction(thread),
Api::SendAction(thread, options),
std::nullopt,
done);
}
@ -1881,7 +1885,33 @@ void WebViewInstance::botSendPreparedMessage(
state->choose = box.data();
panel->showBox(std::move(box));
}, [=](not_null<Data::Thread*> thread) {
send({ thread });
const auto weak = base::make_weak(thread);
state->send = [=](Api::SendOptions options) {
const auto strong = weak.get();
if (!strong) {
state->send = nullptr;
return;
}
const auto withPaymentApproved = [=](int stars) {
if (const auto onstack = state->send) {
auto copy = options;
copy.starsApproved = stars;
onstack(copy);
}
};
const auto checked = state->sendPayment.check(
uiShow(),
strong->peer(),
1,
options.starsApproved,
withPaymentApproved);
if (!checked) {
return;
}
state->send = nullptr;
send({ strong }, options);
};
state->send({});
});
box->boxClosing() | rpl::start_with_next([=] {
if (!state->sent) {

View file

@ -1960,7 +1960,9 @@ object_ptr<Ui::BoxContent> PrepareChooseRecipientBox(
rpl::producer<QString> titleOverride,
FnMut<void()> &&successCallback,
InlineBots::PeerTypes typesRestriction,
Fn<void(std::vector<not_null<Data::Thread*>>)> sendMany) {
Fn<void(
std::vector<not_null<Data::Thread*>>,
Api::SendOptions)> sendMany) {
const auto weak = std::make_shared<QPointer<PeerListBox>>();
const auto selectable = (sendMany != nullptr);
class Controller final : public ChooseRecipientBoxController {
@ -2073,19 +2075,82 @@ object_ptr<Ui::BoxContent> PrepareChooseRecipientBox(
std::move(filter),
selectable);
const auto raw = controller.get();
struct State {
Fn<void(Api::SendOptions)> submit;
rpl::lifetime submitLifetime;
};
const auto state = std::make_shared<State>();
auto initBox = [=](not_null<PeerListBox*> box) {
raw->hasSelectedChanges(
) | rpl::start_with_next([=](bool shown) {
box->clearButtons();
if (shown) {
box->addButton(tr::lng_send_button(), [=] {
const auto weak = Ui::MakeWeak(box);
state->submit = [=](Api::SendOptions options) {
state->submitLifetime.destroy();
const auto show = box->peerListUiShow();
const auto peers = box->collectSelectedRows();
const auto withPaymentApproved = crl::guard(weak, [=](
int approved) {
auto copy = options;
copy.starsApproved = approved;
if (const auto onstack = state->submit) {
onstack(copy);
}
});
const auto alreadyApproved = options.starsApproved;
auto paid = std::vector<not_null<PeerData*>>();
auto waiting = base::flat_set<not_null<PeerData*>>();
auto totalStars = 0;
for (const auto &peer : peers) {
const auto details = ComputePaymentDetails(peer, 1);
if (!details) {
waiting.emplace(peer);
} else if (details->stars > 0) {
totalStars += details->stars;
paid.push_back(peer);
}
}
if (!waiting.empty()) {
session->changes().peerUpdates(
Data::PeerUpdate::Flag::FullInfo
) | rpl::start_with_next([=](const Data::PeerUpdate &update) {
if (waiting.contains(update.peer)) {
withPaymentApproved(alreadyApproved);
}
}, state->submitLifetime);
if (!session->credits().loaded()) {
session->credits().loadedValue(
) | rpl::filter(
rpl::mappers::_1
) | rpl::take(1) | rpl::start_with_next([=] {
withPaymentApproved(alreadyApproved);
}, state->submitLifetime);
}
return;
} else if (totalStars > alreadyApproved) {
ShowSendPaidConfirm(show, paid, SendPaymentDetails{
.messages = 1,
.stars = totalStars,
}, [=] { withPaymentApproved(totalStars); });
return;
}
state->submit = nullptr;
sendMany(ranges::views::all(
peers
) | ranges::views::transform([&](
not_null<PeerData*> peer) -> Controller::Chosen {
return peer->owner().history(peer);
}) | ranges::to_vector);
}) | ranges::to_vector, options);
};
box->addButton(tr::lng_send_button(), [=] {
if (const auto onstack = state->submit) {
onstack({});
}
});
}
box->addButton(tr::lng_cancel(), [=] {
@ -2257,6 +2322,8 @@ QPointer<Ui::BoxContent> ShowForwardMessagesBox(
not_null<ListBox*> box;
not_null<Controller*> controller;
base::unique_qptr<Ui::PopupMenu> menu;
Fn<void(Api::SendOptions options)> submit;
rpl::lifetime submitLifetime;
};
const auto applyFilter = [=](not_null<ListBox*> box, FilterId id) {
@ -2401,8 +2468,62 @@ QPointer<Ui::BoxContent> ShowForwardMessagesBox(
session->data().message(msgIds.front())->history(),
msgIds);
const auto submit = [=](Api::SendOptions options) {
const auto weak = Ui::MakeWeak(state->box);
state->submit = [=](Api::SendOptions options) {
const auto peers = state->box->collectSelectedRows();
const auto checkPaid = [=](int messagesCount) {
const auto withPaymentApproved = crl::guard(weak, [=](
int approved) {
auto copy = options;
copy.starsApproved = approved;
if (const auto onstack = state->submit) {
onstack(copy);
}
});
const auto alreadyApproved = options.starsApproved;
auto paid = std::vector<not_null<PeerData*>>();
auto waiting = base::flat_set<not_null<PeerData*>>();
auto totalStars = 0;
for (const auto &peer : peers) {
const auto details = ComputePaymentDetails(
peer,
messagesCount);
if (!details) {
waiting.emplace(peer);
} else if (details->stars > 0) {
totalStars += details->stars;
paid.push_back(peer);
}
}
if (!waiting.empty()) {
session->changes().peerUpdates(
Data::PeerUpdate::Flag::FullInfo
) | rpl::start_with_next([=](const Data::PeerUpdate &update) {
if (waiting.contains(update.peer)) {
withPaymentApproved(alreadyApproved);
}
}, state->submitLifetime);
if (!session->credits().loaded()) {
session->credits().loadedValue(
) | rpl::filter(
rpl::mappers::_1
) | rpl::take(1) | rpl::start_with_next([=] {
withPaymentApproved(alreadyApproved);
}, state->submitLifetime);
}
return false;
} else if (totalStars > alreadyApproved) {
ShowSendPaidConfirm(show, paid, SendPaymentDetails{
.messages = messagesCount,
.stars = totalStars,
}, [=] { withPaymentApproved(totalStars); });
return false;
}
state->submit = nullptr;
return true;
};
send(
ranges::views::all(
peers
@ -2410,11 +2531,11 @@ QPointer<Ui::BoxContent> ShowForwardMessagesBox(
not_null<PeerData*> peer) -> Controller::Chosen {
return peer->owner().history(peer);
}) | ranges::to_vector,
[](int messagesCount) { return true; },
checkPaid,
comment->entity()->getTextWithAppliedMarkdown(),
options,
state->box->forwardOptionsData());
if (successCallback) {
if (!state->submit && successCallback) {
successCallback();
}
};
@ -2483,7 +2604,12 @@ QPointer<Ui::BoxContent> ShowForwardMessagesBox(
state->menu.get(),
show,
SendMenu::Details{ sendMenuType() },
SendMenu::DefaultCallback(show, crl::guard(parent, submit)));
SendMenu::DefaultCallback(show, crl::guard(parent, [=](
Api::SendOptions options) {
if (const auto onstack = state->submit) {
onstack(options);
}
})));
if (showForwardOptions || !state->menu->empty()) {
state->menu->popup(QCursor::pos());
}
@ -2505,7 +2631,11 @@ QPointer<Ui::BoxContent> ShowForwardMessagesBox(
const auto field = comment->entity();
field->submits(
) | rpl::start_with_next([=] { submit({}); }, field->lifetime());
) | rpl::start_with_next([=] {
if (const auto onstack = state->submit) {
onstack({});
}
}, field->lifetime());
InitMessageFieldHandlers({
.session = session,
.show = show,
@ -2529,9 +2659,12 @@ QPointer<Ui::BoxContent> ShowForwardMessagesBox(
) | rpl::start_with_next([=](bool shown) {
state->box->clearButtons();
if (shown) {
const auto send = state->box->addButton(
tr::lng_send_button(),
[=] { submit({}); });
auto text = tr::lng_send_button();
const auto send = state->box->addButton(std::move(text), [=] {
if (const auto onstack = state->submit) {
onstack({});
}
});
send->setAcceptBoth();
send->clicks(
) | rpl::start_with_next([=](Qt::MouseButton button) {

View file

@ -15,6 +15,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
class History;
namespace Api {
struct SendOptions;
} // namespace Api
namespace Ui {
class RpWidget;
class BoxContent;
@ -146,7 +150,9 @@ object_ptr<Ui::BoxContent> PrepareChooseRecipientBox(
rpl::producer<QString> titleOverride = nullptr,
FnMut<void()> &&successCallback = nullptr,
InlineBots::PeerTypes typesRestriction = 0,
Fn<void(std::vector<not_null<Data::Thread*>>)> sendMany = nullptr);
Fn<void(
std::vector<not_null<Data::Thread*>>,
Api::SendOptions)> sendMany = nullptr);
QPointer<Ui::BoxContent> ShowChooseRecipientBox(
not_null<Window::SessionNavigation*> navigation,
FnMut<bool(not_null<Data::Thread*>)> &&chosen,