Support paid sending in ShareBox.

This commit is contained in:
John Preston 2025-02-21 21:29:10 +04:00
parent 928be4151b
commit 7b7e18e752
20 changed files with 468 additions and 193 deletions

View file

@ -2795,6 +2795,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_credits_small_balance_subscribe" = "Buy **Stars** and subscribe to **{channel}** and other channels.";
"lng_credits_small_balance_star_gift" = "Buy **Stars** to send gifts to {user} and other contacts.";
"lng_credits_small_balance_for_message" = "Buy **Stars** to send messages to {user}.";
"lng_credits_small_balance_for_messages" = "Buy **Stars** to send messages.";
"lng_credits_small_balance_fallback" = "Buy **Stars** to unlock content and services on Telegram.";
"lng_credits_purchase_blocked" = "Sorry, you can't purchase this item with Telegram Stars.";
"lng_credits_enough" = "You have enough stars at the moment. {link}";
@ -4834,6 +4835,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_payment_confirm_text#other" = "{name} charges **{count}** Stars per message.";
"lng_payment_confirm_amount#one" = "**{count}** Star";
"lng_payment_confirm_amount#other" = "**{count}** Stars";
"lng_payment_confirm_users#one" = "You selected **{count}** user who charge Stars for messages.";
"lng_payment_confirm_users#other" = "You selected **{count}** users who charge Stars for messages.";
"lng_payment_confirm_chats#one" = "You selected **{count}** chat where you pay Stars for messages.";
"lng_payment_confirm_chats#other" = "You selected **{count}** chats where you pay Stars for messages.";
"lng_payment_confirm_sure#one" = "Would you like to pay {amount} to send **{count}** message?";
"lng_payment_confirm_sure#other" = "Would you like to pay {amount} to send **{count}** messages?";
"lng_payment_confirm_dont_ask" = "Don't ask me again";

View file

