diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index cb9eec67d5..ec5aa69c82 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -256,8 +256,6 @@ PRIVATE boxes/sessions_box.h boxes/share_box.cpp boxes/share_box.h - boxes/single_choice_box.cpp - boxes/single_choice_box.h boxes/sticker_set_box.cpp boxes/sticker_set_box.h boxes/stickers_box.cpp @@ -390,8 +388,6 @@ PRIVATE data/data_cloud_file.h data/data_cloud_themes.cpp data/data_cloud_themes.h - data/data_countries.cpp - data/data_countries.h data/data_document.cpp data/data_document.h data/data_document_media.cpp @@ -803,8 +799,6 @@ PRIVATE passport/passport_panel.h passport/passport_panel_controller.cpp passport/passport_panel_controller.h - passport/passport_panel_details_row.cpp - passport/passport_panel_details_row.h passport/passport_panel_edit_contact.cpp passport/passport_panel_edit_contact.h passport/passport_panel_edit_document.cpp diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp index 085bb89212..182ffcce22 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp @@ -11,7 +11,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "boxes/add_contact_box.h" #include "boxes/confirm_box.h" -#include "boxes/single_choice_box.h" #include "boxes/peer_list_controllers.h" #include "boxes/peers/edit_participants_box.h" #include "boxes/peers/edit_peer_type_box.h" @@ -20,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peers/edit_peer_invite_links.h" #include "boxes/peers/edit_linked_chat_box.h" #include "boxes/stickers_box.h" +#include "ui/boxes/single_choice_box.h" #include "chat_helpers/emoji_suggestions_widget.h" #include "core/application.h" #include "core/core_settings.h" diff --git a/Telegram/SourceFiles/calls/calls_group_settings.cpp b/Telegram/SourceFiles/calls/calls_group_settings.cpp index 42491c72df..4a21d30279 100644 --- a/Telegram/SourceFiles/calls/calls_group_settings.cpp +++ b/Telegram/SourceFiles/calls/calls_group_settings.cpp @@ -35,7 +35,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_group_call.h" #include "data/data_changes.h" #include "core/application.h" -#include "boxes/single_choice_box.h" +#include "ui/boxes/single_choice_box.h" #include "webrtc/webrtc_audio_input_tester.h" #include "webrtc/webrtc_media_devices.h" #include "settings/settings_common.h" diff --git a/Telegram/SourceFiles/export/data/export_data_types.cpp b/Telegram/SourceFiles/export/data/export_data_types.cpp index 2caf799352..8bc8950d90 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.cpp +++ b/Telegram/SourceFiles/export/data/export_data_types.cpp @@ -1820,7 +1820,7 @@ Utf8String FormatDateTime( ).toUtf8(); } -Utf8String FormatMoneyAmount(uint64 amount, const Utf8String ¤cy) { +Utf8String FormatMoneyAmount(int64 amount, const Utf8String ¤cy) { return Ui::FillAmountAndCurrency( amount, QString::fromUtf8(currency)).toUtf8(); diff --git a/Telegram/SourceFiles/export/data/export_data_types.h b/Telegram/SourceFiles/export/data/export_data_types.h index ff169de80f..694c941db7 100644 --- a/Telegram/SourceFiles/export/data/export_data_types.h +++ b/Telegram/SourceFiles/export/data/export_data_types.h @@ -660,7 +660,7 @@ Utf8String FormatDateTime( QChar dateSeparator = QChar('.'), QChar timeSeparator = QChar(':'), QChar separator = QChar(' ')); -Utf8String FormatMoneyAmount(uint64 amount, const Utf8String ¤cy); +Utf8String FormatMoneyAmount(int64 amount, const Utf8String ¤cy); Utf8String FormatFileSize(int64 size); Utf8String FormatDuration(int64 seconds); diff --git a/Telegram/SourceFiles/history/history_service.cpp b/Telegram/SourceFiles/history/history_service.cpp index 15bced85ce..91385fca19 100644 --- a/Telegram/SourceFiles/history/history_service.cpp +++ b/Telegram/SourceFiles/history/history_service.cpp @@ -958,7 +958,9 @@ void HistoryService::createFromMtp(const MTPDmessageService &message) { UpdateComponents(HistoryServicePayment::Bit()); const auto amount = data.vtotal_amount().v; const auto currency = qs(data.vcurrency()); - Get()->amount = Ui::FillAmountAndCurrency(amount, currency); + Get()->amount = Ui::FillAmountAndCurrency( + amount, + currency); } else if (message.vaction().type() == mtpc_messageActionGroupCall) { const auto &data = message.vaction().c_messageActionGroupCall(); if (data.vduration()) { diff --git a/Telegram/SourceFiles/intro/intro_phone.cpp b/Telegram/SourceFiles/intro/intro_phone.cpp index 48820b6999..3f61abb398 100644 --- a/Telegram/SourceFiles/intro/intro_phone.cpp +++ b/Telegram/SourceFiles/intro/intro_phone.cpp @@ -61,8 +61,8 @@ PhoneWidget::PhoneWidget( setErrorCentered(true); setupQrLogin(); - if (!_country->onChooseCountry(getData()->country)) { - _country->onChooseCountry(qsl("US")); + if (!_country->chooseCountry(getData()->country)) { + _country->chooseCountry(qsl("US")); } _changed = false; } @@ -251,7 +251,7 @@ QString PhoneWidget::fullNumber() const { } void PhoneWidget::selectCountry(const QString &country) { - _country->onChooseCountry(country); + _country->chooseCountry(country); } void PhoneWidget::setInnerFocus() { diff --git a/Telegram/SourceFiles/passport/passport_panel_controller.cpp b/Telegram/SourceFiles/passport/passport_panel_controller.cpp index a18be25408..6fe13111a5 100644 --- a/Telegram/SourceFiles/passport/passport_panel_controller.cpp +++ b/Telegram/SourceFiles/passport/passport_panel_controller.cpp @@ -9,10 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "passport/passport_panel_edit_document.h" -#include "passport/passport_panel_details_row.h" #include "passport/passport_panel_edit_contact.h" #include "passport/passport_panel_edit_scans.h" #include "passport/passport_panel.h" +#include "passport/ui/passport_details_row.h" #include "base/openssl_help.h" #include "base/unixtime.h" #include "boxes/passcode_box.h" @@ -212,7 +212,7 @@ EditDocumentScheme GetDocumentScheme( result.rows = { { ValueClass::Fields, - PanelDetailsType::Text, + Ui::PanelDetailsType::Text, qsl("first_name"), tr::lng_passport_first_name(tr::now), NameValidate, @@ -221,7 +221,7 @@ EditDocumentScheme GetDocumentScheme( }, { ValueClass::Fields, - PanelDetailsType::Text, + Ui::PanelDetailsType::Text, qsl("middle_name"), tr::lng_passport_middle_name(tr::now), NameOrEmptyValidate, @@ -231,7 +231,7 @@ EditDocumentScheme GetDocumentScheme( }, { ValueClass::Fields, - PanelDetailsType::Text, + Ui::PanelDetailsType::Text, qsl("last_name"), tr::lng_passport_last_name(tr::now), NameValidate, @@ -241,7 +241,7 @@ EditDocumentScheme GetDocumentScheme( }, { ValueClass::Fields, - PanelDetailsType::Date, + Ui::PanelDetailsType::Date, qsl("birth_date"), tr::lng_passport_birth_date(tr::now), DateValidate, @@ -249,7 +249,7 @@ EditDocumentScheme GetDocumentScheme( }, { ValueClass::Fields, - PanelDetailsType::Gender, + Ui::PanelDetailsType::Gender, qsl("gender"), tr::lng_passport_gender(tr::now), GenderValidate, @@ -257,7 +257,7 @@ EditDocumentScheme GetDocumentScheme( }, { ValueClass::Fields, - PanelDetailsType::Country, + Ui::PanelDetailsType::Country, qsl("country_code"), tr::lng_passport_country(tr::now), CountryValidate, @@ -265,7 +265,7 @@ EditDocumentScheme GetDocumentScheme( }, { ValueClass::Fields, - PanelDetailsType::Country, + Ui::PanelDetailsType::Country, qsl("residence_country_code"), tr::lng_passport_residence_country(tr::now), CountryValidate, @@ -273,7 +273,7 @@ EditDocumentScheme GetDocumentScheme( }, { ValueClass::Scans, - PanelDetailsType::Text, + Ui::PanelDetailsType::Text, qsl("document_no"), tr::lng_passport_document_number(tr::now), DocumentValidate, @@ -282,7 +282,7 @@ EditDocumentScheme GetDocumentScheme( }, { ValueClass::Scans, - PanelDetailsType::Date, + Ui::PanelDetailsType::Date, qsl("expiry_date"), tr::lng_passport_expiry_date(tr::now), DateOrEmptyValidate, @@ -344,7 +344,7 @@ EditDocumentScheme GetDocumentScheme( auto additional = std::initializer_list{ { ValueClass::Additional, - PanelDetailsType::Text, + Ui::PanelDetailsType::Text, qsl("first_name_native"), tr::lng_passport_first_name(tr::now), NativeNameValidate, @@ -355,7 +355,7 @@ EditDocumentScheme GetDocumentScheme( }, { ValueClass::Additional, - PanelDetailsType::Text, + Ui::PanelDetailsType::Text, qsl("middle_name_native"), tr::lng_passport_middle_name(tr::now), NativeNameOrEmptyValidate, @@ -366,7 +366,7 @@ EditDocumentScheme GetDocumentScheme( }, { ValueClass::Additional, - PanelDetailsType::Text, + Ui::PanelDetailsType::Text, qsl("last_name_native"), tr::lng_passport_last_name(tr::now), NativeNameValidate, @@ -411,7 +411,7 @@ EditDocumentScheme GetDocumentScheme( result.rows = { { ValueClass::Fields, - PanelDetailsType::Text, + Ui::PanelDetailsType::Text, qsl("street_line1"), tr::lng_passport_street(tr::now), StreetValidate, @@ -420,7 +420,7 @@ EditDocumentScheme GetDocumentScheme( }, { ValueClass::Fields, - PanelDetailsType::Text, + Ui::PanelDetailsType::Text, qsl("street_line2"), tr::lng_passport_street(tr::now), DontValidate, @@ -429,7 +429,7 @@ EditDocumentScheme GetDocumentScheme( }, { ValueClass::Fields, - PanelDetailsType::Text, + Ui::PanelDetailsType::Text, qsl("city"), tr::lng_passport_city(tr::now), CityValidate, @@ -438,7 +438,7 @@ EditDocumentScheme GetDocumentScheme( }, { ValueClass::Fields, - PanelDetailsType::Text, + Ui::PanelDetailsType::Text, qsl("state"), tr::lng_passport_state(tr::now), DontValidate, @@ -447,7 +447,7 @@ EditDocumentScheme GetDocumentScheme( }, { ValueClass::Fields, - PanelDetailsType::Country, + Ui::PanelDetailsType::Country, qsl("country_code"), tr::lng_passport_country(tr::now), CountryValidate, @@ -455,7 +455,7 @@ EditDocumentScheme GetDocumentScheme( }, { ValueClass::Fields, - PanelDetailsType::Postcode, + Ui::PanelDetailsType::Postcode, qsl("post_code"), tr::lng_passport_postcode(tr::now), PostcodeValidate, diff --git a/Telegram/SourceFiles/passport/passport_panel_edit_contact.cpp b/Telegram/SourceFiles/passport/passport_panel_edit_contact.cpp index 578075696c..3b9986aded 100644 --- a/Telegram/SourceFiles/passport/passport_panel_edit_contact.cpp +++ b/Telegram/SourceFiles/passport/passport_panel_edit_contact.cpp @@ -8,7 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "passport/passport_panel_edit_contact.h" #include "passport/passport_panel_controller.h" -#include "passport/passport_panel_details_row.h" +#include "passport/ui/passport_details_row.h" #include "ui/widgets/input_fields.h" #include "ui/widgets/labels.h" #include "ui/widgets/buttons.h" diff --git a/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp b/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp index fc018a42c0..506f07fe87 100644 --- a/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp +++ b/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp @@ -8,8 +8,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "passport/passport_panel_edit_document.h" #include "passport/passport_panel_controller.h" -#include "passport/passport_panel_details_row.h" #include "passport/passport_panel_edit_scans.h" +#include "passport/ui/passport_details_row.h" #include "ui/widgets/input_fields.h" #include "ui/widgets/scroll_area.h" #include "ui/widgets/labels.h" @@ -19,6 +19,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/wrap/vertical_layout.h" #include "ui/wrap/fade_wrap.h" #include "ui/wrap/slide_wrap.h" +#include "data/data_countries.h" +#include "data/data_user.h" // ->bot()->session() +#include "main/main_session.h" // ->session().user() #include "ui/text/text_utilities.h" // Ui::Text::ToUpper #include "boxes/abstract_box.h" #include "boxes/confirm_box.h" @@ -363,7 +366,7 @@ not_null PanelEditDocument::setupContent( const ValueMap &fields) { accumulate_max( maxLabelWidth, - PanelDetailsRow::LabelWidth(row.label)); + Ui::PanelDetailsRow::LabelWidth(row.label)); }); if (maxLabelWidth > 0) { if (error && !error->isEmpty()) { @@ -513,12 +516,20 @@ void PanelEditDocument::createDetailsRow( }; const auto current = valueOrEmpty(fields, row.key); + const auto showBox = [controller = _controller]( + object_ptr box) { + controller->show(std::move(box)); + }; + const auto isoByPhone = Data::CountryISO2ByPhone( + _controller->bot()->session().user()->phone()); + const auto [it, ok] = _details.emplace( i, - container->add(PanelDetailsRow::Create( + container->add(Ui::PanelDetailsRow::Create( container, + showBox, + isoByPhone, row.inputType, - _controller, row.label, maxLabelWidth, current.text, @@ -537,7 +548,7 @@ void PanelEditDocument::createDetailsRow( }, it->second->lifetime()); } -not_null PanelEditDocument::findRow( +not_null PanelEditDocument::findRow( const QString &key) const { for (auto i = 0, count = int(_scheme.rows.size()); i != count; ++i) { const auto &row = _scheme.rows[i]; @@ -636,7 +647,7 @@ bool PanelEditDocument::validate() { _scroll->scrollToY(_scroll->scrollTop() + scrolldelta); error = firsttop.y(); } - auto first = QPointer(); + auto first = QPointer(); for (const auto &[i, field] : ranges::views::reverse(_details)) { const auto &row = _scheme.rows[i]; if (row.valueClass == Scheme::ValueClass::Additional diff --git a/Telegram/SourceFiles/passport/passport_panel_edit_document.h b/Telegram/SourceFiles/passport/passport_panel_edit_document.h index bf9dd9f96a..aa13908cef 100644 --- a/Telegram/SourceFiles/passport/passport_panel_edit_document.h +++ b/Telegram/SourceFiles/passport/passport_panel_edit_document.h @@ -24,15 +24,19 @@ template class SlideWrap; } // namespace Ui +namespace Passport::Ui { +using namespace ::Ui; +enum class PanelDetailsType; +class PanelDetailsRow; +} // namespace Passport::Ui + namespace Passport { class PanelController; struct ValueMap; struct ScanInfo; class EditScans; -class PanelDetailsRow; enum class FileType; -enum class PanelDetailsType; struct ScanListData; struct EditDocumentScheme { @@ -50,7 +54,7 @@ struct EditDocumentScheme { using Validator = Fn(const QString &value)>; using Formatter = Fn; ValueClass valueClass = ValueClass::Fields; - PanelDetailsType inputType = PanelDetailsType(); + Ui::PanelDetailsType inputType = Ui::PanelDetailsType(); QString key; QString label; Validator error; @@ -140,7 +144,7 @@ private: const Scheme::Row &row, const ValueMap &fields, int maxLabelWidth); - not_null findRow(const QString &key) const; + not_null findRow(const QString &key) const; not_null _controller; Scheme _scheme; @@ -151,7 +155,7 @@ private: QPointer _editScans; QPointer> _commonError; - std::map> _details; + std::map> _details; bool _fieldsChanged = false; bool _additionalShown = false; diff --git a/Telegram/SourceFiles/passport/passport_panel_edit_scans.cpp b/Telegram/SourceFiles/passport/passport_panel_edit_scans.cpp index 9ef957d15a..88c668408a 100644 --- a/Telegram/SourceFiles/passport/passport_panel_edit_scans.cpp +++ b/Telegram/SourceFiles/passport/passport_panel_edit_scans.cpp @@ -8,7 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "passport/passport_panel_edit_scans.h" #include "passport/passport_panel_controller.h" -#include "passport/passport_panel_details_row.h" +#include "passport/ui/passport_details_row.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" #include "ui/widgets/box_content_divider.h" diff --git a/Telegram/SourceFiles/passport/passport_panel_form.cpp b/Telegram/SourceFiles/passport/passport_panel_form.cpp index 75eeba4c3c..5802e6c3ae 100644 --- a/Telegram/SourceFiles/passport/passport_panel_form.cpp +++ b/Telegram/SourceFiles/passport/passport_panel_form.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "passport/passport_panel_form.h" #include "passport/passport_panel_controller.h" +#include "passport/ui/passport_form_row.h" #include "lang/lang_keys.h" #include "boxes/abstract_box.h" #include "core/click_handler_types.h" @@ -30,145 +31,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Passport { -class PanelForm::Row : public Ui::RippleButton { -public: - explicit Row(QWidget *parent); - - void updateContent( - const QString &title, - const QString &description, - bool ready, - bool error, - anim::type animated); - -protected: - int resizeGetHeight(int newWidth) override; - - void paintEvent(QPaintEvent *e) override; - -private: - int countAvailableWidth() const; - int countAvailableWidth(int newWidth) const; - - Ui::Text::String _title; - Ui::Text::String _description; - int _titleHeight = 0; - int _descriptionHeight = 0; - bool _ready = false; - bool _error = false; - Ui::Animations::Simple _errorAnimation; - -}; - -PanelForm::Row::Row(QWidget *parent) -: RippleButton(parent, st::passportRowRipple) -, _title(st::boxWideWidth / 2) -, _description(st::boxWideWidth / 2) { -} - -void PanelForm::Row::updateContent( - const QString &title, - const QString &description, - bool ready, - bool error, - anim::type animated) { - _title.setText( - st::semiboldTextStyle, - title, - Ui::NameTextOptions()); - _description.setText( - st::defaultTextStyle, - description, - TextParseOptions { - TextParseMultiline, - 0, - 0, - Qt::LayoutDirectionAuto - }); - _ready = ready && !error; - if (_error != error) { - _error = error; - if (animated == anim::type::instant) { - _errorAnimation.stop(); - } else { - _errorAnimation.start( - [=] { update(); }, - _error ? 0. : 1., - _error ? 1. : 0., - st::fadeWrapDuration); - } - } - resizeToWidth(width()); - update(); -} - -int PanelForm::Row::resizeGetHeight(int newWidth) { - const auto availableWidth = countAvailableWidth(newWidth); - _titleHeight = _title.countHeight(availableWidth); - _descriptionHeight = _description.countHeight(availableWidth); - const auto result = st::passportRowPadding.top() - + _titleHeight - + st::passportRowSkip - + _descriptionHeight - + st::passportRowPadding.bottom(); - return result; -} - -int PanelForm::Row::countAvailableWidth(int newWidth) const { - return newWidth - - st::passportRowPadding.left() - - st::passportRowPadding.right() - - (_ready - ? st::passportRowReadyIcon - : st::passportRowEmptyIcon).width() - - st::passportRowIconSkip; -} - -int PanelForm::Row::countAvailableWidth() const { - return countAvailableWidth(width()); -} - -void PanelForm::Row::paintEvent(QPaintEvent *e) { - Painter p(this); - - paintRipple(p, 0, 0); - - const auto left = st::passportRowPadding.left(); - const auto availableWidth = countAvailableWidth(); - auto top = st::passportRowPadding.top(); - - const auto error = _errorAnimation.value(_error ? 1. : 0.); - - p.setPen(st::passportRowTitleFg); - _title.drawLeft(p, left, top, availableWidth, width()); - top += _titleHeight + st::passportRowSkip; - - p.setPen(anim::pen( - st::passportRowDescriptionFg, - st::boxTextFgError, - error)); - _description.drawLeft(p, left, top, availableWidth, width()); - top += _descriptionHeight + st::passportRowPadding.bottom(); - - const auto &icon = _ready - ? st::passportRowReadyIcon - : st::passportRowEmptyIcon; - if (error > 0. && !_ready) { - icon.paint( - p, - width() - st::passportRowPadding.right() - icon.width(), - (height() - icon.height()) / 2, - width(), - anim::color(st::menuIconFgOver, st::boxTextFgError, error)); - } else { - icon.paint( - p, - width() - st::passportRowPadding.right() - icon.width(), - (height() - icon.height()) / 2, - width()); - } -} - PanelForm::PanelForm( QWidget *parent, not_null controller) diff --git a/Telegram/SourceFiles/passport/passport_panel_form.h b/Telegram/SourceFiles/passport/passport_panel_form.h index badc773f0f..26b2dfa3b4 100644 --- a/Telegram/SourceFiles/passport/passport_panel_form.h +++ b/Telegram/SourceFiles/passport/passport_panel_form.h @@ -19,6 +19,11 @@ class FlatLabel; class UserpicButton; } // namespace Ui +namespace Passport::Ui { +using namespace ::Ui; +class FormRow; +} // namespace Passport::Ui + namespace Passport { class PanelController; @@ -33,7 +38,7 @@ protected: void resizeEvent(QResizeEvent *e) override; private: - class Row; + using Row = Ui::FormRow; void setupControls(); not_null setupContent(); diff --git a/Telegram/SourceFiles/passport/passport_panel_details_row.cpp b/Telegram/SourceFiles/passport/ui/passport_details_row.cpp similarity index 90% rename from Telegram/SourceFiles/passport/passport_panel_details_row.cpp rename to Telegram/SourceFiles/passport/ui/passport_details_row.cpp index 20b7c9732a..a93c30ebd6 100644 --- a/Telegram/SourceFiles/passport/passport_panel_details_row.cpp +++ b/Telegram/SourceFiles/passport/ui/passport_details_row.cpp @@ -5,9 +5,8 @@ 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 "passport/passport_panel_details_row.h" +#include "passport/ui/passport_details_row.h" -#include "passport/passport_panel_controller.h" #include "lang/lang_keys.h" #include "base/platform/base_platform_info.h" #include "ui/widgets/input_fields.h" @@ -15,17 +14,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/buttons.h" #include "ui/widgets/checkbox.h" #include "ui/wrap/slide_wrap.h" -#include "ui/countryinput.h" -#include "main/main_session.h" -#include "data/data_user.h" +#include "ui/layers/box_content.h" +#include "ui/boxes/country_select_box.h" #include "data/data_countries.h" #include "styles/style_layers.h" #include "styles/style_passport.h" -namespace Passport { +#include + +namespace Passport::Ui { namespace { -class PostcodeInput : public Ui::MaskedInputField { +class PostcodeInput : public MaskedInputField { public: PostcodeInput( QWidget *parent, @@ -103,7 +103,8 @@ class CountryRow : public PanelDetailsRow { public: CountryRow( QWidget *parent, - not_null controller, + Fn)> showBox, + const QString &defaultCountry, const QString &label, int maxLabelWidth, const QString &value); @@ -121,15 +122,16 @@ private: void toggleError(bool shown); void errorAnimationCallback(); - not_null _controller; - object_ptr _link; + QString _defaultCountry; + Fn)> _showBox; + object_ptr _link; rpl::variable _value; bool _errorShown = false; - Ui::Animations::Simple _errorAnimation; + Animations::Simple _errorAnimation; }; -class DateInput final : public Ui::MaskedInputField { +class DateInput final : public MaskedInputField { public: using MaskedInputField::MaskedInputField; @@ -191,21 +193,21 @@ private: int number(const object_ptr &field) const; object_ptr _day; - object_ptr> _separator1; + object_ptr> _separator1; object_ptr _month; - object_ptr> _separator2; + object_ptr> _separator2; object_ptr _year; rpl::variable _value; style::cursor _cursor = style::cur_default; - Ui::Animations::Simple _a_borderShown; + Animations::Simple _a_borderShown; int _borderAnimationStart = 0; - Ui::Animations::Simple _a_borderOpacity; + Animations::Simple _a_borderOpacity; bool _borderVisible = false; - Ui::Animations::Simple _a_error; + Animations::Simple _a_error; bool _error = false; - Ui::Animations::Simple _a_focused; + Animations::Simple _a_focused; bool _focused = false; }; @@ -238,18 +240,18 @@ private: void hideGenderError(); void errorAnimationCallback(); - std::unique_ptr createRadioView( - Ui::RadioView* &weak) const; + std::unique_ptr createRadioView( + RadioView* &weak) const; - std::shared_ptr> _group; - Ui::RadioView *_maleRadio = nullptr; - Ui::RadioView *_femaleRadio = nullptr; - object_ptr> _male; - object_ptr> _female; + std::shared_ptr> _group; + RadioView *_maleRadio = nullptr; + RadioView *_femaleRadio = nullptr; + object_ptr> _male; + object_ptr> _female; rpl::variable _value; bool _errorShown = false; - Ui::Animations::Simple _errorAnimation; + Animations::Simple _errorAnimation; }; @@ -308,12 +310,14 @@ QString CountryString(const QString &code) { CountryRow::CountryRow( QWidget *parent, - not_null controller, + Fn)> showBox, + const QString &defaultCountry, const QString &label, int maxLabelWidth, const QString &value) : PanelDetailsRow(parent, label, maxLabelWidth) -, _controller(controller) +, _defaultCountry(defaultCountry) +, _showBox(std::move(showBox)) , _link(this, CountryString(value), st::boxLinkButton) , _value(value) { _value.changes( @@ -380,20 +384,23 @@ void CountryRow::errorAnimationCallback() { void CountryRow::chooseCountry() { const auto top = _value.current(); const auto name = Data::CountryNameByISO2(top); - const auto isoByPhone = Data::CountryISO2ByPhone( - _controller->bot()->session().user()->phone()); - const auto box = _controller->show(Box(!name.isEmpty() + const auto country = !name.isEmpty() ? top - : !isoByPhone.isEmpty() - ? isoByPhone - : Platform::SystemCountry(), - CountrySelectBox::Type::Countries)); - connect(box, &CountrySelectBox::countryChosen, this, [=](QString iso) { + : !_defaultCountry.isEmpty() + ? _defaultCountry + : Platform::SystemCountry(); + auto box = Box( + country, + CountrySelectBox::Type::Countries); + const auto raw = box.data(); + raw->countryChosen( + ) | rpl::start_with_next([=](QString iso) { _value = iso; _link->setText(CountryString(iso)); hideCountryError(); - box->closeBox(); - }); + raw->closeBox(); + }, lifetime()); + _showBox(std::move(box)); } QDate ValidateDate(const QString &value) { @@ -528,7 +535,7 @@ DateRow::DateRow( GetDay(value)) , _separator1( this, - object_ptr( + object_ptr( this, QString(" / "), st::passportDetailsSeparator), @@ -540,7 +547,7 @@ DateRow::DateRow( GetMonth(value)) , _separator2( this, - object_ptr( + object_ptr( this, QString(" / "), st::passportDetailsSeparator), @@ -552,7 +559,7 @@ DateRow::DateRow( GetYear(value)) , _value(valueCurrent()) { const auto focused = [=](const object_ptr &field) { - return [this, pointer = Ui::MakeWeak(field.data())]{ + return [this, pointer = MakeWeak(field.data())]{ _borderAnimationStart = pointer->borderAnimationStart() + pointer->x() - _day->x(); @@ -565,15 +572,15 @@ DateRow::DateRow( const auto changed = [=] { _value = valueCurrent(); }; - connect(_day, &Ui::MaskedInputField::focused, focused(_day)); - connect(_month, &Ui::MaskedInputField::focused, focused(_month)); - connect(_year, &Ui::MaskedInputField::focused, focused(_year)); - connect(_day, &Ui::MaskedInputField::blurred, blurred); - connect(_month, &Ui::MaskedInputField::blurred, blurred); - connect(_year, &Ui::MaskedInputField::blurred, blurred); - connect(_day, &Ui::MaskedInputField::changed, changed); - connect(_month, &Ui::MaskedInputField::changed, changed); - connect(_year, &Ui::MaskedInputField::changed, changed); + connect(_day, &MaskedInputField::focused, focused(_day)); + connect(_month, &MaskedInputField::focused, focused(_month)); + connect(_year, &MaskedInputField::focused, focused(_year)); + connect(_day, &MaskedInputField::blurred, blurred); + connect(_month, &MaskedInputField::blurred, blurred); + connect(_year, &MaskedInputField::blurred, blurred); + connect(_day, &MaskedInputField::changed, changed); + connect(_month, &MaskedInputField::changed, changed); + connect(_year, &MaskedInputField::changed, changed); _day->setMaxValue(31); _day->putNext() | rpl::start_with_next([=](QChar ch) { putNext(_month, ch); @@ -845,8 +852,8 @@ GenderRow::GenderRow( const QString &value) : PanelDetailsRow(parent, label, maxLabelWidth) , _group(StringToGender(value).has_value() - ? std::make_shared>(*StringToGender(value)) - : std::make_shared>()) + ? std::make_shared>(*StringToGender(value)) + : std::make_shared>()) , _male( this, _group, @@ -868,9 +875,9 @@ GenderRow::GenderRow( }); } -std::unique_ptr GenderRow::createRadioView( - Ui::RadioView* &weak) const { - auto result = std::make_unique(st::defaultRadio, false); +std::unique_ptr GenderRow::createRadioView( + RadioView* &weak) const { + auto result = std::make_unique(st::defaultRadio, false); weak = result.get(); return result; } @@ -959,8 +966,9 @@ PanelDetailsRow::PanelDetailsRow( object_ptr PanelDetailsRow::Create( QWidget *parent, + Fn)> showBox, + const QString &defaultCountry, Type type, - not_null controller, const QString &label, int maxLabelWidth, const QString &value, @@ -969,7 +977,7 @@ object_ptr PanelDetailsRow::Create( auto result = [&]() -> object_ptr { switch (type) { case Type::Text: - return object_ptr>( + return object_ptr>( parent, label, maxLabelWidth, @@ -985,7 +993,8 @@ object_ptr PanelDetailsRow::Create( case Type::Country: return object_ptr( parent, - controller, + showBox, + defaultCountry, label, maxLabelWidth, value); @@ -1062,7 +1071,7 @@ void PanelDetailsRow::showError(std::optional error) { if (!_error) { _error.create( this, - object_ptr( + object_ptr( this, *error, st::passportVerifyErrorLabel)); @@ -1122,4 +1131,4 @@ void PanelDetailsRow::paintEvent(QPaintEvent *e) { p.drawTextLeft(padding.left(), padding.top(), width(), _label); } -} // namespace Passport +} // namespace Passport::Ui diff --git a/Telegram/SourceFiles/passport/passport_panel_details_row.h b/Telegram/SourceFiles/passport/ui/passport_details_row.h similarity index 83% rename from Telegram/SourceFiles/passport/passport_panel_details_row.h rename to Telegram/SourceFiles/passport/ui/passport_details_row.h index fc02049670..d93708fe44 100644 --- a/Telegram/SourceFiles/passport/passport_panel_details_row.h +++ b/Telegram/SourceFiles/passport/ui/passport_details_row.h @@ -11,18 +11,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/animations.h" #include "ui/wrap/padding_wrap.h" #include "ui/widgets/labels.h" -#include "boxes/abstract_box.h" namespace Ui { +class BoxContent; class InputField; class FlatLabel; template class SlideWrap; } // namespace Ui -namespace Passport { +namespace Passport::Ui { -class PanelController; +using namespace ::Ui; enum class PanelDetailsType { Text, @@ -32,7 +32,7 @@ enum class PanelDetailsType { Gender, }; -class PanelDetailsRow : public Ui::RpWidget { +class PanelDetailsRow : public RpWidget { public: using Type = PanelDetailsType; @@ -43,8 +43,9 @@ public: static object_ptr Create( QWidget *parent, + Fn)> showBox, + const QString &defaultCountry, Type type, - not_null controller, const QString &label, int maxLabelWidth, const QString &value, @@ -74,11 +75,11 @@ private: QString _label; int _maxLabelWidth = 0; - object_ptr> _error = { nullptr }; + object_ptr> _error = { nullptr }; bool _errorShown = false; bool _errorHideSubscription = false; - Ui::Animations::Simple _errorAnimation; + Animations::Simple _errorAnimation; }; -} // namespace Passport +} // namespace Passport::Ui diff --git a/Telegram/SourceFiles/passport/ui/passport_form_row.cpp b/Telegram/SourceFiles/passport/ui/passport_form_row.cpp new file mode 100644 index 0000000000..6d31044083 --- /dev/null +++ b/Telegram/SourceFiles/passport/ui/passport_form_row.cpp @@ -0,0 +1,125 @@ +/* +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 "passport/ui/passport_form_row.h" + +#include "ui/text/text_options.h" +#include "styles/style_passport.h" +#include "styles/style_layers.h" + +namespace Passport::Ui { + +FormRow::FormRow(QWidget *parent) +: RippleButton(parent, st::passportRowRipple) +, _title(st::boxWideWidth / 2) +, _description(st::boxWideWidth / 2) { +} + +void FormRow::updateContent( + const QString &title, + const QString &description, + bool ready, + bool error, + anim::type animated) { + _title.setText( + st::semiboldTextStyle, + title, + NameTextOptions()); + _description.setText( + st::defaultTextStyle, + description, + TextParseOptions { + TextParseMultiline, + 0, + 0, + Qt::LayoutDirectionAuto + }); + _ready = ready && !error; + if (_error != error) { + _error = error; + if (animated == anim::type::instant) { + _errorAnimation.stop(); + } else { + _errorAnimation.start( + [=] { update(); }, + _error ? 0. : 1., + _error ? 1. : 0., + st::fadeWrapDuration); + } + } + resizeToWidth(width()); + update(); +} + +int FormRow::resizeGetHeight(int newWidth) { + const auto availableWidth = countAvailableWidth(newWidth); + _titleHeight = _title.countHeight(availableWidth); + _descriptionHeight = _description.countHeight(availableWidth); + const auto result = st::passportRowPadding.top() + + _titleHeight + + st::passportRowSkip + + _descriptionHeight + + st::passportRowPadding.bottom(); + return result; +} + +int FormRow::countAvailableWidth(int newWidth) const { + return newWidth + - st::passportRowPadding.left() + - st::passportRowPadding.right() + - (_ready + ? st::passportRowReadyIcon + : st::passportRowEmptyIcon).width() + - st::passportRowIconSkip; +} + +int FormRow::countAvailableWidth() const { + return countAvailableWidth(width()); +} + +void FormRow::paintEvent(QPaintEvent *e) { + Painter p(this); + + paintRipple(p, 0, 0); + + const auto left = st::passportRowPadding.left(); + const auto availableWidth = countAvailableWidth(); + auto top = st::passportRowPadding.top(); + + const auto error = _errorAnimation.value(_error ? 1. : 0.); + + p.setPen(st::passportRowTitleFg); + _title.drawLeft(p, left, top, availableWidth, width()); + top += _titleHeight + st::passportRowSkip; + + p.setPen(anim::pen( + st::passportRowDescriptionFg, + st::boxTextFgError, + error)); + _description.drawLeft(p, left, top, availableWidth, width()); + top += _descriptionHeight + st::passportRowPadding.bottom(); + + const auto &icon = _ready + ? st::passportRowReadyIcon + : st::passportRowEmptyIcon; + if (error > 0. && !_ready) { + icon.paint( + p, + width() - st::passportRowPadding.right() - icon.width(), + (height() - icon.height()) / 2, + width(), + anim::color(st::menuIconFgOver, st::boxTextFgError, error)); + } else { + icon.paint( + p, + width() - st::passportRowPadding.right() - icon.width(), + (height() - icon.height()) / 2, + width()); + } +} + +} // namespace Passport::Ui diff --git a/Telegram/SourceFiles/passport/ui/passport_form_row.h b/Telegram/SourceFiles/passport/ui/passport_form_row.h new file mode 100644 index 0000000000..2778df2048 --- /dev/null +++ b/Telegram/SourceFiles/passport/ui/passport_form_row.h @@ -0,0 +1,48 @@ +/* +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 "ui/text/text.h" +#include "ui/effects/animations.h" +#include "ui/widgets/buttons.h" + +namespace Passport::Ui { + +using namespace ::Ui; + +class FormRow : public RippleButton { +public: + explicit FormRow(QWidget *parent); + + void updateContent( + const QString &title, + const QString &description, + bool ready, + bool error, + anim::type animated); + +protected: + int resizeGetHeight(int newWidth) override; + + void paintEvent(QPaintEvent *e) override; + +private: + int countAvailableWidth() const; + int countAvailableWidth(int newWidth) const; + + Text::String _title; + Text::String _description; + int _titleHeight = 0; + int _descriptionHeight = 0; + bool _ready = false; + bool _error = false; + Animations::Simple _errorAnimation; + +}; + +} // namespace Passport::Ui diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.cpp b/Telegram/SourceFiles/payments/payments_checkout_process.cpp index 916150a204..d0606e4cae 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.cpp +++ b/Telegram/SourceFiles/payments/payments_checkout_process.cpp @@ -95,11 +95,19 @@ not_null CheckoutProcess::panelDelegate() { void CheckoutProcess::handleFormUpdate(const FormUpdate &update) { v::match(update.data, [&](const FormReady &) { - _panel->showForm(_form->invoice()); - }, [&](const FormError &error) { + showForm(); + }, [&](const FormError &error) { // #TODO payments refactor errors handleFormError(error); + }, [&](const ValidateError &error) { + handleValidateError(error); }, [&](const SendError &error) { handleSendError(error); + }, [&](const ValidateFinished &) { + showForm(); + if (_submitState == SubmitState::Validation) { + _submitState = SubmitState::Validated; + panelSubmit(); + } }, [&](const VerificationNeeded &info) { if (_webviewWindow) { _webviewWindow->navigate(info.url); @@ -130,8 +138,44 @@ void CheckoutProcess::handleFormError(const FormError &error) { } else if (type == u"INVOICE_ALREADY_PAID"_q) { } - App::wnd()->activate(); - Ui::Toast::Show("payments.getPaymentForm: " + type); + if (_panel) { + _panel->showToast("payments.getPaymentForm: " + type); + } else { + App::wnd()->activate(); + Ui::Toast::Show("payments.getPaymentForm: " + type); + } +} + +void CheckoutProcess::handleValidateError(const ValidateError &error) { + // #TODO payments errors + const auto &type = error.type; + if (type == u"REQ_INFO_NAME_INVALID"_q) { + + } else if (type == u"REQ_INFO_EMAIL_INVALID"_q) { + + } else if (type == u"REQ_INFO_PHONE_INVALID"_q) { + + } else if (type == u"ADDRESS_STREET_LINE1_INVALID"_q) { + + } else if (type == u"ADDRESS_CITY_INVALID"_q) { + + } else if (type == u"ADDRESS_STATE_INVALID"_q) { + + } else if (type == u"ADDRESS_COUNTRY_INVALID"_q) { + + } else if (type == u"ADDRESS_POSTCODE_INVALID"_q) { + + } else if (type == u"SHIPPING_BOT_TIMEOUT"_q) { + + } else if (type == u"SHIPPING_NOT_AVAILABLE"_q) { + + } + if (_panel) { + _panel->showToast("payments.validateRequestedInfo: " + type); + } else { + App::wnd()->activate(); + Ui::Toast::Show("payments.validateRequestedInfo: " + type); + } } void CheckoutProcess::handleSendError(const SendError &error) { @@ -150,8 +194,12 @@ void CheckoutProcess::handleSendError(const SendError &error) { } else if (type == u"BOT_PRECHECKOUT_FAILED"_q) { } - App::wnd()->activate(); - Ui::Toast::Show("payments.sendPaymentForm: " + type); + if (_panel) { + _panel->showToast("payments.sendPaymentForm: " + type); + } else { + App::wnd()->activate(); + Ui::Toast::Show("payments.sendPaymentForm: " + type); + } } void CheckoutProcess::panelRequestClose() { @@ -176,6 +224,26 @@ void CheckoutProcess::panelCloseSure() { } void CheckoutProcess::panelSubmit() { + if (_submitState == SubmitState::Validation + || _submitState == SubmitState::Finishing) { + return; + } + const auto &invoice = _form->invoice(); + const auto &options = _form->shippingOptions(); + if (!options.list.empty() && options.selectedId.isEmpty()) { + chooseShippingOption(); + return; + } else if (_submitState != SubmitState::Validated + && options.list.empty() + && (invoice.isShippingAddressRequested + || invoice.isNameRequested + || invoice.isEmailRequested + || invoice.isPhoneRequested)) { + _submitState = SubmitState::Validation; + _form->validateInformation(_form->savedInformation()); + return; + } + _submitState = SubmitState::Finishing; _webviewWindow = std::make_unique( _form->details().url, panelDelegate()); @@ -237,4 +305,62 @@ bool CheckoutProcess::panelWebviewNavigationAttempt(const QString &uri) { return false; } +void CheckoutProcess::panelEditShippingInformation() { + showEditInformation(Ui::EditField::ShippingInformation); +} + +void CheckoutProcess::panelEditName() { + showEditInformation(Ui::EditField::Name); +} + +void CheckoutProcess::panelEditEmail() { + showEditInformation(Ui::EditField::Email); +} + +void CheckoutProcess::panelEditPhone() { + showEditInformation(Ui::EditField::Phone); +} + +void CheckoutProcess::showForm() { + _panel->showForm( + _form->invoice(), + _form->savedInformation(), + _form->shippingOptions()); +} + +void CheckoutProcess::showEditInformation(Ui::EditField field) { + if (_submitState != SubmitState::None) { + return; + } + _panel->showEditInformation( + _form->invoice(), + _form->savedInformation(), + field); +} + +void CheckoutProcess::chooseShippingOption() { + _panel->chooseShippingOption(_form->shippingOptions()); +} + +void CheckoutProcess::panelChooseShippingOption() { + if (_submitState != SubmitState::None) { + return; + } + chooseShippingOption(); +} + +void CheckoutProcess::panelChangeShippingOption(const QString &id) { + _form->setShippingOption(id); + showForm(); +} + +void CheckoutProcess::panelValidateInformation( + Ui::RequestedInformation data) { + _form->validateInformation(data); +} + +void CheckoutProcess::panelShowBox(object_ptr box) { + _panel->showBox(std::move(box)); +} + } // namespace Payments diff --git a/Telegram/SourceFiles/payments/payments_checkout_process.h b/Telegram/SourceFiles/payments/payments_checkout_process.h index 1eff37db96..7c130f9d5b 100644 --- a/Telegram/SourceFiles/payments/payments_checkout_process.h +++ b/Telegram/SourceFiles/payments/payments_checkout_process.h @@ -19,6 +19,7 @@ class Session; namespace Payments::Ui { class Panel; class WebviewWindow; +enum class EditField; } // namespace Payments::Ui namespace Payments { @@ -27,6 +28,7 @@ class Form; struct FormUpdate; struct FormError; struct SendError; +struct ValidateError; class CheckoutProcess final : public base::has_weak_ptr @@ -45,22 +47,44 @@ public: void requestActivate(); private: + enum class SubmitState { + None, + Validation, + Validated, + Finishing, + }; [[nodiscard]] not_null panelDelegate(); void handleFormUpdate(const FormUpdate &update); void handleFormError(const FormError &error); + void handleValidateError(const ValidateError &error); void handleSendError(const SendError &error); + void showForm(); + void showEditInformation(Ui::EditField field); + void chooseShippingOption(); + void panelRequestClose() override; void panelCloseSure() override; void panelSubmit() override; void panelWebviewMessage(const QJsonDocument &message) override; bool panelWebviewNavigationAttempt(const QString &uri) override; + void panelEditShippingInformation() override; + void panelEditName() override; + void panelEditEmail() override; + void panelEditPhone() override; + void panelChooseShippingOption() override; + void panelChangeShippingOption(const QString &id) override; + + void panelValidateInformation(Ui::RequestedInformation data) override; + void panelShowBox(object_ptr box) override; + const not_null _session; const std::unique_ptr
_form; const std::unique_ptr _panel; std::unique_ptr _webviewWindow; + SubmitState _submitState = SubmitState::None; rpl::lifetime _lifetime; diff --git a/Telegram/SourceFiles/payments/payments_form.cpp b/Telegram/SourceFiles/payments/payments_form.cpp index 397a5058f1..cc60acb9ad 100644 --- a/Telegram/SourceFiles/payments/payments_form.cpp +++ b/Telegram/SourceFiles/payments/payments_form.cpp @@ -27,16 +27,53 @@ namespace { }); } +[[nodiscard]] std::vector ParsePrices( + const MTPVector &data) { + return ranges::views::all( + data.v + ) | ranges::views::transform([](const MTPLabeledPrice &price) { + return price.match([&](const MTPDlabeledPrice &data) { + return Ui::LabeledPrice{ + .label = qs(data.vlabel()), + .price = *reinterpret_cast(&data.vamount().v), + }; + }); + }) | ranges::to_vector; +} + +[[nodiscard]] MTPPaymentRequestedInfo Serialize( + const Ui::RequestedInformation &information) { + using Flag = MTPDpaymentRequestedInfo::Flag; + return MTP_paymentRequestedInfo( + MTP_flags((information.name.isEmpty() ? Flag(0) : Flag::f_name) + | (information.email.isEmpty() ? Flag(0) : Flag::f_email) + | (information.phone.isEmpty() ? Flag(0) : Flag::f_phone) + | (information.shippingAddress + ? Flag::f_shipping_address + : Flag(0))), + MTP_string(information.name), + MTP_string(information.phone), + MTP_string(information.email), + MTP_postAddress( + MTP_string(information.shippingAddress.address1), + MTP_string(information.shippingAddress.address2), + MTP_string(information.shippingAddress.city), + MTP_string(information.shippingAddress.state), + MTP_string(information.shippingAddress.countryIso2), + MTP_string(information.shippingAddress.postCode))); +} + } // namespace Form::Form(not_null session, FullMsgId itemId) : _session(session) +, _api(&_session->mtp()) , _msgId(itemId.msg) { requestForm(); } void Form::requestForm() { - _session->api().request(MTPpayments_GetPaymentForm( + _api.request(MTPpayments_GetPaymentForm( MTP_int(_msgId) )).done([=](const MTPpayments_PaymentForm &result) { result.match([&](const auto &data) { @@ -69,18 +106,8 @@ void Form::processForm(const MTPDpayments_paymentForm &data) { } void Form::processInvoice(const MTPDinvoice &data) { - auto &&prices = ranges::views::all( - data.vprices().v - ) | ranges::views::transform([](const MTPLabeledPrice &price) { - return price.match([&](const MTPDlabeledPrice &data) { - return Ui::LabeledPrice{ - .label = qs(data.vlabel()), - .price = data.vamount().v, - }; - }); - }); _invoice = Ui::Invoice{ - .prices = prices | ranges::to_vector, + .prices = ParsePrices(data.vprices()), .currency = qs(data.vcurrency()), .isNameRequested = data.is_name_requested(), @@ -115,7 +142,7 @@ void Form::processDetails(const MTPDpayments_paymentForm &data) { void Form::processSavedInformation(const MTPDpaymentRequestedInfo &data) { const auto address = data.vshipping_address(); - _savedInformation = Ui::SavedInformation{ + _savedInformation = Ui::RequestedInformation{ .name = qs(data.vname().value_or_empty()), .phone = qs(data.vphone().value_or_empty()), .email = qs(data.vemail().value_or_empty()), @@ -132,11 +159,17 @@ void Form::processSavedCredentials( } void Form::send(const QByteArray &serializedCredentials) { - _session->api().request(MTPpayments_SendPaymentForm( - MTP_flags(0), + using Flag = MTPpayments_SendPaymentForm::Flag; + _api.request(MTPpayments_SendPaymentForm( + MTP_flags((_requestedInformationId.isEmpty() + ? Flag(0) + : Flag::f_requested_info_id) + | (_shippingOptions.selectedId.isEmpty() + ? Flag(0) + : Flag::f_shipping_option_id)), MTP_int(_msgId), - MTPstring(), // requested_info_id - MTPstring(), // shipping_option_id, + MTP_string(_requestedInformationId), + MTP_string(_shippingOptions.selectedId), MTP_inputPaymentCredentials( MTP_flags(0), MTP_dataJSON(MTP_bytes(serializedCredentials))) @@ -151,4 +184,59 @@ void Form::send(const QByteArray &serializedCredentials) { }).send(); } +void Form::validateInformation(const Ui::RequestedInformation &information) { + if (_validateRequestId) { + if (_validatedInformation == information) { + return; + } + _api.request(base::take(_validateRequestId)).cancel(); + } + _validatedInformation = information; + _validateRequestId = _api.request(MTPpayments_ValidateRequestedInfo( + MTP_flags(0), // #TODO payments save information + MTP_int(_msgId), + Serialize(information) + )).done([=](const MTPpayments_ValidatedRequestedInfo &result) { + _validateRequestId = 0; + const auto oldSelectedId = _shippingOptions.selectedId; + result.match([&](const MTPDpayments_validatedRequestedInfo &data) { + _requestedInformationId = data.vid().value_or_empty(); + processShippingOptions( + data.vshipping_options().value_or_empty()); + }); + _shippingOptions.selectedId = ranges::contains( + _shippingOptions.list, + oldSelectedId, + &Ui::ShippingOption::id + ) ? oldSelectedId : QString(); + if (_shippingOptions.selectedId.isEmpty() + && _shippingOptions.list.size() == 1) { + _shippingOptions.selectedId = _shippingOptions.list.front().id; + } + _savedInformation = _validatedInformation; + _updates.fire({ ValidateFinished{} }); + }).fail([=](const MTP::Error &error) { + _validateRequestId = 0; + _updates.fire({ ValidateError{ error.type() } }); + }).send(); +} + +void Form::setShippingOption(const QString &id) { + _shippingOptions.selectedId = id; +} + +void Form::processShippingOptions(const QVector &data) { + _shippingOptions = Ui::ShippingOptions{ ranges::views::all( + data + ) | ranges::views::transform([](const MTPShippingOption &option) { + return option.match([](const MTPDshippingOption &data) { + return Ui::ShippingOption{ + .id = qs(data.vid()), + .title = qs(data.vtitle()), + .prices = ParsePrices(data.vprices()), + }; + }); + }) | ranges::to_vector }; +} + } // namespace Payments diff --git a/Telegram/SourceFiles/payments/payments_form.h b/Telegram/SourceFiles/payments/payments_form.h index 92288605ce..c553ab5c2b 100644 --- a/Telegram/SourceFiles/payments/payments_form.h +++ b/Telegram/SourceFiles/payments/payments_form.h @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "payments/ui/payments_panel_data.h" +#include "mtproto/sender.h" namespace Main { class Session; @@ -34,10 +35,16 @@ struct FormDetails { struct FormReady {}; +struct ValidateFinished {}; + struct FormError { QString type; }; +struct ValidateError { + QString type; +}; + struct SendError { QString type; }; @@ -54,8 +61,10 @@ struct FormUpdate { std::variant< FormReady, FormError, + ValidateError, SendError, VerificationNeeded, + ValidateFinished, PaymentFinished> data; }; @@ -69,17 +78,22 @@ public: [[nodiscard]] const FormDetails &details() const { return _details; } - [[nodiscard]] const Ui::SavedInformation &savedInformation() const { + [[nodiscard]] const Ui::RequestedInformation &savedInformation() const { return _savedInformation; } [[nodiscard]] const Ui::SavedCredentials &savedCredentials() const { return _savedCredentials; } + [[nodiscard]] const Ui::ShippingOptions &shippingOptions() const { + return _shippingOptions; + } [[nodiscard]] rpl::producer updates() const { return _updates.events(); } + void validateInformation(const Ui::RequestedInformation &information); + void setShippingOption(const QString &id); void send(const QByteArray &serializedCredentials); private: @@ -90,15 +104,23 @@ private: void processSavedInformation(const MTPDpaymentRequestedInfo &data); void processSavedCredentials( const MTPDpaymentSavedCredentialsCard &data); + void processShippingOptions(const QVector &data); const not_null _session; + MTP::Sender _api; MsgId _msgId = 0; Ui::Invoice _invoice; FormDetails _details; - Ui::SavedInformation _savedInformation; + Ui::RequestedInformation _savedInformation; Ui::SavedCredentials _savedCredentials; + Ui::RequestedInformation _validatedInformation; + mtpRequestId _validateRequestId = 0; + + Ui::ShippingOptions _shippingOptions; + QString _requestedInformationId; + rpl::event_stream _updates; }; diff --git a/Telegram/SourceFiles/payments/ui/payments.style b/Telegram/SourceFiles/payments/ui/payments.style index 6e99489ebd..95700e28c4 100644 --- a/Telegram/SourceFiles/payments/ui/payments.style +++ b/Telegram/SourceFiles/payments/ui/payments.style @@ -8,3 +8,5 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL using "ui/basic.style"; using "passport/passport.style"; + +paymentsFormPricePadding: margins(22px, 7px, 22px, 6px); diff --git a/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp b/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp new file mode 100644 index 0000000000..08026b5859 --- /dev/null +++ b/Telegram/SourceFiles/payments/ui/payments_edit_information.cpp @@ -0,0 +1,262 @@ +/* +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 "payments/ui/payments_edit_information.h" + +#include "payments/ui/payments_panel_delegate.h" +#include "passport/ui/passport_details_row.h" +#include "ui/widgets/scroll_area.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/labels.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/wrap/fade_wrap.h" +#include "lang/lang_keys.h" +#include "styles/style_payments.h" +#include "styles/style_passport.h" + +namespace Payments::Ui { +namespace { + +constexpr auto kMaxStreetSize = 64; +constexpr auto kMaxPostcodeSize = 10; +constexpr auto kMaxNameSize = 64; +constexpr auto kMaxEmailSize = 128; +constexpr auto kMaxPhoneSize = 16; + +} // namespace + +EditInformation::EditInformation( + QWidget *parent, + const Invoice &invoice, + const RequestedInformation ¤t, + EditField field, + not_null delegate) +: _delegate(delegate) +, _invoice(invoice) +, _information(current) +, _scroll(this, st::passportPanelScroll) +, _topShadow(this) +, _bottomShadow(this) +, _done( + this, + tr::lng_about_done(), + st::passportPanelSaveValue) { + setupControls(); +} + +void EditInformation::setupControls() { + const auto inner = setupContent(); + + _done->addClickHandler([=] { + _delegate->panelValidateInformation(collect()); + }); + + using namespace rpl::mappers; + + _topShadow->toggleOn( + _scroll->scrollTopValue() | rpl::map(_1 > 0)); + _bottomShadow->toggleOn(rpl::combine( + _scroll->scrollTopValue(), + _scroll->heightValue(), + inner->heightValue(), + _1 + _2 < _3)); +} + +not_null EditInformation::setupContent() { + const auto inner = _scroll->setOwnedWidget( + object_ptr(this)); + + _scroll->widthValue( + ) | rpl::start_with_next([=](int width) { + inner->resizeToWidth(width); + }, inner->lifetime()); + + const auto showBox = [=](object_ptr box) { + _delegate->panelShowBox(std::move(box)); + }; + using Type = Passport::Ui::PanelDetailsType; + auto maxLabelWidth = 0; + if (_invoice.isShippingAddressRequested) { + accumulate_max( + maxLabelWidth, + Row::LabelWidth(tr::lng_passport_street(tr::now))); + accumulate_max( + maxLabelWidth, + Row::LabelWidth(tr::lng_passport_city(tr::now))); + accumulate_max( + maxLabelWidth, + Row::LabelWidth(tr::lng_passport_state(tr::now))); + accumulate_max( + maxLabelWidth, + Row::LabelWidth(tr::lng_passport_country(tr::now))); + accumulate_max( + maxLabelWidth, + Row::LabelWidth(tr::lng_passport_postcode(tr::now))); + } + if (_invoice.isNameRequested) { + accumulate_max( + maxLabelWidth, + Row::LabelWidth(tr::lng_payments_info_name(tr::now))); + } + if (_invoice.isEmailRequested) { + accumulate_max( + maxLabelWidth, + Row::LabelWidth(tr::lng_payments_info_email(tr::now))); + } + if (_invoice.isPhoneRequested) { + accumulate_max( + maxLabelWidth, + Row::LabelWidth(tr::lng_payments_info_phone(tr::now))); + } + if (_invoice.isShippingAddressRequested) { + _street1 = inner->add( + Row::Create( + inner, + showBox, + QString(), + Type::Text, + tr::lng_passport_street(tr::now), + maxLabelWidth, + _information.shippingAddress.address1, + QString(), + kMaxStreetSize)); + _street2 = inner->add( + Row::Create( + inner, + showBox, + QString(), + Type::Text, + tr::lng_passport_street(tr::now), + maxLabelWidth, + _information.shippingAddress.address2, + QString(), + kMaxStreetSize)); + _city = inner->add( + Row::Create( + inner, + showBox, + QString(), + Type::Text, + tr::lng_passport_city(tr::now), + maxLabelWidth, + _information.shippingAddress.city, + QString(), + kMaxStreetSize)); + _state = inner->add( + Row::Create( + inner, + showBox, + QString(), + Type::Text, + tr::lng_passport_state(tr::now), + maxLabelWidth, + _information.shippingAddress.state, + QString(), + kMaxStreetSize)); + _country = inner->add( + Row::Create( + inner, + showBox, + QString(), + Type::Country, + tr::lng_passport_country(tr::now), + maxLabelWidth, + _information.shippingAddress.countryIso2, + QString())); + _postcode = inner->add( + Row::Create( + inner, + showBox, + QString(), + Type::Postcode, + tr::lng_passport_postcode(tr::now), + maxLabelWidth, + _information.shippingAddress.postCode, + QString(), + kMaxPostcodeSize)); + //StreetValidate, // #TODO payments + //CityValidate, + //CountryValidate, + //CountryFormat, + //PostcodeValidate, + } + if (_invoice.isNameRequested) { + _name = inner->add( + Row::Create( + inner, + showBox, + QString(), + Type::Text, + tr::lng_payments_info_name(tr::now), + maxLabelWidth, + _information.name, + QString(), + kMaxNameSize)); + } + if (_invoice.isEmailRequested) { + _email = inner->add( + Row::Create( + inner, + showBox, + QString(), + Type::Text, + tr::lng_payments_info_email(tr::now), + maxLabelWidth, + _information.email, + QString(), + kMaxEmailSize)); + } + if (_invoice.isPhoneRequested) { + _phone = inner->add( + Row::Create( + inner, + showBox, + QString(), + Type::Text, + tr::lng_payments_info_phone(tr::now), + maxLabelWidth, + _information.phone, + QString(), + kMaxPhoneSize)); + } + return inner; +} + +void EditInformation::resizeEvent(QResizeEvent *e) { + updateControlsGeometry(); +} + +void EditInformation::updateControlsGeometry() { + const auto submitTop = height() - _done->height(); + _scroll->setGeometry(0, 0, width(), submitTop); + _topShadow->resizeToWidth(width()); + _topShadow->moveToLeft(0, 0); + _bottomShadow->resizeToWidth(width()); + _bottomShadow->moveToLeft(0, submitTop - st::lineWidth); + _done->setFullWidth(width()); + _done->moveToLeft(0, submitTop); + + _scroll->updateBars(); +} + +RequestedInformation EditInformation::collect() const { + return { + .name = _name ? _name->valueCurrent() : QString(), + .phone = _phone ? _phone->valueCurrent() : QString(), + .email = _email ? _email->valueCurrent() : QString(), + .shippingAddress = { + .address1 = _street1 ? _street1->valueCurrent() : QString(), + .address2 = _street2 ? _street2->valueCurrent() : QString(), + .city = _city ? _city->valueCurrent() : QString(), + .state = _state ? _state->valueCurrent() : QString(), + .countryIso2 = _country ? _country->valueCurrent() : QString(), + .postCode = _postcode ? _postcode->valueCurrent() : QString(), + }, + }; +} + +} // namespace Payments::Ui diff --git a/Telegram/SourceFiles/payments/ui/payments_edit_information.h b/Telegram/SourceFiles/payments/ui/payments_edit_information.h new file mode 100644 index 0000000000..fecd707367 --- /dev/null +++ b/Telegram/SourceFiles/payments/ui/payments_edit_information.h @@ -0,0 +1,71 @@ +/* +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 "ui/rp_widget.h" +#include "payments/ui/payments_panel_data.h" +#include "base/object_ptr.h" + +namespace Ui { +class ScrollArea; +class FadeShadow; +class RoundButton; +} // namespace Ui + +namespace Passport::Ui { +class PanelDetailsRow; +} // namespace Passport::Ui + +namespace Payments::Ui { + +using namespace ::Ui; + +class PanelDelegate; + +class EditInformation final : public RpWidget { +public: + EditInformation( + QWidget *parent, + const Invoice &invoice, + const RequestedInformation ¤t, + EditField field, + not_null delegate); + +private: + using Row = Passport::Ui::PanelDetailsRow; + + void resizeEvent(QResizeEvent *e) override; + + void setupControls(); + [[nodiscard]] not_null setupContent(); + void updateControlsGeometry(); + + [[nodiscard]] RequestedInformation collect() const; + + const not_null _delegate; + Invoice _invoice; + RequestedInformation _information; + + object_ptr _scroll; + object_ptr _topShadow; + object_ptr _bottomShadow; + object_ptr _done; + + Row *_street1 = nullptr; + Row *_street2 = nullptr; + Row *_city = nullptr; + Row *_state = nullptr; + Row *_country = nullptr; + Row *_postcode = nullptr; + Row *_name = nullptr; + Row *_email = nullptr; + Row *_phone = nullptr; + +}; + +} // namespace Payments::Ui diff --git a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp index 935aa7cdd7..03656e6b24 100644 --- a/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_form_summary.cpp @@ -8,10 +8,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "payments/ui/payments_form_summary.h" #include "payments/ui/payments_panel_delegate.h" +#include "passport/ui/passport_form_row.h" #include "ui/widgets/scroll_area.h" #include "ui/widgets/buttons.h" +#include "ui/widgets/labels.h" #include "ui/wrap/vertical_layout.h" #include "ui/wrap/fade_wrap.h" +#include "ui/text/format_values.h" #include "lang/lang_keys.h" #include "styles/style_payments.h" #include "styles/style_passport.h" @@ -19,24 +22,56 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Payments::Ui { using namespace ::Ui; +using namespace Passport::Ui; class PanelDelegate; FormSummary::FormSummary( QWidget *parent, const Invoice &invoice, + const RequestedInformation ¤t, + const ShippingOptions &options, not_null delegate) : _delegate(delegate) +, _invoice(invoice) +, _options(options) +, _information(current) , _scroll(this, st::passportPanelScroll) , _topShadow(this) , _bottomShadow(this) , _submit( this, - tr::lng_payments_pay_amount(lt_amount, rpl::single(QString("much"))), + tr::lng_payments_pay_amount( + lt_amount, + rpl::single(computeTotalAmount())), st::passportPanelAuthorize) { setupControls(); } +QString FormSummary::computeAmount(int64 amount) const { + return FillAmountAndCurrency(amount, _invoice.currency); +} + +QString FormSummary::computeTotalAmount() const { + const auto total = ranges::accumulate( + _invoice.prices, + int64(0), + std::plus<>(), + &LabeledPrice::price); + const auto selected = ranges::find( + _options.list, + _options.selectedId, + &ShippingOption::id); + const auto shipping = (selected != end(_options.list)) + ? ranges::accumulate( + selected->prices, + int64(0), + std::plus<>(), + &LabeledPrice::price) + : int64(0); + return computeAmount(total + shipping); +} + void FormSummary::setupControls() { const auto inner = setupContent(); @@ -64,6 +99,116 @@ not_null FormSummary::setupContent() { inner->resizeToWidth(width); }, inner->lifetime()); + for (const auto &price : _invoice.prices) { + inner->add( + object_ptr( + inner, + price.label + ": " + computeAmount(price.price), + st::passportFormPolicy), + st::paymentsFormPricePadding); + } + const auto selected = ranges::find( + _options.list, + _options.selectedId, + &ShippingOption::id); + if (selected != end(_options.list)) { + for (const auto &price : selected->prices) { + inner->add( + object_ptr( + inner, + price.label + ": " + computeAmount(price.price), + st::passportFormPolicy), + st::paymentsFormPricePadding); + } + } + inner->add( + object_ptr( + inner, + "Total: " + computeTotalAmount(), + st::passportFormHeader), + st::passportFormHeaderPadding); + + inner->add( + object_ptr( + inner, + st::passportFormDividerHeight), + { 0, 0, 0, st::passportFormHeaderPadding.top() }); + + if (_invoice.isShippingAddressRequested) { + const auto info = inner->add(object_ptr(inner)); + info->addClickHandler([=] { + _delegate->panelEditShippingInformation(); + }); + auto list = QStringList(); + const auto push = [&](const QString &value) { + if (!value.isEmpty()) { + list.push_back(value); + } + }; + push(_information.shippingAddress.address1); + push(_information.shippingAddress.address2); + push(_information.shippingAddress.city); + push(_information.shippingAddress.state); + push(_information.shippingAddress.countryIso2); + push(_information.shippingAddress.postCode); + info->updateContent( + tr::lng_payments_shipping_address(tr::now), + (list.isEmpty() ? "enter pls" : list.join(", ")), + !list.isEmpty(), + false, + anim::type::instant); + } + if (!_options.list.empty()) { + const auto options = inner->add(object_ptr(inner)); + options->addClickHandler([=] { + _delegate->panelChooseShippingOption(); + }); + options->updateContent( + tr::lng_payments_shipping_method(tr::now), + (selected != end(_options.list) + ? selected->title + : "enter pls"), + (selected != end(_options.list)), + false, + anim::type::instant); + } + if (_invoice.isNameRequested) { + const auto name = inner->add(object_ptr(inner)); + name->addClickHandler([=] { _delegate->panelEditName(); }); + name->updateContent( + tr::lng_payments_info_name(tr::now), + (_information.name.isEmpty() + ? "enter pls" + : _information.name), + !_information.name.isEmpty(), + false, + anim::type::instant); + } + if (_invoice.isEmailRequested) { + const auto email = inner->add(object_ptr(inner)); + email->addClickHandler([=] { _delegate->panelEditEmail(); }); + email->updateContent( + tr::lng_payments_info_email(tr::now), + (_information.email.isEmpty() + ? "enter pls" + : _information.email), + !_information.email.isEmpty(), + false, + anim::type::instant); + } + if (_invoice.isPhoneRequested) { + const auto phone = inner->add(object_ptr(inner)); + phone->addClickHandler([=] { _delegate->panelEditPhone(); }); + phone->updateContent( + tr::lng_payments_info_email(tr::now), + (_information.phone.isEmpty() + ? "enter pls" + : _information.phone), + !_information.phone.isEmpty(), + false, + anim::type::instant); + } + return inner; } diff --git a/Telegram/SourceFiles/payments/ui/payments_form_summary.h b/Telegram/SourceFiles/payments/ui/payments_form_summary.h index 39ca9e7f06..32dd89bc75 100644 --- a/Telegram/SourceFiles/payments/ui/payments_form_summary.h +++ b/Telegram/SourceFiles/payments/ui/payments_form_summary.h @@ -28,6 +28,8 @@ public: FormSummary( QWidget *parent, const Invoice &invoice, + const RequestedInformation ¤t, + const ShippingOptions &options, not_null delegate); private: @@ -37,7 +39,13 @@ private: [[nodiscard]] not_null setupContent(); void updateControlsGeometry(); + [[nodiscard]] QString computeAmount(int64 amount) const; + [[nodiscard]] QString computeTotalAmount() const; + const not_null _delegate; + Invoice _invoice; + ShippingOptions _options; + RequestedInformation _information; object_ptr _scroll; object_ptr _topShadow; object_ptr _bottomShadow; diff --git a/Telegram/SourceFiles/payments/ui/payments_panel.cpp b/Telegram/SourceFiles/payments/ui/payments_panel.cpp index 7c5ab32fd6..22a977c879 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_panel.cpp @@ -8,8 +8,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "payments/ui/payments_panel.h" #include "payments/ui/payments_form_summary.h" +#include "payments/ui/payments_edit_information.h" #include "payments/ui/payments_panel_delegate.h" #include "ui/widgets/separate_panel.h" +#include "ui/boxes/single_choice_box.h" #include "lang/lang_keys.h" #include "styles/style_payments.h" #include "styles/style_passport.h" @@ -39,9 +41,63 @@ void Panel::requestActivate() { _widget->showAndActivate(); } -void Panel::showForm(const Invoice &invoice) { +void Panel::showForm( + const Invoice &invoice, + const RequestedInformation ¤t, + const ShippingOptions &options) { _widget->showInner( - base::make_unique_q(_widget.get(), invoice, _delegate)); + base::make_unique_q( + _widget.get(), + invoice, + current, + options, + _delegate)); +} + +void Panel::showEditInformation( + const Invoice &invoice, + const RequestedInformation ¤t, + EditField field) { + _widget->showInner(base::make_unique_q( + _widget.get(), + invoice, + current, + field, + _delegate)); +} + +void Panel::chooseShippingOption(const ShippingOptions &options) { + showBox(Box([=](not_null box) { + auto list = options.list | ranges::views::transform( + &ShippingOption::title + ) | ranges::to_vector; + const auto i = ranges::find( + options.list, + options.selectedId, + &ShippingOption::id); + const auto save = [=](int option) { + _delegate->panelChangeShippingOption(options.list[option].id); + }; + SingleChoiceBox(box, { + .title = tr::lng_payments_shipping_method(), + .options = list, + .initialSelection = (i != end(options.list) + ? (i - begin(options.list)) + : -1), + .callback = save, + }); + })); +} + +void Panel::showBox(object_ptr box) { + _widget->showBox( + std::move(box), + Ui::LayerOption::KeepOther, + anim::type::normal); +} + +void Panel::showToast(const QString &text) { + _widget->showToast(text); } } // namespace Payments::Ui diff --git a/Telegram/SourceFiles/payments/ui/payments_panel.h b/Telegram/SourceFiles/payments/ui/payments_panel.h index 0aa68c456f..102f0fa2cb 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel.h +++ b/Telegram/SourceFiles/payments/ui/payments_panel.h @@ -7,8 +7,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "base/object_ptr.h" + namespace Ui { class SeparatePanel; +class BoxContent; } // namespace Ui namespace Payments::Ui { @@ -17,6 +20,9 @@ using namespace ::Ui; class PanelDelegate; struct Invoice; +struct RequestedInformation; +struct ShippingOptions; +enum class EditField; class Panel final { public: @@ -25,7 +31,18 @@ public: void requestActivate(); - void showForm(const Invoice &invoice); + void showForm( + const Invoice &invoice, + const RequestedInformation ¤t, + const ShippingOptions &options); + void showEditInformation( + const Invoice &invoice, + const RequestedInformation ¤t, + EditField field); + void chooseShippingOption(const ShippingOptions &options); + + void showBox(object_ptr box); + void showToast(const QString &text); private: const not_null _delegate; diff --git a/Telegram/SourceFiles/payments/ui/payments_panel_data.h b/Telegram/SourceFiles/payments/ui/payments_panel_data.h index 7351363500..f282bcbc9d 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel_data.h +++ b/Telegram/SourceFiles/payments/ui/payments_panel_data.h @@ -11,7 +11,7 @@ namespace Payments::Ui { struct LabeledPrice { QString label; - uint64 price = 0; + int64 price = 0; }; struct Invoice { @@ -36,6 +36,17 @@ struct Invoice { } }; +struct ShippingOption { + QString id; + QString title; + std::vector prices; +}; + +struct ShippingOptions { + std::vector list; + QString selectedId; +}; + struct Address { QString address1; QString address2; @@ -52,9 +63,21 @@ struct Address { [[nodiscard]] explicit operator bool() const { return valid(); } + + inline bool operator==(const Address &other) const { + return (address1 == other.address1) + && (address2 == other.address2) + && (city == other.city) + && (state == other.state) + && (countryIso2 == other.countryIso2) + && (postCode == other.postCode); + } + inline bool operator!=(const Address &other) const { + return !(*this == other); + } }; -struct SavedInformation { +struct RequestedInformation { QString name; QString phone; QString email; @@ -69,6 +92,16 @@ struct SavedInformation { [[nodiscard]] explicit operator bool() const { return !empty(); } + + inline bool operator==(const RequestedInformation &other) const { + return (name == other.name) + && (phone == other.phone) + && (email == other.email) + && (shippingAddress == other.shippingAddress); + } + inline bool operator!=(const RequestedInformation &other) const { + return !(*this == other); + } }; struct SavedCredentials { @@ -83,4 +116,11 @@ struct SavedCredentials { } }; +enum class EditField { + ShippingInformation, + Name, + Email, + Phone, +}; + } // namespace Payments::Ui diff --git a/Telegram/SourceFiles/payments/ui/payments_panel_delegate.h b/Telegram/SourceFiles/payments/ui/payments_panel_delegate.h index 49cc5b988a..f737dc2b4b 100644 --- a/Telegram/SourceFiles/payments/ui/payments_panel_delegate.h +++ b/Telegram/SourceFiles/payments/ui/payments_panel_delegate.h @@ -7,11 +7,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "base/object_ptr.h" + class QJsonDocument; class QString; +namespace Ui { +class BoxContent; +} // namespace Ui + namespace Payments::Ui { +using namespace ::Ui; + +struct RequestedInformation; + class PanelDelegate { public: virtual void panelRequestClose() = 0; @@ -19,6 +29,16 @@ public: virtual void panelSubmit() = 0; virtual void panelWebviewMessage(const QJsonDocument &message) = 0; virtual bool panelWebviewNavigationAttempt(const QString &uri) = 0; + + virtual void panelEditShippingInformation() = 0; + virtual void panelEditName() = 0; + virtual void panelEditEmail() = 0; + virtual void panelEditPhone() = 0; + virtual void panelChooseShippingOption() = 0; + virtual void panelChangeShippingOption(const QString &id) = 0; + + virtual void panelValidateInformation(RequestedInformation data) = 0; + virtual void panelShowBox(object_ptr box) = 0; }; } // namespace Payments::Ui diff --git a/Telegram/SourceFiles/settings/settings_calls.cpp b/Telegram/SourceFiles/settings/settings_calls.cpp index 4faea034e0..3a6b277c03 100644 --- a/Telegram/SourceFiles/settings/settings_calls.cpp +++ b/Telegram/SourceFiles/settings/settings_calls.cpp @@ -14,7 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/checkbox.h" #include "ui/widgets/level_meter.h" #include "ui/widgets/buttons.h" -#include "boxes/single_choice_box.h" +#include "ui/boxes/single_choice_box.h" #include "boxes/confirm_box.h" #include "platform/platform_specific.h" #include "main/main_session.h" diff --git a/Telegram/SourceFiles/ui/boxes/country_select_box.cpp b/Telegram/SourceFiles/ui/boxes/country_select_box.cpp new file mode 100644 index 0000000000..33b8139a27 --- /dev/null +++ b/Telegram/SourceFiles/ui/boxes/country_select_box.cpp @@ -0,0 +1,443 @@ +/* +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 "ui/boxes/country_select_box.h" + +#include "lang/lang_keys.h" +#include "ui/widgets/scroll_area.h" +#include "ui/widgets/multi_select.h" +#include "ui/effects/ripple_animation.h" +#include "data/data_countries.h" +#include "base/qt_adapters.h" +#include "styles/style_layers.h" +#include "styles/style_boxes.h" +#include "styles/style_intro.h" + +#include + +namespace Ui { +namespace { + +QString LastValidISO; + +} // namespace + +class CountrySelectBox::Inner : public TWidget { +public: + Inner(QWidget *parent, Type type); + ~Inner(); + + void updateFilter(QString filter = QString()); + + void selectSkip(int32 dir); + void selectSkipPage(int32 h, int32 dir); + + void chooseCountry(); + + void refresh(); + + [[nodiscard]] rpl::producer countryChosen() const { + return _countryChosen.events(); + } + + [[nodiscard]] rpl::producer mustScrollTo() const { + return _mustScrollTo.events(); + } + +protected: + void paintEvent(QPaintEvent *e) override; + void enterEventHook(QEvent *e) override; + void leaveEventHook(QEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; + +private: + void updateSelected() { + updateSelected(mapFromGlobal(QCursor::pos())); + } + void updateSelected(QPoint localPos); + void updateSelectedRow(); + void updateRow(int index); + void setPressed(int pressed); + const std::vector> ¤t() const; + + Type _type = Type::Phones; + int _rowHeight = 0; + + int _selected = -1; + int _pressed = -1; + QString _filter; + bool _mouseSelection = false; + + std::vector> _ripples; + + std::vector> _list; + std::vector> _filtered; + base::flat_map> _byLetter; + std::vector> _namesList; + + rpl::event_stream _countryChosen; + rpl::event_stream _mustScrollTo; + +}; + +CountrySelectBox::CountrySelectBox(QWidget*) +: _select(this, st::defaultMultiSelect, tr::lng_country_ph()) { +} + +CountrySelectBox::CountrySelectBox(QWidget*, const QString &iso, Type type) +: _type(type) +, _select(this, st::defaultMultiSelect, tr::lng_country_ph()) { + if (Data::CountriesByISO2().contains(iso)) { + LastValidISO = iso; + } +} + +rpl::producer CountrySelectBox::countryChosen() const { + return _inner->countryChosen(); +} + +void CountrySelectBox::prepare() { + setTitle(tr::lng_country_select()); + + _select->resizeToWidth(st::boxWidth); + _select->setQueryChangedCallback([=](const QString &query) { + applyFilterUpdate(query); + }); + _select->setSubmittedCallback([=](Qt::KeyboardModifiers) { + submit(); + }); + + _inner = setInnerWidget( + object_ptr(this, _type), + st::countriesScroll, + _select->height()); + + addButton(tr::lng_close(), [=] { closeBox(); }); + + setDimensions(st::boxWidth, st::boxMaxListHeight); + + _inner->mustScrollTo( + ) | rpl::start_with_next([=](ScrollToRequest request) { + onScrollToY(request.ymin, request.ymax); + }, lifetime()); +} + +void CountrySelectBox::submit() { + _inner->chooseCountry(); +} + +void CountrySelectBox::keyPressEvent(QKeyEvent *e) { + if (e->key() == Qt::Key_Down) { + _inner->selectSkip(1); + } else if (e->key() == Qt::Key_Up) { + _inner->selectSkip(-1); + } else if (e->key() == Qt::Key_PageDown) { + _inner->selectSkipPage(height() - _select->height(), 1); + } else if (e->key() == Qt::Key_PageUp) { + _inner->selectSkipPage(height() - _select->height(), -1); + } else { + BoxContent::keyPressEvent(e); + } +} + +void CountrySelectBox::resizeEvent(QResizeEvent *e) { + BoxContent::resizeEvent(e); + + _select->resizeToWidth(width()); + _select->moveToLeft(0, 0); + + _inner->resizeToWidth(width()); +} + +void CountrySelectBox::applyFilterUpdate(const QString &query) { + onScrollToY(0); + _inner->updateFilter(query); +} + +void CountrySelectBox::setInnerFocus() { + _select->setInnerFocus(); +} + +CountrySelectBox::Inner::Inner(QWidget *parent, Type type) +: TWidget(parent) +, _type(type) +, _rowHeight(st::countryRowHeight) { + setAttribute(Qt::WA_OpaquePaintEvent); + + const auto &byISO2 = Data::CountriesByISO2(); + + _list.reserve(byISO2.size()); + _namesList.reserve(byISO2.size()); + + const auto l = byISO2.constFind(LastValidISO); + const auto lastValid = (l != byISO2.cend()) ? (*l) : nullptr; + if (lastValid) { + _list.emplace_back(lastValid); + } + for (const auto &entry : Data::Countries()) { + if (&entry != lastValid) { + _list.emplace_back(&entry); + } + } + auto index = 0; + for (const auto info : _list) { + auto full = QString::fromUtf8(info->name) + + ' ' + + (info->alternativeName + ? QString::fromUtf8(info->alternativeName) + : QString()); + const auto namesList = std::move(full).toLower().split( + QRegularExpression("[\\s\\-]"), + base::QStringSkipEmptyParts); + auto &names = _namesList.emplace_back(); + names.reserve(namesList.size()); + for (const auto &name : namesList) { + const auto part = name.trimmed(); + if (part.isEmpty()) { + continue; + } + + const auto ch = part[0]; + auto &byLetter = _byLetter[ch]; + if (byLetter.empty() || byLetter.back() != index) { + byLetter.push_back(index); + } + names.push_back(part); + } + ++index; + } + + _filter = u"a"_q; + updateFilter(); +} + +void CountrySelectBox::Inner::paintEvent(QPaintEvent *e) { + Painter p(this); + QRect r(e->rect()); + p.setClipRect(r); + + const auto &list = current(); + if (list.empty()) { + p.fillRect(r, st::boxBg); + p.setFont(st::noContactsFont); + p.setPen(st::noContactsColor); + p.drawText(QRect(0, 0, width(), st::noContactsHeight), tr::lng_country_none(tr::now), style::al_center); + return; + } + const auto l = int(list.size()); + if (r.intersects(QRect(0, 0, width(), st::countriesSkip))) { + p.fillRect(r.intersected(QRect(0, 0, width(), st::countriesSkip)), st::countryRowBg); + } + int32 from = std::clamp((r.y() - st::countriesSkip) / _rowHeight, 0, l); + int32 to = std::clamp((r.y() + r.height() - st::countriesSkip + _rowHeight - 1) / _rowHeight, 0, l); + for (int32 i = from; i < to; ++i) { + auto selected = (i == (_pressed >= 0 ? _pressed : _selected)); + auto y = st::countriesSkip + i * _rowHeight; + + p.fillRect(0, y, width(), _rowHeight, selected ? st::countryRowBgOver : st::countryRowBg); + if (_ripples.size() > i && _ripples[i]) { + _ripples[i]->paint(p, 0, y, width()); + if (_ripples[i]->empty()) { + _ripples[i].reset(); + } + } + + auto code = QString("+") + list[i]->code; + auto codeWidth = st::countryRowCodeFont->width(code); + + auto name = QString::fromUtf8(list[i]->name); + auto nameWidth = st::countryRowNameFont->width(name); + auto availWidth = width() - st::countryRowPadding.left() - st::countryRowPadding.right() - codeWidth - st::boxScroll.width; + if (nameWidth > availWidth) { + name = st::countryRowNameFont->elided(name, availWidth); + nameWidth = st::countryRowNameFont->width(name); + } + + p.setFont(st::countryRowNameFont); + p.setPen(st::countryRowNameFg); + p.drawTextLeft(st::countryRowPadding.left(), y + st::countryRowPadding.top(), width(), name); + + if (_type == Type::Phones) { + p.setFont(st::countryRowCodeFont); + p.setPen(selected ? st::countryRowCodeFgOver : st::countryRowCodeFg); + p.drawTextLeft(st::countryRowPadding.left() + nameWidth + st::countryRowPadding.right(), y + st::countryRowPadding.top(), width(), code); + } + } +} + +void CountrySelectBox::Inner::enterEventHook(QEvent *e) { + setMouseTracking(true); +} + +void CountrySelectBox::Inner::leaveEventHook(QEvent *e) { + _mouseSelection = false; + setMouseTracking(false); + if (_selected >= 0) { + updateSelectedRow(); + _selected = -1; + } +} + +void CountrySelectBox::Inner::mouseMoveEvent(QMouseEvent *e) { + _mouseSelection = true; + updateSelected(e->pos()); +} + +void CountrySelectBox::Inner::mousePressEvent(QMouseEvent *e) { + _mouseSelection = true; + updateSelected(e->pos()); + + setPressed(_selected); + const auto &list = current(); + if (_pressed >= 0 && _pressed < list.size()) { + if (_ripples.size() <= _pressed) { + _ripples.reserve(_pressed + 1); + while (_ripples.size() <= _pressed) { + _ripples.push_back(nullptr); + } + } + if (!_ripples[_pressed]) { + auto mask = RippleAnimation::rectMask(QSize(width(), _rowHeight)); + _ripples[_pressed] = std::make_unique(st::countryRipple, std::move(mask), [this, index = _pressed] { + updateRow(index); + }); + _ripples[_pressed]->add(e->pos() - QPoint(0, st::countriesSkip + _pressed * _rowHeight)); + } + } +} + +void CountrySelectBox::Inner::mouseReleaseEvent(QMouseEvent *e) { + auto pressed = _pressed; + setPressed(-1); + updateSelectedRow(); + if (e->button() == Qt::LeftButton) { + if ((pressed >= 0) && pressed == _selected) { + chooseCountry(); + } + } +} + +void CountrySelectBox::Inner::updateFilter(QString filter) { + const auto words = TextUtilities::PrepareSearchWords(filter); + filter = words.isEmpty() ? QString() : words.join(' '); + if (_filter == filter) { + return; + } + _filter = filter; + + const auto findWord = [&]( + const std::vector &names, + const QString &word) { + for (const auto &name : names) { + if (name.startsWith(word)) { + return true; + } + } + return false; + }; + const auto hasAllWords = [&](const std::vector &names) { + for (const auto &word : words) { + if (!findWord(names, word)) { + return false; + } + } + return true; + }; + if (!_filter.isEmpty()) { + _filtered.clear(); + for (const auto index : _byLetter[_filter[0].toLower()]) { + const auto &names = _namesList[index]; + if (hasAllWords(_namesList[index])) { + _filtered.push_back(_list[index]); + } + } + } + refresh(); + _selected = current().empty() ? -1 : 0; + update(); +} + +void CountrySelectBox::Inner::selectSkip(int32 dir) { + _mouseSelection = false; + + const auto &list = current(); + int cur = (_selected >= 0) ? _selected : -1; + cur += dir; + if (cur <= 0) { + _selected = list.empty() ? -1 : 0; + } else if (cur >= list.size()) { + _selected = -1; + } else { + _selected = cur; + } + if (_selected >= 0) { + _mustScrollTo.fire(ScrollToRequest( + st::countriesSkip + _selected * _rowHeight, + st::countriesSkip + (_selected + 1) * _rowHeight)); + } + update(); +} + +void CountrySelectBox::Inner::selectSkipPage(int32 h, int32 dir) { + int32 points = h / _rowHeight; + if (!points) return; + selectSkip(points * dir); +} + +void CountrySelectBox::Inner::chooseCountry() { + const auto &list = current(); + _countryChosen.fire((_selected >= 0 && _selected < list.size()) + ? QString(list[_selected]->iso2) + : QString()); +} + +void CountrySelectBox::Inner::refresh() { + const auto &list = current(); + resize(width(), list.empty() ? st::noContactsHeight : (list.size() * _rowHeight + st::countriesSkip)); +} + +void CountrySelectBox::Inner::updateSelected(QPoint localPos) { + if (!_mouseSelection) return; + + auto in = parentWidget()->rect().contains(parentWidget()->mapFromGlobal(QCursor::pos())); + + const auto &list = current(); + auto selected = (in && localPos.y() >= st::countriesSkip && localPos.y() < st::countriesSkip + list.size() * _rowHeight) ? ((localPos.y() - st::countriesSkip) / _rowHeight) : -1; + if (_selected != selected) { + updateSelectedRow(); + _selected = selected; + updateSelectedRow(); + } +} + +auto CountrySelectBox::Inner::current() const +-> const std::vector> & { + return _filter.isEmpty() ? _list : _filtered; +} + +void CountrySelectBox::Inner::updateSelectedRow() { + updateRow(_selected); +} + +void CountrySelectBox::Inner::updateRow(int index) { + if (index >= 0) { + update(0, st::countriesSkip + index * _rowHeight, width(), _rowHeight); + } +} + +void CountrySelectBox::Inner::setPressed(int pressed) { + if (_pressed >= 0 && _pressed < _ripples.size() && _ripples[_pressed]) { + _ripples[_pressed]->lastStop(); + } + _pressed = pressed; +} + +CountrySelectBox::Inner::~Inner() = default; + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/boxes/country_select_box.h b/Telegram/SourceFiles/ui/boxes/country_select_box.h new file mode 100644 index 0000000000..ba580a3421 --- /dev/null +++ b/Telegram/SourceFiles/ui/boxes/country_select_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 "boxes/abstract_box.h" +#include "styles/style_widgets.h" + +namespace Data { +struct CountryInfo; +} // namespace Data + +namespace Ui { + +class MultiSelect; +class RippleAnimation; + +class CountrySelectBox : public BoxContent { +public: + enum class Type { + Phones, + Countries, + }; + + CountrySelectBox(QWidget*); + CountrySelectBox(QWidget*, const QString &iso, Type type); + + [[nodiscard]] rpl::producer countryChosen() const; + +protected: + void prepare() override; + void setInnerFocus() override; + + void keyPressEvent(QKeyEvent *e) override; + void resizeEvent(QResizeEvent *e) override; + +private: + void submit(); + void applyFilterUpdate(const QString &query); + + Type _type = Type::Phones; + object_ptr _select; + + class Inner; + QPointer _inner; + +}; + +} // namespace Ui diff --git a/Telegram/SourceFiles/boxes/single_choice_box.cpp b/Telegram/SourceFiles/ui/boxes/single_choice_box.cpp similarity index 94% rename from Telegram/SourceFiles/boxes/single_choice_box.cpp rename to Telegram/SourceFiles/ui/boxes/single_choice_box.cpp index af788ecf6e..6b1ea3e5f7 100644 --- a/Telegram/SourceFiles/boxes/single_choice_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/single_choice_box.cpp @@ -5,11 +5,9 @@ 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/single_choice_box.h" +#include "ui/boxes/single_choice_box.h" #include "lang/lang_keys.h" -#include "storage/localstorage.h" -#include "mainwindow.h" #include "ui/widgets/checkbox.h" #include "ui/wrap/vertical_layout.h" #include "ui/wrap/padding_wrap.h" diff --git a/Telegram/SourceFiles/boxes/single_choice_box.h b/Telegram/SourceFiles/ui/boxes/single_choice_box.h similarity index 100% rename from Telegram/SourceFiles/boxes/single_choice_box.h rename to Telegram/SourceFiles/ui/boxes/single_choice_box.h diff --git a/Telegram/SourceFiles/ui/countryinput.cpp b/Telegram/SourceFiles/ui/countryinput.cpp index e06e3fc6e2..f9871a6bda 100644 --- a/Telegram/SourceFiles/ui/countryinput.cpp +++ b/Telegram/SourceFiles/ui/countryinput.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/scroll_area.h" #include "ui/widgets/multi_select.h" #include "ui/effects/ripple_animation.h" +#include "ui/boxes/country_select_box.h" #include "data/data_countries.h" #include "base/qt_adapters.h" #include "styles/style_layers.h" @@ -23,7 +24,8 @@ QString LastValidISO; } // namespace -CountryInput::CountryInput(QWidget *parent, const style::InputField &st) : TWidget(parent) +CountryInput::CountryInput(QWidget *parent, const style::InputField &st) +: RpWidget(parent) , _st(st) , _text(tr::lng_country_code(tr::now)) { resize(_st.width, _st.heightMin); @@ -91,8 +93,11 @@ void CountryInput::mouseMoveEvent(QMouseEvent *e) { void CountryInput::mousePressEvent(QMouseEvent *e) { mouseMoveEvent(e); if (_active) { - auto box = Ui::show(Box()); - connect(box, SIGNAL(countryChosen(const QString&)), this, SLOT(onChooseCountry(const QString&))); + auto box = Ui::show(Box()); + box->countryChosen( + ) | rpl::start_with_next([=](QString iso) { + chooseCountry(iso); + }, lifetime()); } } @@ -125,7 +130,7 @@ void CountryInput::onChooseCode(const QString &code) { update(); } -bool CountryInput::onChooseCountry(const QString &iso) { +bool CountryInput::chooseCountry(const QString &iso) { Ui::hideLayer(); const auto &byISO2 = Data::CountriesByISO2(); @@ -146,349 +151,3 @@ bool CountryInput::onChooseCountry(const QString &iso) { void CountryInput::setText(const QString &newText) { _text = _st.font->elided(newText, width() - _st.textMargins.left() - _st.textMargins.right()); } - -CountrySelectBox::CountrySelectBox(QWidget*) -: _select(this, st::defaultMultiSelect, tr::lng_country_ph()) { -} - -CountrySelectBox::CountrySelectBox(QWidget*, const QString &iso, Type type) -: _type(type) -, _select(this, st::defaultMultiSelect, tr::lng_country_ph()) { - if (Data::CountriesByISO2().contains(iso)) { - LastValidISO = iso; - } -} - -void CountrySelectBox::prepare() { - setTitle(tr::lng_country_select()); - - _select->resizeToWidth(st::boxWidth); - _select->setQueryChangedCallback([=](const QString &query) { - applyFilterUpdate(query); - }); - _select->setSubmittedCallback([=](Qt::KeyboardModifiers) { - submit(); - }); - - _inner = setInnerWidget( - object_ptr(this, _type), - st::countriesScroll, - _select->height()); - - addButton(tr::lng_close(), [=] { closeBox(); }); - - setDimensions(st::boxWidth, st::boxMaxListHeight); - - connect(_inner, SIGNAL(mustScrollTo(int, int)), this, SLOT(onScrollToY(int, int))); - connect(_inner, SIGNAL(countryChosen(const QString&)), this, SIGNAL(countryChosen(const QString&))); -} - -void CountrySelectBox::submit() { - _inner->chooseCountry(); -} - -void CountrySelectBox::keyPressEvent(QKeyEvent *e) { - if (e->key() == Qt::Key_Down) { - _inner->selectSkip(1); - } else if (e->key() == Qt::Key_Up) { - _inner->selectSkip(-1); - } else if (e->key() == Qt::Key_PageDown) { - _inner->selectSkipPage(height() - _select->height(), 1); - } else if (e->key() == Qt::Key_PageUp) { - _inner->selectSkipPage(height() - _select->height(), -1); - } else { - BoxContent::keyPressEvent(e); - } -} - -void CountrySelectBox::resizeEvent(QResizeEvent *e) { - BoxContent::resizeEvent(e); - - _select->resizeToWidth(width()); - _select->moveToLeft(0, 0); - - _inner->resizeToWidth(width()); -} - -void CountrySelectBox::applyFilterUpdate(const QString &query) { - onScrollToY(0); - _inner->updateFilter(query); -} - -void CountrySelectBox::setInnerFocus() { - _select->setInnerFocus(); -} - -CountrySelectBox::Inner::Inner(QWidget *parent, Type type) -: TWidget(parent) -, _type(type) -, _rowHeight(st::countryRowHeight) { - setAttribute(Qt::WA_OpaquePaintEvent); - - const auto &byISO2 = Data::CountriesByISO2(); - - _list.reserve(byISO2.size()); - _namesList.reserve(byISO2.size()); - - const auto l = byISO2.constFind(LastValidISO); - const auto lastValid = (l != byISO2.cend()) ? (*l) : nullptr; - if (lastValid) { - _list.emplace_back(lastValid); - } - for (const auto &entry : Data::Countries()) { - if (&entry != lastValid) { - _list.emplace_back(&entry); - } - } - auto index = 0; - for (const auto info : _list) { - auto full = QString::fromUtf8(info->name) - + ' ' - + (info->alternativeName - ? QString::fromUtf8(info->alternativeName) - : QString()); - const auto namesList = std::move(full).toLower().split( - QRegularExpression("[\\s\\-]"), - base::QStringSkipEmptyParts); - auto &names = _namesList.emplace_back(); - names.reserve(namesList.size()); - for (const auto &name : namesList) { - const auto part = name.trimmed(); - if (part.isEmpty()) { - continue; - } - - const auto ch = part[0]; - auto &byLetter = _byLetter[ch]; - if (byLetter.empty() || byLetter.back() != index) { - byLetter.push_back(index); - } - names.push_back(part); - } - ++index; - } - - _filter = qsl("a"); - updateFilter(); -} - -void CountrySelectBox::Inner::paintEvent(QPaintEvent *e) { - Painter p(this); - QRect r(e->rect()); - p.setClipRect(r); - - const auto &list = current(); - if (list.empty()) { - p.fillRect(r, st::boxBg); - p.setFont(st::noContactsFont); - p.setPen(st::noContactsColor); - p.drawText(QRect(0, 0, width(), st::noContactsHeight), tr::lng_country_none(tr::now), style::al_center); - return; - } - const auto l = list.size(); - if (r.intersects(QRect(0, 0, width(), st::countriesSkip))) { - p.fillRect(r.intersected(QRect(0, 0, width(), st::countriesSkip)), st::countryRowBg); - } - int32 from = floorclamp(r.y() - st::countriesSkip, _rowHeight, 0, l); - int32 to = ceilclamp(r.y() + r.height() - st::countriesSkip, _rowHeight, 0, l); - for (int32 i = from; i < to; ++i) { - auto selected = (i == (_pressed >= 0 ? _pressed : _selected)); - auto y = st::countriesSkip + i * _rowHeight; - - p.fillRect(0, y, width(), _rowHeight, selected ? st::countryRowBgOver : st::countryRowBg); - if (_ripples.size() > i && _ripples[i]) { - _ripples[i]->paint(p, 0, y, width()); - if (_ripples[i]->empty()) { - _ripples[i].reset(); - } - } - - auto code = QString("+") + list[i]->code; - auto codeWidth = st::countryRowCodeFont->width(code); - - auto name = QString::fromUtf8(list[i]->name); - auto nameWidth = st::countryRowNameFont->width(name); - auto availWidth = width() - st::countryRowPadding.left() - st::countryRowPadding.right() - codeWidth - st::boxScroll.width; - if (nameWidth > availWidth) { - name = st::countryRowNameFont->elided(name, availWidth); - nameWidth = st::countryRowNameFont->width(name); - } - - p.setFont(st::countryRowNameFont); - p.setPen(st::countryRowNameFg); - p.drawTextLeft(st::countryRowPadding.left(), y + st::countryRowPadding.top(), width(), name); - - if (_type == Type::Phones) { - p.setFont(st::countryRowCodeFont); - p.setPen(selected ? st::countryRowCodeFgOver : st::countryRowCodeFg); - p.drawTextLeft(st::countryRowPadding.left() + nameWidth + st::countryRowPadding.right(), y + st::countryRowPadding.top(), width(), code); - } - } -} - -void CountrySelectBox::Inner::enterEventHook(QEvent *e) { - setMouseTracking(true); -} - -void CountrySelectBox::Inner::leaveEventHook(QEvent *e) { - _mouseSelection = false; - setMouseTracking(false); - if (_selected >= 0) { - updateSelectedRow(); - _selected = -1; - } -} - -void CountrySelectBox::Inner::mouseMoveEvent(QMouseEvent *e) { - _mouseSelection = true; - updateSelected(e->pos()); -} - -void CountrySelectBox::Inner::mousePressEvent(QMouseEvent *e) { - _mouseSelection = true; - updateSelected(e->pos()); - - setPressed(_selected); - const auto &list = current(); - if (_pressed >= 0 && _pressed < list.size()) { - if (_ripples.size() <= _pressed) { - _ripples.reserve(_pressed + 1); - while (_ripples.size() <= _pressed) { - _ripples.push_back(nullptr); - } - } - if (!_ripples[_pressed]) { - auto mask = Ui::RippleAnimation::rectMask(QSize(width(), _rowHeight)); - _ripples[_pressed] = std::make_unique(st::countryRipple, std::move(mask), [this, index = _pressed] { - updateRow(index); - }); - _ripples[_pressed]->add(e->pos() - QPoint(0, st::countriesSkip + _pressed * _rowHeight)); - } - } -} - -void CountrySelectBox::Inner::mouseReleaseEvent(QMouseEvent *e) { - auto pressed = _pressed; - setPressed(-1); - updateSelectedRow(); - if (e->button() == Qt::LeftButton) { - if ((pressed >= 0) && pressed == _selected) { - chooseCountry(); - } - } -} - -void CountrySelectBox::Inner::updateFilter(QString filter) { - const auto words = TextUtilities::PrepareSearchWords(filter); - filter = words.isEmpty() ? QString() : words.join(' '); - if (_filter == filter) { - return; - } - _filter = filter; - - const auto findWord = [&]( - const std::vector &names, - const QString &word) { - for (const auto &name : names) { - if (name.startsWith(word)) { - return true; - } - } - return false; - }; - const auto hasAllWords = [&](const std::vector &names) { - for (const auto &word : words) { - if (!findWord(names, word)) { - return false; - } - } - return true; - }; - if (!_filter.isEmpty()) { - _filtered.clear(); - for (const auto index : _byLetter[_filter[0].toLower()]) { - const auto &names = _namesList[index]; - if (hasAllWords(_namesList[index])) { - _filtered.push_back(_list[index]); - } - } - } - refresh(); - _selected = current().empty() ? -1 : 0; - update(); -} - -void CountrySelectBox::Inner::selectSkip(int32 dir) { - _mouseSelection = false; - - const auto &list = current(); - int cur = (_selected >= 0) ? _selected : -1; - cur += dir; - if (cur <= 0) { - _selected = list.empty() ? -1 : 0; - } else if (cur >= list.size()) { - _selected = -1; - } else { - _selected = cur; - } - if (_selected >= 0) { - mustScrollTo(st::countriesSkip + _selected * _rowHeight, st::countriesSkip + (_selected + 1) * _rowHeight); - } - update(); -} - -void CountrySelectBox::Inner::selectSkipPage(int32 h, int32 dir) { - int32 points = h / _rowHeight; - if (!points) return; - selectSkip(points * dir); -} - -void CountrySelectBox::Inner::chooseCountry() { - const auto &list = current(); - countryChosen((_selected >= 0 && _selected < list.size()) - ? QString(list[_selected]->iso2) - : QString()); -} - -void CountrySelectBox::Inner::refresh() { - const auto &list = current(); - resize(width(), list.empty() ? st::noContactsHeight : (list.size() * _rowHeight + st::countriesSkip)); -} - -void CountrySelectBox::Inner::updateSelected(QPoint localPos) { - if (!_mouseSelection) return; - - auto in = parentWidget()->rect().contains(parentWidget()->mapFromGlobal(QCursor::pos())); - - const auto &list = current(); - auto selected = (in && localPos.y() >= st::countriesSkip && localPos.y() < st::countriesSkip + list.size() * _rowHeight) ? ((localPos.y() - st::countriesSkip) / _rowHeight) : -1; - if (_selected != selected) { - updateSelectedRow(); - _selected = selected; - updateSelectedRow(); - } -} - -auto CountrySelectBox::Inner::current() const --> const std::vector> & { - return _filter.isEmpty() ? _list : _filtered; -} - -void CountrySelectBox::Inner::updateSelectedRow() { - updateRow(_selected); -} - -void CountrySelectBox::Inner::updateRow(int index) { - if (index >= 0) { - update(0, st::countriesSkip + index * _rowHeight, width(), _rowHeight); - } -} - -void CountrySelectBox::Inner::setPressed(int pressed) { - if (_pressed >= 0 && _pressed < _ripples.size() && _ripples[_pressed]) { - _ripples[_pressed]->lastStop(); - } - _pressed = pressed; -} - -CountrySelectBox::Inner::~Inner() = default; diff --git a/Telegram/SourceFiles/ui/countryinput.h b/Telegram/SourceFiles/ui/countryinput.h index ffd1a707ec..7b0050bc03 100644 --- a/Telegram/SourceFiles/ui/countryinput.h +++ b/Telegram/SourceFiles/ui/countryinput.h @@ -7,7 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -#include "boxes/abstract_box.h" +#include "ui/rp_widget.h" #include "styles/style_widgets.h" namespace Data { @@ -19,19 +19,19 @@ class MultiSelect; class RippleAnimation; } // namespace Ui -class CountryInput : public TWidget { +class CountryInput : public Ui::RpWidget { Q_OBJECT public: CountryInput(QWidget *parent, const style::InputField &st); - QString iso() const { + [[nodiscard]] QString iso() const { return _chosenIso; } + bool chooseCountry(const QString &country); public Q_SLOTS: void onChooseCode(const QString &code); - bool onChooseCountry(const QString &country); Q_SIGNALS: void codeChanged(const QString &code); @@ -53,94 +53,3 @@ private: QPainterPath _placeholderPath; }; - -class CountrySelectBox : public Ui::BoxContent { - Q_OBJECT - -public: - enum class Type { - Phones, - Countries, - }; - - CountrySelectBox(QWidget*); - CountrySelectBox(QWidget*, const QString &iso, Type type); - -Q_SIGNALS: - void countryChosen(const QString &iso); - -protected: - void prepare() override; - void setInnerFocus() override; - - void keyPressEvent(QKeyEvent *e) override; - void resizeEvent(QResizeEvent *e) override; - -private: - void submit(); - void applyFilterUpdate(const QString &query); - - Type _type = Type::Phones; - object_ptr _select; - - class Inner; - QPointer _inner; - -}; - -// This class is hold in header because it requires Qt preprocessing. -class CountrySelectBox::Inner : public TWidget { - Q_OBJECT - -public: - Inner(QWidget *parent, Type type); - - void updateFilter(QString filter = QString()); - - void selectSkip(int32 dir); - void selectSkipPage(int32 h, int32 dir); - - void chooseCountry(); - - void refresh(); - - ~Inner(); - -Q_SIGNALS: - void countryChosen(const QString &iso); - void mustScrollTo(int ymin, int ymax); - -protected: - void paintEvent(QPaintEvent *e) override; - void enterEventHook(QEvent *e) override; - void leaveEventHook(QEvent *e) override; - void mouseMoveEvent(QMouseEvent *e) override; - void mousePressEvent(QMouseEvent *e) override; - void mouseReleaseEvent(QMouseEvent *e) override; - -private: - void updateSelected() { - updateSelected(mapFromGlobal(QCursor::pos())); - } - void updateSelected(QPoint localPos); - void updateSelectedRow(); - void updateRow(int index); - void setPressed(int pressed); - const std::vector> ¤t() const; - - Type _type = Type::Phones; - int _rowHeight = 0; - - int _selected = -1; - int _pressed = -1; - QString _filter; - bool _mouseSelection = false; - - std::vector> _ripples; - - std::vector> _list; - std::vector> _filtered; - base::flat_map> _byLetter; - std::vector> _namesList; - -}; diff --git a/Telegram/SourceFiles/ui/text/format_values.cpp b/Telegram/SourceFiles/ui/text/format_values.cpp index 9cb1e10ba0..b614fe14cf 100644 --- a/Telegram/SourceFiles/ui/text/format_values.cpp +++ b/Telegram/SourceFiles/ui/text/format_values.cpp @@ -125,7 +125,7 @@ QString FormatPlayedText(qint64 played, qint64 duration) { return tr::lng_duration_played(tr::now, lt_played, FormatDurationText(played), lt_duration, FormatDurationText(duration)); } -QString FillAmountAndCurrency(uint64 amount, const QString ¤cy) { +QString FillAmountAndCurrency(int64 amount, const QString ¤cy) { static const auto ShortCurrencyNames = QMap{ { u"USD"_q, QString::fromUtf8("\x24") }, { u"GBP"_q, QString::fromUtf8("\xC2\xA3") }, diff --git a/Telegram/SourceFiles/ui/text/format_values.h b/Telegram/SourceFiles/ui/text/format_values.h index 15f9b02114..779d3e7ce6 100644 --- a/Telegram/SourceFiles/ui/text/format_values.h +++ b/Telegram/SourceFiles/ui/text/format_values.h @@ -24,7 +24,7 @@ inline constexpr auto FileStatusSizeFailed = 0x7FFFFFF2; [[nodiscard]] QString FormatPlayedText(qint64 played, qint64 duration); [[nodiscard]] QString FillAmountAndCurrency( - uint64 amount, + int64 amount, const QString ¤cy); [[nodiscard]] QString ComposeNameString( diff --git a/Telegram/SourceFiles/ui/widgets/multi_select.h b/Telegram/SourceFiles/ui/widgets/multi_select.h index 679a4c8f43..0a0e486803 100644 --- a/Telegram/SourceFiles/ui/widgets/multi_select.h +++ b/Telegram/SourceFiles/ui/widgets/multi_select.h @@ -12,6 +12,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/animations.h" #include "base/object_ptr.h" +#include + namespace Ui { class InputField; diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 57338bf699..de30444876 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -52,6 +52,9 @@ PRIVATE core/mime_type.cpp core/mime_type.h + data/data_countries.cpp + data/data_countries.h + media/clip/media_clip_check_streaming.cpp media/clip/media_clip_check_streaming.h media/clip/media_clip_ffmpeg.cpp @@ -61,6 +64,13 @@ PRIVATE media/clip/media_clip_reader.cpp media/clip/media_clip_reader.h + passport/ui/passport_details_row.cpp + passport/ui/passport_details_row.h + passport/ui/passport_form_row.cpp + passport/ui/passport_form_row.h + + payments/ui/payments_edit_information.cpp + payments/ui/payments_edit_information.h payments/ui/payments_form_summary.cpp payments/ui/payments_form_summary.h payments/ui/payments_panel.cpp @@ -80,10 +90,14 @@ PRIVATE ui/boxes/calendar_box.h ui/boxes/choose_date_time.cpp ui/boxes/choose_date_time.h + ui/boxes/country_select_box.cpp + ui/boxes/country_select_box.h ui/boxes/edit_invite_link.cpp ui/boxes/edit_invite_link.h ui/boxes/report_box.cpp ui/boxes/report_box.h + ui/boxes/single_choice_box.cpp + ui/boxes/single_choice_box.h ui/chat/attach/attach_album_thumbnail.cpp ui/chat/attach/attach_album_thumbnail.h ui/chat/attach/attach_album_preview.cpp