From 9032489786f7cb39b1ddbf31dcf5c558bc5dbb22 Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Fri, 28 Feb 2025 17:26:02 +0400 Subject: [PATCH] Add pays-me status bar in chat. --- .../chat_helpers/chat_helpers.style | 4 + Telegram/SourceFiles/data/data_changes.h | 31 ++-- Telegram/SourceFiles/data/data_peer.cpp | 34 +++- Telegram/SourceFiles/data/data_peer.h | 3 + .../SourceFiles/history/history_widget.cpp | 29 +++- Telegram/SourceFiles/history/history_widget.h | 2 + .../view/history_view_contact_status.cpp | 164 +++++++++++++++++- .../view/history_view_contact_status.h | 37 +++- 8 files changed, 284 insertions(+), 20 deletions(-) diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index d0d59bb5a..8d265ab8a 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -918,6 +918,10 @@ historyBusinessBotSettings: IconButton(defaultIconButton) { height: 58px; width: 48px; } +paysStatusLabel: FlatLabel(historyBusinessBotStatus) { + align: align(top); + minWidth: 240px; +} historyReplyCancelIcon: icon {{ "box_button_close", historyReplyCancelFg }}; historyReplyCancelIconOver: icon {{ "box_button_close", historyReplyCancelFgOver }}; diff --git a/Telegram/SourceFiles/data/data_changes.h b/Telegram/SourceFiles/data/data_changes.h index 9e19fc617..f44ae72bb 100644 --- a/Telegram/SourceFiles/data/data_changes.h +++ b/Telegram/SourceFiles/data/data_changes.h @@ -95,27 +95,28 @@ struct PeerUpdate { Birthday = (1ULL << 33), PersonalChannel = (1ULL << 34), StarRefProgram = (1ULL << 35), + PaysPerMessage = (1ULL << 36), // For chats and channels - InviteLinks = (1ULL << 36), - Members = (1ULL << 37), - Admins = (1ULL << 38), - BannedUsers = (1ULL << 39), - Rights = (1ULL << 40), - PendingRequests = (1ULL << 41), - Reactions = (1ULL << 42), + InviteLinks = (1ULL << 37), + Members = (1ULL << 38), + Admins = (1ULL << 39), + BannedUsers = (1ULL << 40), + Rights = (1ULL << 41), + PendingRequests = (1ULL << 42), + Reactions = (1ULL << 43), // For channels - ChannelAmIn = (1ULL << 43), - StickersSet = (1ULL << 44), - EmojiSet = (1ULL << 45), - ChannelLinkedChat = (1ULL << 46), - ChannelLocation = (1ULL << 47), - Slowmode = (1ULL << 48), - GroupCall = (1ULL << 49), + ChannelAmIn = (1ULL << 44), + StickersSet = (1ULL << 45), + EmojiSet = (1ULL << 46), + ChannelLinkedChat = (1ULL << 47), + ChannelLocation = (1ULL << 48), + Slowmode = (1ULL << 49), + GroupCall = (1ULL << 50), // For iteration - LastUsedBit = (1ULL << 49), + LastUsedBit = (1ULL << 50), }; using Flags = base::flags<Flag>; friend inline constexpr auto is_flag_type(Flag) { return true; } diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index ad9449593..988c12dd8 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -734,7 +734,7 @@ void PeerData::checkFolder(FolderId folderId) { void PeerData::clearBusinessBot() { if (const auto details = _barDetails.get()) { - if (details->requestChatDate) { + if (details->requestChatDate || details->paysPerMessage) { details->businessBot = nullptr; details->businessBotManageUrl = QString(); } else { @@ -777,7 +777,10 @@ void PeerData::saveTranslationDisabled(bool disabled) { void PeerData::setBarSettings(const MTPPeerSettings &data) { data.match([&](const MTPDpeerSettings &data) { - if (!data.vbusiness_bot_id() && !data.vrequest_chat_title()) { + const auto wasPaysPerMessage = paysPerMessage(); + if (!data.vbusiness_bot_id() + && !data.vrequest_chat_title() + && !data.vcharge_paid_message_stars()) { _barDetails = nullptr; } else if (!_barDetails) { _barDetails = std::make_unique<PeerBarDetails>(); @@ -792,6 +795,8 @@ void PeerData::setBarSettings(const MTPPeerSettings &data) { : nullptr; _barDetails->businessBotManageUrl = qs(data.vbusiness_bot_manage_url().value_or_empty()); + _barDetails->paysPerMessage + = data.vcharge_paid_message_stars().value_or_empty(); } using Flag = PeerBarSetting; setBarSettings((data.is_add_contact() ? Flag::AddContact : Flag()) @@ -815,8 +820,33 @@ void PeerData::setBarSettings(const MTPPeerSettings &data) { | (data.is_business_bot_can_reply() ? Flag::BusinessBotCanReply : Flag())); + if (wasPaysPerMessage != paysPerMessage()) { + session().changes().peerUpdated( + this, + UpdateFlag::PaysPerMessage); + } }); } + +int PeerData::paysPerMessage() const { + return _barDetails ? _barDetails->paysPerMessage : 0; +} + +void PeerData::clearPaysPerMessage() { + if (const auto details = _barDetails.get()) { + if (details->paysPerMessage) { + if (details->businessBot || details->requestChatDate) { + details->paysPerMessage = 0; + } else { + _barDetails = nullptr; + } + session().changes().peerUpdated( + this, + UpdateFlag::PaysPerMessage); + } + } +} + QString PeerData::requestChatTitle() const { return _barDetails ? _barDetails->requestChatTitle : QString(); } diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h index dcec75edf..d0b6d687a 100644 --- a/Telegram/SourceFiles/data/data_peer.h +++ b/Telegram/SourceFiles/data/data_peer.h @@ -177,6 +177,7 @@ struct PeerBarDetails { TimeId requestChatDate; UserData *businessBot = nullptr; QString businessBotManageUrl; + int paysPerMessage = 0; }; class PeerData { @@ -412,6 +413,8 @@ public: ? _barSettings.changes() : (_barSettings.value() | rpl::type_erased()); } + [[nodiscard]] int paysPerMessage() const; + void clearPaysPerMessage(); [[nodiscard]] QString requestChatTitle() const; [[nodiscard]] TimeId requestChatDate() const; [[nodiscard]] UserData *businessBot() const; diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index c862ccc5e..c7c55f463 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -1693,6 +1693,9 @@ void HistoryWidget::orderWidgets() { if (_contactStatus) { _contactStatus->bar().raise(); } + if (_paysStatus) { + _paysStatus->bar().raise(); + } if (_translateBar) { _translateBar->raise(); } @@ -2416,6 +2419,7 @@ void HistoryWidget::showHistory( _showAtMsgId = showAtMsgId; _showAtMsgParams = params; _historyInited = false; + _paysStatus = nullptr; _contactStatus = nullptr; _businessBotStatus = nullptr; @@ -2436,6 +2440,14 @@ void HistoryWidget::showHistory( refreshGiftToChannelShown(); if (const auto user = _peer->asUser()) { + _paysStatus = std::make_unique<PaysStatus>( + controller(), + this, + user); + _paysStatus->bar().heightValue( + ) | rpl::start_with_next([=] { + updateControlsGeometry(); + }, _paysStatus->bar().lifetime()); _businessBotStatus = std::make_unique<BusinessBotStatus>( controller(), this, @@ -3077,6 +3089,9 @@ void HistoryWidget::updateControlsVisibility() { if (_requestsBar) { _requestsBar->show(); } + if (_paysStatus) { + _paysStatus->show(); + } if (_contactStatus) { _contactStatus->show(); } @@ -4305,6 +4320,9 @@ void HistoryWidget::hideChildWidgets() { if (_chooseTheme) { _chooseTheme->hide(); } + if (_paysStatus) { + _paysStatus->hide(); + } if (_contactStatus) { _contactStatus->hide(); } @@ -6266,8 +6284,13 @@ void HistoryWidget::updateControlsGeometry() { _translateBar->move(0, translateTop); _translateBar->resizeToWidth(width()); } - const auto contactStatusTop = translateTop + const auto paysStatusTop = translateTop + (_translateBar ? _translateBar->height() : 0); + if (_paysStatus) { + _paysStatus->bar().move(0, paysStatusTop); + } + const auto contactStatusTop = paysStatusTop + + (_paysStatus ? _paysStatus->bar().height() : 0); if (_contactStatus) { _contactStatus->bar().move(0, contactStatusTop); } @@ -6518,6 +6541,9 @@ void HistoryWidget::updateHistoryGeometry( if (_requestsBar) { newScrollHeight -= _requestsBar->height(); } + if (_paysStatus) { + newScrollHeight -= _paysStatus->bar().height(); + } if (_contactStatus) { newScrollHeight -= _contactStatus->bar().height(); } @@ -6940,6 +6966,7 @@ void HistoryWidget::botCallbackSent(not_null<HistoryItem*> item) { int HistoryWidget::computeMaxFieldHeight() const { const auto available = height() - _topBar->height() + - (_paysStatus ? _paysStatus->bar().height() : 0) - (_contactStatus ? _contactStatus->bar().height() : 0) - (_businessBotStatus ? _businessBotStatus->bar().height() : 0) - (_sponsoredMessageBar ? _sponsoredMessageBar->height() : 0) diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index 2b956c2c1..e8dbd4f8c 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -100,6 +100,7 @@ namespace HistoryView { class StickerToast; class PaidReactionToast; class TopBarWidget; +class PaysStatus; class ContactStatus; class BusinessBotStatus; class Element; @@ -782,6 +783,7 @@ private: Webrtc::RecordAvailability _recordAvailability = {}; + std::unique_ptr<HistoryView::PaysStatus> _paysStatus; std::unique_ptr<HistoryView::ContactStatus> _contactStatus; std::unique_ptr<HistoryView::BusinessBotStatus> _businessBotStatus; diff --git a/Telegram/SourceFiles/history/view/history_view_contact_status.cpp b/Telegram/SourceFiles/history/view/history_view_contact_status.cpp index a25ebeed0..60e1b4e86 100644 --- a/Telegram/SourceFiles/history/view/history_view_contact_status.cpp +++ b/Telegram/SourceFiles/history/view/history_view_contact_status.cpp @@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/text_utilities.h" #include "ui/boxes/confirm_box.h" #include "ui/layers/generic_box.h" +#include "chat_helpers/message_field.h" // PaidSendButtonText #include "core/click_handler_types.h" #include "core/ui_integration.h" #include "data/business/data_business_chatbots.h" @@ -595,9 +596,9 @@ auto ContactStatus::PeerState(not_null<PeerData*> peer) return { .type = Type::RequestChatInfo, .requestChatName = peer->requestChatTitle(), + .requestDate = peer->requestChatDate(), .requestChatIsBroadcast = !!(settings.value & PeerBarSetting::RequestChatIsBroadcast), - .requestDate = peer->requestChatDate(), }; } else if (settings.value & PeerBarSetting::AutoArchived) { return { Type::UnarchiveOrBlock }; @@ -1131,4 +1132,165 @@ void TopicReopenBar::setupHandler() { }); } +class PaysStatus::Bar final : public Ui::RpWidget { +public: + Bar(QWidget *parent, not_null<PeerData*> peer); + + void showState(State state); + + [[nodiscard]] rpl::producer<> removeClicks() const; + +private: + void paintEvent(QPaintEvent *e) override; + int resizeGetHeight(int newWidth) override; + + not_null<PeerData*> _peer; + object_ptr<Ui::FlatLabel> _label; + object_ptr<Ui::LinkButton> _remove; + rpl::event_stream<> _removeClicks; + +}; + +PaysStatus::Bar::Bar(QWidget *parent, not_null<PeerData*> peer) +: RpWidget(parent) +, _peer(peer) +, _label(this, st::paysStatusLabel) +, _remove(this, tr::lng_payment_bar_button(tr::now)) { + _label->setAttribute(Qt::WA_TransparentForMouseEvents); +} + +void PaysStatus::Bar::showState(State state) { + _label->setMarkedText(tr::lng_payment_bar_text( + tr::now, + lt_name, + TextWithEntities{ _peer->shortName() }, + lt_cost, + PaidSendButtonText(tr::now, state.perMessage), + Ui::Text::WithEntities)); + resizeToWidth(width()); +} + +rpl::producer<> PaysStatus::Bar::removeClicks() const { + return _remove->clicks() | rpl::to_empty; +} + +void PaysStatus::Bar::paintEvent(QPaintEvent *e) { + QPainter p(this); + p.fillRect(e->rect(), st::historyContactStatusButton.bgColor); +} + +int PaysStatus::Bar::resizeGetHeight(int newWidth) { + const auto skip = st::defaultPeerListItem.photoPosition.y(); + _label->resizeToWidth(newWidth - skip); + _label->moveToLeft(skip, skip, newWidth); + _remove->move( + (newWidth - _remove->width()) / 2, + skip + _label->height() + skip); + return _remove->y() + _remove->height() + skip; +} + +PaysStatus::PaysStatus( + not_null<Window::SessionController*> window, + not_null<Ui::RpWidget*> parent, + not_null<UserData*> user) +: _controller(window) +, _user(user) +, _inner(Ui::CreateChild<Bar>(parent.get(), user)) +, _bar(parent, object_ptr<Bar>::fromRaw(_inner)) { + setupState(); + setupHandlers(); +} + +void PaysStatus::setupState() { + _user->session().api().requestPeerSettings(_user); + + _user->session().changes().peerFlagsValue( + _user, + Data::PeerUpdate::Flag::PaysPerMessage + ) | rpl::start_with_next([=] { + _state = State{ _user->paysPerMessage() }; + if (_state.perMessage > 0) { + _inner->showState(_state); + _bar.toggleContent(true); + } else { + _bar.toggleContent(false); + } + }, _bar.lifetime()); +} + +void PaysStatus::setupHandlers() { + _inner->removeClicks( + ) | rpl::start_with_next([=] { + const auto user = _user; + const auto exception = [=](bool refund) { + using Flag = MTPaccount_AddNoPaidMessagesException::Flag; + const auto api = &user->session().api(); + api->request(MTPaccount_AddNoPaidMessagesException( + MTP_flags(refund ? Flag::f_refund_charged : Flag()), + user->inputUser + )).done([=] { + user->clearPaysPerMessage(); + }).send(); + }; + _controller->show(Box([=](not_null<Ui::GenericBox*> box) { + const auto refund = std::make_shared<QPointer<Ui::Checkbox>>(); + Ui::ConfirmBox(box, { + .text = tr::lng_payment_refund_text( + tr::now, + lt_name, + Ui::Text::Bold(user->shortName()), + Ui::Text::WithEntities), + .confirmed = [=](Fn<void()> close) { + exception(*refund && (*refund)->checked()); + close(); + }, + .confirmText = tr::lng_payment_refund_confirm(tr::now), + .title = tr::lng_payment_refund_title(tr::now), + }); + const auto paid = box->lifetime().make_state< + rpl::variable<int> + >(); + *paid = _paidAlready.value(); + paid->value() | rpl::start_with_next([=](int already) { + if (!already) { + delete base::take(*refund); + } else if (!*refund) { + const auto skip = st::defaultCheckbox.margin.top(); + *refund = box->addRow( + object_ptr<Ui::Checkbox>( + box, + tr::lng_payment_refund_also( + lt_count, + paid->value() | tr::to_count()), + false, + st::defaultCheckbox), + st::boxRowPadding + QMargins(0, skip, 0, skip)); + } + }, box->lifetime()); + + user->session().api().request(MTPaccount_GetPaidMessagesRevenue( + user->inputUser + )).done(crl::guard(_inner, [=]( + const MTPaccount_PaidMessagesRevenue &result) { + _paidAlready = result.data().vstars_amount().v; + })).send(); + })); + }, _bar.lifetime()); +} + +void PaysStatus::show() { + if (!_shown) { + _shown = true; + if (_state.perMessage > 0) { + _inner->showState(_state); + _bar.toggleContent(true); + } + } + _bar.show(); +} + +void PaysStatus::hide() { + _bar.hide(); +} + } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_contact_status.h b/Telegram/SourceFiles/history/view/history_view_contact_status.h index aafc23a6a..56be475a8 100644 --- a/Telegram/SourceFiles/history/view/history_view_contact_status.h +++ b/Telegram/SourceFiles/history/view/history_view_contact_status.h @@ -95,9 +95,10 @@ private: RequestChatInfo, }; Type type = Type::None; + int starsPerMessage = 0; QString requestChatName; - bool requestChatIsBroadcast = false; TimeId requestDate = 0; + bool requestChatIsBroadcast = false; }; void setupState(not_null<PeerData*> peer, bool showInForum); @@ -181,4 +182,38 @@ private: }; +class PaysStatus final { +public: + PaysStatus( + not_null<Window::SessionController*> controller, + not_null<Ui::RpWidget*> parent, + not_null<UserData*> user); + + void show(); + void hide(); + + [[nodiscard]] SlidingBar &bar() { + return _bar; + } + +private: + class Bar; + + struct State { + int perMessage = 0; + }; + + void setupState(); + void setupHandlers(); + + const not_null<Window::SessionController*> _controller; + const not_null<UserData*> _user; + rpl::variable<int> _paidAlready; + State _state; + QPointer<Bar> _inner; + SlidingBar _bar; + bool _shown = false; + +}; + } // namespace HistoryView