@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_text_entities.h"
#include "apiwrap.h"
#include "base/random.h"
#include "data/data_channel.h"
#include "data/data_document.h"
#include "data/data_peer.h"
#include "data/data_peer_values.h"
@ -714,12 +715,18 @@ rpl::producer<rpl::no_value, QString> SponsoredToggle::setToggled(bool v) {
MessageMoneyRestriction ResolveMessageMoneyRestrictions(
not_null<PeerData*> peer,
History *maybeHistory) {
if (const auto channel = peer->asChannel()) {
return {
.starsPerMessage = channel->starsPerMessageChecked(),
.known = true,
};
}
const auto user = peer->asUser();
if (!user) {
return { .known = true };
} else if (user->messageMoneyRestrictionsKnown()) {
return {
.starsPerMessage = user->starsPerMessage(),
.starsPerMessage = user->starsPerMessageChecked(),
.premiumRequired = (user->requiresPremiumToWrite()
&& !user->session().premium()),
.known = true,

View file

@ -250,6 +250,14 @@ struct MessageMoneyRestriction {
int starsPerMessage = 0;
bool premiumRequired = false;
bool known = false;
explicit operator bool() const {
return starsPerMessage != 0 || premiumRequired;
}
friend inline bool operator==(
const MessageMoneyRestriction &,
const MessageMoneyRestriction &) = default;
};
[[nodiscard]] MessageMoneyRestriction ResolveMessageMoneyRestrictions(
not_null<PeerData*> peer,

View file

@ -46,7 +46,7 @@ constexpr auto kPremiumsRowId = PeerId(FakeChatId(BareId(1))).value;
constexpr auto kMiniAppsRowId = PeerId(FakeChatId(BareId(2))).value;
constexpr auto kGetPercent = 85;
constexpr auto kStarsMin = 1;
constexpr auto kStarsMax = 9000;
constexpr auto kStarsMax = 10000;
constexpr auto kDefaultChargeStars = 10;
using Exceptions = Api::UserPrivacy::Exceptions;

View file

@ -8,7 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/peer_list_controllers.h"
#include "api/api_chat_participants.h"
#include "api/api_premium.h"
#include "api/api_premium.h" // MessageMoneyRestriction.
#include "base/random.h"
#include "boxes/filters/edit_filter_chats_list.h"
#include "settings/settings_premium.h"
@ -41,6 +41,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history.h"
#include "history/history_item.h"
#include "dialogs/dialogs_main_list.h"
#include "payments/ui/payments_reaction_box.h"
#include "ui/effects/outline_segments.h"
#include "ui/wrap/slide_wrap.h"
#include "window/window_separate_id.h"
@ -275,28 +276,56 @@ bool PeerListGlobalSearchController::isLoading() {
return _timer.isActive() || _requestId;
}
struct RecipientRow::Restriction {
Api::MessageMoneyRestriction value;
RestrictionBadgeCache cache;
};
RecipientRow::RecipientRow(
not_null<PeerData*> peer,
const style::PeerListItem *maybeLockedSt,
History *maybeHistory)
: PeerListRow(peer)
, _maybeHistory(maybeHistory)
, _resolvePremiumRequired(maybeLockedSt != nullptr) {
if (maybeLockedSt
&& Api::ResolveMessageMoneyRestrictions(
, _maybeLockedSt(maybeLockedSt) {
if (_maybeLockedSt) {
setRestriction(Api::ResolveMessageMoneyRestrictions(
peer,
maybeHistory).premiumRequired) {
_lockedSt = maybeLockedSt;
maybeHistory));
}
}
Api::MessageMoneyRestriction RecipientRow::restriction() const {
return _restriction
? _restriction->value
: Api::MessageMoneyRestriction();
}
void RecipientRow::setRestriction(Api::MessageMoneyRestriction restriction) {
if (!restriction) {
_restriction = nullptr;
return;
} else if (!_restriction) {
_restriction = std::make_unique<Restriction>();
}
_restriction->value = restriction;
}
PaintRoundImageCallback RecipientRow::generatePaintUserpicCallback(
bool forceRound) {
auto result = PeerListRow::generatePaintUserpicCallback(forceRound);
if (const auto st = _lockedSt) {
if (const auto &r = _restriction) {
return [=](Painter &p, int x, int y, int outerWidth, int size) {
result(p, x, y, outerWidth, size);
PaintPremiumRequiredLock(p, st, x, y, outerWidth, size);
PaintRestrictionBadge(
p,
_maybeLockedSt,
r->value.starsPerMessage,
r->cache,
x,
y,
outerWidth,
size);
};
}
return result;
@ -305,12 +334,14 @@ PaintRoundImageCallback RecipientRow::generatePaintUserpicCallback(
bool RecipientRow::refreshLock(
not_null<const style::PeerListItem*> maybeLockedSt) {
if (const auto user = peer()->asUser()) {
const auto locked = _resolvePremiumRequired
&& Api::ResolveMessageMoneyRestrictions(
using Restriction = Api::MessageMoneyRestriction;
const auto r = _maybeLockedSt
? Api::ResolveMessageMoneyRestrictions(
user,
_maybeHistory).premiumRequired;
if (this->locked() != locked) {
setLocked(locked ? maybeLockedSt.get() : nullptr);
_maybeHistory)
: Restriction();
if ((_restriction ? _restriction->value : Restriction()) != r) {
setRestriction(r);
return true;
}
}
@ -320,14 +351,20 @@ bool RecipientRow::refreshLock(
void RecipientRow::preloadUserpic() {
PeerListRow::preloadUserpic();
if (!_resolvePremiumRequired) {
if (!_maybeLockedSt) {
return;
} else if (!Api::ResolveMessageMoneyRestrictions(
peer(),
_maybeHistory).known) {
const auto user = peer()->asUser();
user->session().api().premium().resolveMessageMoneyRestrictions(
user);
}
const auto peer = this->peer();
const auto known = Api::ResolveMessageMoneyRestrictions(
peer,
_maybeHistory).known;
if (known) {
return;
} else if (const auto user = peer->asUser()) {
const auto api = &user->session().api();
api->premium().resolveMessageMoneyRestrictions(user);
} else if (const auto group = peer->asChannel()) {
group->updateFull();
}
}
@ -844,7 +881,8 @@ bool RecipientRow::ShowLockedError(
not_null<PeerListController*> controller,
not_null<PeerListRow*> row,
Fn<RecipientMoneyRestrictionError(not_null<UserData*>)> error) {
if (!static_cast<RecipientRow*>(row.get())->locked()) {
const auto recipient = static_cast<RecipientRow*>(row.get());
if (!recipient->restriction().premiumRequired) {
return false;
}
::Settings::ShowPremiumPromoToast(
@ -1100,25 +1138,61 @@ auto ChooseTopicBoxController::createRow(not_null<Data::ForumTopic*> topic)
return skip ? nullptr : std::make_unique<Row>(topic);
};
void PaintPremiumRequiredLock(
void PaintRestrictionBadge(
Painter &p,
not_null<const style::PeerListItem*> st,
int stars,
RestrictionBadgeCache &cache,
int x,
int y,
int outerWidth,
int size) {
auto hq = PainterHighQualityEnabler(p);
const auto paletteVersion = style::PaletteVersion();
const auto good = !cache.badge.isNull()
&& (cache.stars == stars)
&& (cache.paletteVersion == paletteVersion);
const auto &check = st->checkbox.check;
auto pen = check.border->p;
pen.setWidthF(check.width);
p.setPen(pen);
p.setBrush(st::premiumButtonBg2);
const auto &icon = st::stickersPremiumLock;
const auto width = icon.width();
const auto height = icon.height();
const auto rect = QRect(
QPoint(x + size - width, y + size - height),
icon.size());
p.drawEllipse(rect);
icon.paintInCenter(p, rect);
const auto add = check.width;
if (!good) {
cache.stars = stars;
cache.paletteVersion = paletteVersion;
if (stars) {
const auto text = (stars >= 1000)
? (QString::number(stars / 1000) + 'K')
: QString::number(stars);
cache.badge = Ui::GenerateSmallBadgeImage(
text,
st::paidReactTopStarIcon,
check.bgActive->c,
st::premiumButtonFg->c,
&check);
} else {
auto hq = PainterHighQualityEnabler(p);
const auto &icon = st::stickersPremiumLock;
const auto width = icon.width();
const auto height = icon.height();
const auto rect = QRect(
QPoint(x + size - width, y + size - height),
icon.size());
const auto added = QMargins(add, add, add, add);
const auto ratio = style::DevicePixelRatio();
cache.badge = QImage(
(rect + added).size() * ratio,
QImage::Format_ARGB32_Premultiplied);
cache.badge.setDevicePixelRatio(ratio);
cache.badge.fill(Qt::transparent);
const auto inner = QRect(add, add, rect.width(), rect.height());
auto q = QPainter(&cache.badge);
auto pen = check.border->p;
pen.setWidthF(check.width);
q.setPen(pen);
q.setBrush(st::premiumButtonBg2);
q.drawEllipse(inner);
icon.paintInCenter(q, inner);
}
}
const auto cached = cache.badge.size() / cache.badge.devicePixelRatio();
const auto left = x + size + add - cached.width();
const auto top = stars ? (y - add) : (y + size + add - cached.height());
p.drawImage(left, top, cache.badge);
}

View file

@ -19,6 +19,10 @@ namespace style {
struct PeerListItem;
} // namespace style
namespace Api {
struct MessageMoneyRestriction;
} // namespace Api
namespace Data {
class Thread;
class Forum;
@ -100,6 +104,21 @@ struct RecipientMoneyRestrictionError {
[[nodiscard]] RecipientMoneyRestrictionError WriteMoneyRestrictionError(
not_null<UserData*> user);
struct RestrictionBadgeCache {
int paletteVersion = 0;
int stars = 0;
QImage badge;
};
void PaintRestrictionBadge(
Painter &p,
not_null<const style::PeerListItem*> st,
int stars,
RestrictionBadgeCache &cache,
int x,
int y,
int outerWidth,
int size);
class RecipientRow : public PeerListRow {
public:
explicit RecipientRow(
@ -117,21 +136,20 @@ public:
[[nodiscard]] History *maybeHistory() const {
return _maybeHistory;
}
[[nodiscard]] bool locked() const {
return _lockedSt != nullptr;
}
void setLocked(const style::PeerListItem *lockedSt) {
_lockedSt = lockedSt;
}
PaintRoundImageCallback generatePaintUserpicCallback(
bool forceRound) override;
void preloadUserpic() override;
[[nodiscard]] Api::MessageMoneyRestriction restriction() const;
void setRestriction(Api::MessageMoneyRestriction restriction);
private:
struct Restriction;
History *_maybeHistory = nullptr;
const style::PeerListItem *_lockedSt = nullptr;
bool _resolvePremiumRequired = false;
const style::PeerListItem *_maybeLockedSt = nullptr;
std::shared_ptr<Restriction> _restriction;
};
@ -371,11 +389,3 @@ private:
Fn<bool(not_null<Data::ForumTopic*>)> _filter;
};
void PaintPremiumRequiredLock(
Painter &p,
not_null<const style::PeerListItem*> st,
int x,
int y,
int outerWidth,
int size);

View file

@ -1486,6 +1486,7 @@ object_ptr<Ui::BoxContent> ShareInviteLinkBox(
};
auto submitCallback = [=](
std::vector<not_null<Data::Thread*>> &&result,
Fn<bool(int messages)> checkPaid,
TextWithTags &&comment,
Api::SendOptions options,
Data::ForwardOptions) {
@ -1503,6 +1504,8 @@ object_ptr<Ui::BoxContent> ShareInviteLinkBox(
result.size() > 1));
}
return;
} else if (!checkPaid(1)) {
return;
}
*sending = true;

View file

@ -118,12 +118,13 @@ private:
Ui::RoundImageCheckbox checkbox;
Ui::Text::String name;
Ui::Animations::Simple nameActive;
bool locked = false;
Api::MessageMoneyRestriction restriction;
RestrictionBadgeCache badgeCache;
};
void invalidateCache();
bool showLockedError(not_null<Chat*> chat);
void refreshLockedRows();
void refreshRestrictedRows();
[[nodiscard]] int displayedChatsCount() const;
[[nodiscard]] not_null<Data::Thread*> chatThread(
@ -132,7 +133,7 @@ private:
void paintChat(Painter &p, not_null<Chat*> chat, int index);
void updateChat(not_null<PeerData*> peer);
void updateChatName(not_null<Chat*> chat);
void initChatLocked(not_null<Chat*> chat);
void initChatRestriction(not_null<Chat*> chat);
void repaintChat(not_null<PeerData*> peer);
int chatIndex(not_null<PeerData*> peer) const;
void repaintChatAtIndex(int index);
@ -652,6 +653,67 @@ void ShareBox::innerSelectedChanged(
}
void ShareBox::submit(Api::SendOptions options) {
_submitLifetime.destroy();
auto threads = _inner->selected();
const auto weak = Ui::MakeWeak(this);
const auto checkPaid = [=](int messagesCount) {
const auto withPaymentApproved = crl::guard(weak, [=](int approved) {
auto copy = options;
copy.starsApproved = approved;
submit(copy);
});
const auto alreadyApproved = options.starsApproved;
auto paid = std::vector<not_null<Data::Thread*>>();
auto waiting = base::flat_set<not_null<PeerData*>>();
auto totalStars = 0;
for (const auto &thread : threads) {
const auto peer = thread->peer();
const auto details = ComputePaymentDetails(peer, messagesCount);
if (!details) {
waiting.emplace(peer);
} else if (details->stars > 0) {
totalStars += details->stars;
paid.push_back(thread);
}
}
if (!waiting.empty()) {
_descriptor.session->changes().peerUpdates(
Data::PeerUpdate::Flag::FullInfo
) | rpl::start_with_next([=](const Data::PeerUpdate &update) {
if (waiting.contains(update.peer)) {
withPaymentApproved(alreadyApproved);
}
}, _submitLifetime);
if (!_descriptor.session->credits().loaded()) {
_descriptor.session->credits().loadedValue(
) | rpl::filter(
rpl::mappers::_1
) | rpl::take(1) | rpl::start_with_next([=] {
withPaymentApproved(alreadyApproved);
}, _submitLifetime);
}
return false;
} else if (totalStars > alreadyApproved) {
const auto show = uiShow();
const auto session = _descriptor.session;
const auto sessionShow = Main::MakeSessionShow(show, session);
const auto scheduleBoxSt = _descriptor.st.scheduleBox.get();
ShowSendPaidConfirm(sessionShow, paid, SendPaymentDetails{
.messages = messagesCount,
.stars = totalStars,
}, [=] { withPaymentApproved(totalStars); }, PaidConfirmStyles{
.label = (scheduleBoxSt
? scheduleBoxSt->chooseDateTimeArgs.labelStyle
: nullptr),
.checkbox = _descriptor.st.checkbox,
});
return false;
}
return true;
};
if (const auto onstack = _descriptor.submitCallback) {
const auto forwardOptions = (_forwardOptions.captionsCount
&& _forwardOptions.dropCaptions)
@ -660,7 +722,8 @@ void ShareBox::submit(Api::SendOptions options) {
? Data::ForwardOptions::NoSenderNames
: Data::ForwardOptions::PreserveInfo;
onstack(
_inner->selected(),
std::move(threads),
checkPaid,
_comment->entity()->getTextWithAppliedMarkdown(),
options,
forwardOptions);
@ -727,7 +790,7 @@ ShareBox::Inner::Inner(
Data::AmPremiumValue(session) | rpl::to_empty,
session->api().premium().someMessageMoneyRestrictionsResolved()
) | rpl::start_with_next([=] {
refreshLockedRows();
refreshRestrictedRows();
}, lifetime());
}
@ -788,7 +851,7 @@ void ShareBox::Inner::invalidateCache() {
}
bool ShareBox::Inner::showLockedError(not_null<Chat*> chat) {
if (!chat->locked) {
if (!chat->restriction.premiumRequired) {
return false;
}
::Settings::ShowPremiumPromoToast(
@ -799,25 +862,25 @@ bool ShareBox::Inner::showLockedError(not_null<Chat*> chat) {
return true;
}
void ShareBox::Inner::refreshLockedRows() {
void ShareBox::Inner::refreshRestrictedRows() {
auto changed = false;
for (const auto &[peer, data] : _dataMap) {
const auto history = data->history;
const auto locked = Api::ResolveMessageMoneyRestrictions(
const auto restriction = Api::ResolveMessageMoneyRestrictions(
history->peer,
history).premiumRequired;
if (data->locked != locked) {
data->locked = locked;
history);
if (data->restriction != restriction) {
data->restriction = restriction;
changed = true;
}
}
for (const auto &data : d_byUsernameFiltered) {
const auto history = data->history;
const auto locked = Api::ResolveMessageMoneyRestrictions(
const auto restriction = Api::ResolveMessageMoneyRestrictions(
history->peer,
history).premiumRequired;
if (data->locked != locked) {
data->locked = locked;
history);
if (data->restriction != restriction) {
data->restriction = restriction;
changed = true;
}
}
@ -884,13 +947,14 @@ void ShareBox::Inner::updateChatName(not_null<Chat*> chat) {
chat->name.setText(_st.item.nameStyle, text, Ui::NameTextOptions());
}
void ShareBox::Inner::initChatLocked(not_null<Chat*> chat) {
void ShareBox::Inner::initChatRestriction(not_null<Chat*> chat) {
if (_descriptor.moneyRestrictionError) {
const auto history = chat->history;
if (Api::ResolveMessageMoneyRestrictions(
history->peer,
history).premiumRequired) {
chat->locked = true;
const auto restriction = Api::ResolveMessageMoneyRestrictions(
history->peer,
history);
if (restriction) {
chat->restriction = restriction;
}
}
}
@ -1021,9 +1085,10 @@ void ShareBox::Inner::preloadUserpic(not_null<Dialogs::Entry*> entry) {
} else if (!Api::ResolveMessageMoneyRestrictions(
history->peer,
history).known) {
const auto user = history->peer->asUser();
_descriptor.session->api().premium().resolveMessageMoneyRestrictions(
user);
if (const auto user = history->peer->asUser()) {
const auto api = &_descriptor.session->api();
api->premium().resolveMessageMoneyRestrictions(user);
}
}
}
@ -1046,7 +1111,7 @@ auto ShareBox::Inner::getChat(not_null<Dialogs::Row*> row)
repaintChat(peer);
}));
updateChatName(i->second.get());
initChatLocked(i->second.get());
initChatRestriction(i->second.get());
row->attached = i->second.get();
return i->second.get();
}
@ -1080,10 +1145,12 @@ void ShareBox::Inner::paintChat(
auto photoTop = st::sharePhotoTop;
chat->checkbox.paint(p, x + photoLeft, y + photoTop, outerWidth);
if (chat->locked) {
PaintPremiumRequiredLock(
if (chat->restriction) {
PaintRestrictionBadge(
p,
&_st.item,
chat->restriction.starsPerMessage,
chat->badgeCache,
x + photoLeft,
y + photoTop,
outerWidth,
@ -1438,7 +1505,7 @@ void ShareBox::Inner::peopleReceived(
_st.item,
[=] { repaintChat(peer); }));
updateChatName(d_byUsernameFiltered.back().get());
initChatLocked(d_byUsernameFiltered.back().get());
initChatRestriction(d_byUsernameFiltered.back().get());
}
}
};
@ -1502,24 +1569,29 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback(
const auto state = std::make_shared<State>();
return [=](
std::vector<not_null<Data::Thread*>> &&result,
TextWithTags &&comment,
Fn<bool(int messagesCount)> checkPaid,
TextWithTags comment,
Api::SendOptions options,
Data::ForwardOptions forwardOptions) {
if (!state->requests.empty()) {
return; // Share clicked already.
}
const auto items = history->owner().idsToItems(msgIds);
const auto existingIds = history->owner().itemsToIds(items);
if (existingIds.empty() || result.empty()) {
return;
}
auto messagesCount = int(items.size()) + (comment.empty() ? 0 : 1);
const auto error = GetErrorForSending(
result,
{ .forward = &items, .text = &comment });
if (error.error) {
show->showBox(MakeSendErrorBox(error, result.size() > 1));
return;
} else if (!checkPaid(messagesCount)) {
return;
}
using Flag = MTPmessages_ForwardMessages::Flag;
@ -1614,7 +1686,11 @@ ShareBox::SubmitCallback ShareBox::DefaultForwardCallback(
}
finish();
}).fail([=](const MTP::Error &error) {
if (error.type() == u"VOICE_MESSAGES_FORBIDDEN"_q) {
const auto type = error.type();
if (type.startsWith(u"ALLOW_PAYMENT_REQUIRED_"_q)) {
show->showToast(u"Payment requirements changed. "
"Please, try again."_q);
} else if (type == u"VOICE_MESSAGES_FORBIDDEN"_q) {
show->showToast(
tr::lng_restricted_send_voice_messages(
tr::now,
@ -1653,6 +1729,7 @@ ShareBoxStyleOverrides DarkShareBoxStyle() {
.comment = &st::groupCallShareBoxComment,
.peerList = &st::groupCallShareBoxList,
.label = &st::groupCallField,
.checkbox = &st::groupCallCheckbox,
.scheduleBox = std::make_shared<ScheduleBoxStyleArgs>(schedule()),
};
}
@ -1765,6 +1842,7 @@ void FastShareLink(
};
auto submitCallback = [=](
std::vector<not_null<::Data::Thread*>> &&result,
Fn<bool(int messages)> checkPaid,
TextWithTags &&comment,
Api::SendOptions options,
::Data::ForwardOptions) {
@ -1781,6 +1859,8 @@ void FastShareLink(
MakeSendErrorBox(error, result.size() > 1));
}
return;
} else if (!checkPaid(1)) {
return;
}
*sending = true;

View file

@ -66,6 +66,7 @@ struct ShareBoxStyleOverrides {
const style::InputField *comment = nullptr;
const style::PeerList *peerList = nullptr;
const style::InputField *label = nullptr;
const style::Checkbox *checkbox = nullptr;
std::shared_ptr<HistoryView::ScheduleBoxStyleArgs> scheduleBox;
};
[[nodiscard]] ShareBoxStyleOverrides DarkShareBoxStyle();
@ -96,6 +97,7 @@ public:
using CopyCallback = Fn<void()>;
using SubmitCallback = Fn<void(
std::vector<not_null<Data::Thread*>>&&,
Fn<bool(int messages)> checkPaid,
TextWithTags&&,
Api::SendOptions,
Data::ForwardOptions)>;
@ -196,5 +198,6 @@ private:
PeopleQueries _peopleQueries;
Ui::Animations::Simple _scrollAnimation;
rpl::lifetime _submitLifetime;
};

View file

@ -134,6 +134,7 @@ object_ptr<ShareBox> ShareInviteLinkBox(
};
auto submitCallback = [=](
std::vector<not_null<Data::Thread*>> &&result,
Fn<bool(int messages)> checkPaid,
TextWithTags &&comment,
Api::SendOptions options,
Data::ForwardOptions) {
@ -150,6 +151,8 @@ object_ptr<ShareBox> ShareInviteLinkBox(
MakeSendErrorBox(error, result.size() > 1));
}
return;
} else if (!checkPaid(1)) {
return;
}
*sending = true;

View file

@ -1747,7 +1747,8 @@ void InitFieldAutocomplete(
&& peer->isUser()
&& !peer->asUser()->isBot()
&& (!shortcutMessages
|| shortcutMessages->shortcuts().list.empty())) {
|| shortcutMessages->shortcuts().list.empty()
|| peer->starsPerMessageChecked() != 0)) {
parsed = {};
}
raw->showFiltered(peer, parsed.query, parsed.fromStart);

View file

@ -249,19 +249,42 @@ void ShowSendPaidConfirm(
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
SendPaymentDetails details,
Fn<void()> confirmed) {
Fn<void()> confirmed,
PaidConfirmStyles styles) {
return ShowSendPaidConfirm(
navigation->uiShow(),
peer,
details,
confirmed);
confirmed,
styles);
}
void ShowSendPaidConfirm(
std::shared_ptr<Main::SessionShow> show,
not_null<PeerData*> peer,
SendPaymentDetails details,
Fn<void()> confirmed) {
Fn<void()> confirmed,
PaidConfirmStyles styles) {
ShowSendPaidConfirm(
std::move(show),
std::vector<not_null<Data::Thread*>>{ peer->owner().history(peer) },
details,
confirmed,
styles);
}
void ShowSendPaidConfirm(
std::shared_ptr<Main::SessionShow> show,
const std::vector<not_null<Data::Thread*>> &threads,
SendPaymentDetails details,
Fn<void()> confirmed,
PaidConfirmStyles styles) {
Expects(!threads.empty());
const auto singlePeer = (threads.size() > 1)
? (PeerData*)nullptr
: threads.front()->peer().get();
const auto recipientId = singlePeer ? singlePeer->id : PeerId();
const auto check = [=] {
const auto required = details.stars;
if (!required) {
@ -276,57 +299,81 @@ void ShowSendPaidConfirm(
Settings::MaybeRequestBalanceIncrease(
show,
required,
Settings::SmallBalanceForMessage{ .recipientId = peer->id },
Settings::SmallBalanceForMessage{ .recipientId = recipientId },
done);
};
const auto session = &peer->session();
if (session->local().isPeerTrustedPayForMessage(peer->id)) {
check();
return;
auto usersOnly = true;
for (const auto &thread : threads) {
if (!thread->peer()->isUser()) {
usersOnly = false;
break;
}
}
if (singlePeer) {
const auto session = &singlePeer->session();
if (session->local().isPeerTrustedPayForMessage(recipientId)) {
check();
return;
}
}
const auto messages = details.messages;
const auto stars = details.stars;
show->showBox(Box([=](not_null<Ui::GenericBox*> box) {
const auto trust = std::make_shared<QPointer<Ui::Checkbox>>();
const auto proceed = [=](Fn<void()> close) {
if ((*trust)->checked()) {
session->local().markPeerTrustedPayForMessage(peer->id);
if (singlePeer && (*trust)->checked()) {
const auto session = &singlePeer->session();
session->local().markPeerTrustedPayForMessage(recipientId);
}
check();
close();
};
Ui::ConfirmBox(box, {
.text = tr::lng_payment_confirm_text(
tr::now,
lt_count,
stars / messages,
lt_name,
Ui::Text::Bold(peer->shortName()),
Ui::Text::RichLangValue).append(' ').append(
tr::lng_payment_confirm_sure(
.text = (singlePeer
? tr::lng_payment_confirm_text(
tr::now,
lt_count,
stars / messages,
lt_name,
Ui::Text::Bold(singlePeer->shortName()),
Ui::Text::RichLangValue)
: (usersOnly
? tr::lng_payment_confirm_users
: tr::lng_payment_confirm_chats)(
tr::now,
lt_count,
messages,
lt_amount,
tr::lng_payment_confirm_amount(
tr::now,
lt_count,
stars,
Ui::Text::RichLangValue),
Ui::Text::RichLangValue)),
int(threads.size()),
Ui::Text::RichLangValue)).append(' ').append(
tr::lng_payment_confirm_sure(
tr::now,
lt_count,
messages,
lt_amount,
tr::lng_payment_confirm_amount(
tr::now,
lt_count,
stars,
Ui::Text::RichLangValue),
Ui::Text::RichLangValue)),
.confirmed = proceed,
.confirmText = tr::lng_payment_confirm_button(
lt_count,
rpl::single(messages * 1.)),
.labelStyle = styles.label,
.title = tr::lng_payment_confirm_title(),
});
const auto skip = st::defaultCheckbox.margin.top();
*trust = box->addRow(
object_ptr<Ui::Checkbox>(
box,
tr::lng_payment_confirm_dont_ask(tr::now)),
st::boxRowPadding + QMargins(0, skip, 0, skip));
if (singlePeer) {
const auto skip = st::defaultCheckbox.margin.top();
*trust = box->addRow(
object_ptr<Ui::Checkbox>(
box,
tr::lng_payment_confirm_dont_ask(tr::now),
false,
(styles.checkbox
? *styles.checkbox
: st::defaultCheckbox)),
st::boxRowPadding + QMargins(0, skip, 0, skip));
}
}));
}

View file

@ -11,6 +11,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
class History;
namespace style {
struct FlatLabel;
struct Checkbox;
} // namespace style
namespace Api {
struct SendOptions;
struct SendAction;
@ -144,16 +149,28 @@ struct SendPaymentDetails {
not_null<PeerData*> peer,
int messagesCount);
struct PaidConfirmStyles {
const style::FlatLabel *label = nullptr;
const style::Checkbox *checkbox = nullptr;
};
void ShowSendPaidConfirm(
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
SendPaymentDetails details,
Fn<void()> confirmed);
Fn<void()> confirmed,
PaidConfirmStyles styles = {});
void ShowSendPaidConfirm(
std::shared_ptr<Main::SessionShow> show,
not_null<PeerData*> peer,
SendPaymentDetails details,
Fn<void()> confirmed);
Fn<void()> confirmed,
PaidConfirmStyles styles = {});
void ShowSendPaidConfirm(
std::shared_ptr<Main::SessionShow> show,
const std::vector<not_null<Data::Thread*>> &threads,
SendPaymentDetails details,
Fn<void()> confirmed,
PaidConfirmStyles styles = {});
class SendPaymentHelper final {
public:

View file

@ -4575,26 +4575,6 @@ void HistoryWidget::reportSelectedMessages() {
}
}
void HistoryWidget::payForMessageSure(bool trust) {
const auto required = _peer->starsPerMessage();
if (!required) {
return;
}
const auto done = [=](Settings::SmallBalanceResult result) {
if (result == Settings::SmallBalanceResult::Success
|| result == Settings::SmallBalanceResult::Already) {
if (canWriteMessage()) {
setInnerFocus();
}
}
};
Settings::MaybeRequestBalanceIncrease(
controller()->uiShow(),
required,
Settings::SmallBalanceForMessage{ .recipientId = _peer->id },
crl::guard(this, done));
}
History *HistoryWidget::history() const {
return _history;
}

View file

@ -426,7 +426,6 @@ private:
[[nodiscard]] int computeMaxFieldHeight() const;
void toggleMuteUnmute();
void reportSelectedMessages();
void payForMessageSure(bool trust = false);
void showKeyboardHideButton();
void toggleKeyboard(bool manual = true);
void startBotCommand();

View file

@ -78,6 +78,7 @@ namespace Media::Stories {
: Fn<void()>();
auto submitCallback = [=](
std::vector<not_null<Data::Thread*>> &&result,
Fn<bool(int messages)> checkPaid,
TextWithTags &&comment,
Api::SendOptions options,
Data::ForwardOptions forwardOptions) {
@ -95,6 +96,8 @@ namespace Media::Stories {
if (error.error) {
show->showBox(MakeSendErrorBox(error, result.size() > 1));
return;
} else if (!checkPaid(comment.text.isEmpty() ? 1 : 2)) {
return;
}
const auto api = &story->owner().session().api();
@ -133,7 +136,7 @@ namespace Media::Stories {
sendFlags |= SendFlag::f_invert_media;
}
const auto starsPaid = std::min(
peer->starsPerMessageChecked(),
threadHistory->peer->starsPerMessageChecked(),
options.starsApproved);
if (starsPaid) {
options.starsApproved -= starsPaid;

View file

@ -147,53 +147,11 @@ void PaidReactionSlider(
}
[[nodiscard]] QImage GenerateBadgeImage(int count) {
const auto text = Lang::FormatCountDecimal(count);
const auto length = st::chatSimilarBadgeFont->width(text);
const auto contents = st::chatSimilarLockedIconPosition.x()
+ st::paidReactTopStarIcon.width()
+ st::paidReactTopStarSkip
+ length;
const auto badge = QRect(
st::chatSimilarBadgePadding.left(),
st::chatSimilarBadgePadding.top(),
contents,
st::chatSimilarBadgeFont->height);
const auto rect = badge.marginsAdded(st::chatSimilarBadgePadding);
auto result = QImage(
rect.size() * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
result.setDevicePixelRatio(style::DevicePixelRatio());
result.fill(Qt::transparent);
auto q = QPainter(&result);
const auto &font = st::chatSimilarBadgeFont;
const auto textTop = badge.y() + font->ascent;
const auto icon = &st::paidReactTopStarIcon;
const auto position = st::chatSimilarLockedIconPosition;
auto hq = PainterHighQualityEnabler(q);
q.setBrush(st::creditsBg3);
q.setPen(Qt::NoPen);
const auto radius = rect.height() / 2.;
q.drawRoundedRect(rect, radius, radius);
auto textLeft = 0;
if (icon) {
icon->paint(
q,
badge.x() + position.x(),
badge.y() + position.y(),
rect.width());
textLeft += position.x() + icon->width() + st::paidReactTopStarSkip;
}
q.setFont(font);
q.setPen(st::premiumButtonFg);
q.drawText(textLeft, textTop, text);
q.end();
return result;
return GenerateSmallBadgeImage(
Lang::FormatCountDecimal(count),
st::paidReactTopStarIcon,
st::creditsBg3->c,
st::premiumButtonFg->c);
}
void AddArrowDown(not_null<RpWidget*> widget) {
@ -321,7 +279,6 @@ void SelectShownPeer(
updateUserpic();
}
(*menu)->popup(QCursor::pos());
}
void FillTopReactors(
@ -633,4 +590,65 @@ object_ptr<BoxContent> MakePaidReactionBox(PaidReactionBoxArgs &&args) {
return Box(PaidReactionsBox, std::move(args));
}
QImage GenerateSmallBadgeImage(
QString text,
const style::icon &icon,
QColor bg,
QColor fg,
const style::RoundCheckbox *borderSt) {
const auto length = st::chatSimilarBadgeFont->width(text);
const auto contents = st::chatSimilarLockedIconPosition.x()
+ icon.width()
+ st::paidReactTopStarSkip
+ length;
const auto badge = QRect(
st::chatSimilarBadgePadding.left(),
st::chatSimilarBadgePadding.top(),
contents,
st::chatSimilarBadgeFont->height);
const auto rect = badge.marginsAdded(st::chatSimilarBadgePadding);
const auto add = borderSt ? borderSt->width : 0;
const auto ratio = style::DevicePixelRatio();
auto result = QImage(
(rect + QMargins(add, add, add, add)).size() * ratio,
QImage::Format_ARGB32_Premultiplied);
result.setDevicePixelRatio(ratio);
result.fill(Qt::transparent);
auto q = QPainter(&result);
const auto &font = st::chatSimilarBadgeFont;
const auto textTop = badge.y() + font->ascent;
const auto position = st::chatSimilarLockedIconPosition;
auto hq = PainterHighQualityEnabler(q);
q.translate(add, add);
q.setBrush(bg);
if (borderSt) {
q.setPen(QPen(borderSt->border->c, borderSt->width));
} else {
q.setPen(Qt::NoPen);
}
const auto radius = rect.height() / 2.;
const auto shift = add / 2.;
q.drawRoundedRect(
QRectF(rect) + QMarginsF(shift, shift, shift, shift),
radius,
radius);
auto textLeft = 0;
icon.paint(
q,
badge.x() + position.x(),
badge.y() + position.y(),
rect.width());
textLeft += position.x() + icon.width() + st::paidReactTopStarSkip;
q.setFont(font);
q.setPen(fg);
q.drawText(textLeft, textTop, text);
q.end();
return result;
}
} // namespace Ui

View file

@ -9,6 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/object_ptr.h"
namespace style {
struct RoundCheckbox;
} // namespace style
namespace Ui {
class BoxContent;
@ -48,4 +52,11 @@ void PaidReactionsBox(
[[nodiscard]] object_ptr<BoxContent> MakePaidReactionBox(
PaidReactionBoxArgs &&args);
[[nodiscard]] QImage GenerateSmallBadgeImage(
QString text,
const style::icon &icon,
QColor bg,
QColor fg,
const style::RoundCheckbox *borderSt = nullptr);
} // namespace Ui

View file

@ -2091,7 +2091,9 @@ void SmallBalanceBox(
}, [&](SmallBalanceStarGift value) {
return owner->peer(value.recipientId)->shortName();
}, [&](SmallBalanceForMessage value) {
return owner->peer(value.recipientId)->shortName();
return value.recipientId
? owner->peer(value.recipientId)->shortName()
: QString();
});
auto needed = show->session().credits().balanceValue(
@ -2131,10 +2133,13 @@ void SmallBalanceBox(
rpl::single(Ui::Text::Bold(name)),
Ui::Text::RichLangValue)
: v::is<SmallBalanceForMessage>(source)
? tr::lng_credits_small_balance_for_message(
lt_user,
rpl::single(Ui::Text::Bold(name)),
Ui::Text::RichLangValue)
? (name.isEmpty()
? tr::lng_credits_small_balance_for_messages(
Ui::Text::RichLangValue)
: tr::lng_credits_small_balance_for_message(
lt_user,
rpl::single(Ui::Text::Bold(name)),
Ui::Text::RichLangValue))
: name.isEmpty()
? tr::lng_credits_small_balance_fallback(
Ui::Text::RichLangValue)

View file

@ -2409,6 +2409,7 @@ QPointer<Ui::BoxContent> ShowForwardMessagesBox(
not_null<PeerData*> peer) -> Controller::Chosen {
return peer->owner().history(peer);
}) | ranges::to_vector,
[](int messagesCount) { return true; },
comment->entity()->getTextWithAppliedMarkdown(),
options,
state->box->forwardOptionsData());