diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 95995112a..e65061429 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1583,6 +1583,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_passport_confirm_phone" = "We've sent an SMS with a confirmation code to your phone {phone}."; "lng_passport_confirm_email" = "We've sent a confirmation code to your email {email}."; "lng_passport_sure_cancel" = "If you continue your changes will be lost."; +"lng_passport_scans_limit_reached" = "Scans limit reached."; // Wnd specific diff --git a/Telegram/SourceFiles/info/profile/info_profile_button.cpp b/Telegram/SourceFiles/info/profile/info_profile_button.cpp index 0e3dc71c6..8258a97a4 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_button.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_button.cpp @@ -58,6 +58,11 @@ rpl::producer Button::toggledValue() const { return rpl::never(); } +void Button::setColorOverride(base::optional textColorOverride) { + _textColorOverride = textColorOverride; + update(); +} + void Button::paintEvent(QPaintEvent *e) { Painter p(this); @@ -69,7 +74,11 @@ void Button::paintEvent(QPaintEvent *e) { auto outerw = width(); p.setFont(_st.font); - p.setPen(paintOver ? _st.textFgOver : _st.textFg); + p.setPen(_textColorOverride + ? QPen(*_textColorOverride) + : paintOver + ? _st.textFgOver + : _st.textFg); p.drawTextLeft( _st.padding.left(), _st.padding.top(), diff --git a/Telegram/SourceFiles/info/profile/info_profile_button.h b/Telegram/SourceFiles/info/profile/info_profile_button.h index 44bb2bb5b..6bf04af40 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_button.h +++ b/Telegram/SourceFiles/info/profile/info_profile_button.h @@ -29,6 +29,8 @@ public: Button *toggleOn(rpl::producer &&toggled); rpl::producer toggledValue() const; + void setColorOverride(base::optional textColorOverride); + protected: int resizeGetHeight(int newWidth) override; void onStateChanged( @@ -48,6 +50,7 @@ private: int _originalWidth = 0; int _textWidth = 0; std::unique_ptr _toggle; + base::optional _textColorOverride; }; diff --git a/Telegram/SourceFiles/passport/passport_form_controller.cpp b/Telegram/SourceFiles/passport/passport_form_controller.cpp index f10047fb1..c55c74c3c 100644 --- a/Telegram/SourceFiles/passport/passport_form_controller.cpp +++ b/Telegram/SourceFiles/passport/passport_form_controller.cpp @@ -25,6 +25,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Passport { namespace { +constexpr auto kDocumentScansLimit = 20; + QImage ReadImage(bytes::const_span buffer) { return App::readImage(QByteArray::fromRawData( reinterpret_cast(buffer.data()), @@ -241,11 +243,14 @@ auto FormController::prepareFinalData() const -> FinalData { addValueToJSON(key, value); } }; + auto hasErrors = false; const auto scopes = ComputeScopes(this); for (const auto &scope : scopes) { const auto ready = ComputeScopeRowReadyString(scope); if (ready.isEmpty()) { + hasErrors = true; _valueError.fire_copy(scope.fields); + continue; } addValue(scope.fields); if (!scope.documents.empty()) { @@ -257,6 +262,9 @@ auto FormController::prepareFinalData() const -> FinalData { } } } + if (hasErrors) { + return {}; + } auto json = QJsonObject(); json.insert("secure_data", secureData); @@ -274,6 +282,9 @@ void FormController::submit() { } const auto prepared = prepareFinalData(); + if (prepared.hashes.empty()) { + return; + } const auto credentialsEncryptedData = EncryptData( bytes::make_span(prepared.credentials)); const auto credentialsEncryptedSecret = EncryptCredentialsSecret( @@ -433,6 +444,10 @@ QString FormController::passwordHint() const { void FormController::uploadScan( not_null value, QByteArray &&content) { + if (!canAddScan(value)) { + _view->showToast(lang(lng_passport_scans_limit_reached)); + return; + } const auto nonconst = findValue(value); auto scanIndex = int(nonconst->scansInEdit.size()); nonconst->scansInEdit.emplace_back( @@ -538,6 +553,12 @@ void FormController::scanDeleteRestore( const auto nonconst = findValue(value); auto &scan = nonconst->scansInEdit[scanIndex]; + if (scan.deleted && !deleted) { + if (!canAddScan(value)) { + _view->showToast(lang(lng_passport_scans_limit_reached)); + return; + } + } scan.deleted = deleted; _scanUpdated.fire(&scan); } @@ -553,6 +574,13 @@ void FormController::selfieDeleteRestore( _scanUpdated.fire(&scan); } +bool FormController::canAddScan(not_null value) const { + const auto scansCount = ranges::count_if( + value->scansInEdit, + [](const EditFile &scan) { return !scan.deleted; }); + return (scansCount < kDocumentScansLimit); +} + void FormController::subscribeToUploader() { if (_uploaderSubscriptions) { return; diff --git a/Telegram/SourceFiles/passport/passport_form_controller.h b/Telegram/SourceFiles/passport/passport_form_controller.h index cbe79dd98..9f77d6cd6 100644 --- a/Telegram/SourceFiles/passport/passport_form_controller.h +++ b/Telegram/SourceFiles/passport/passport_form_controller.h @@ -212,6 +212,7 @@ public: rpl::producer passwordError() const; QString passwordHint() const; + bool canAddScan(not_null value) const; void uploadScan(not_null value, QByteArray &&content); void deleteScan(not_null value, int fileIndex); void restoreScan(not_null value, int fileIndex); diff --git a/Telegram/SourceFiles/passport/passport_form_view_controller.h b/Telegram/SourceFiles/passport/passport_form_view_controller.h index e5f8a18fc..ac3e9398d 100644 --- a/Telegram/SourceFiles/passport/passport_form_view_controller.h +++ b/Telegram/SourceFiles/passport/passport_form_view_controller.h @@ -45,6 +45,7 @@ public: virtual void editScope(int index) = 0; virtual void showBox(object_ptr box) = 0; + virtual void showToast(const QString &text) = 0; virtual ~ViewController() { } diff --git a/Telegram/SourceFiles/passport/passport_panel_controller.cpp b/Telegram/SourceFiles/passport/passport_panel_controller.cpp index f879a5353..f6def4ced 100644 --- a/Telegram/SourceFiles/passport/passport_panel_controller.cpp +++ b/Telegram/SourceFiles/passport/passport_panel_controller.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "passport/passport_panel_edit_scans.h" #include "passport/passport_panel.h" #include "boxes/confirm_box.h" +#include "ui/toast/toast.h" #include "ui/countryinput.h" #include "layout.h" @@ -363,6 +364,14 @@ QString PanelController::defaultPhoneNumber() const { return _form->defaultPhoneNumber(); } +bool PanelController::canAddScan() const { + Expects(_editScope != nullptr); + Expects(_editDocumentIndex >= 0 + && _editDocumentIndex < _editScope->documents.size()); + + return _form->canAddScan(_editScope->documents[_editDocumentIndex]); +} + void PanelController::uploadScan(QByteArray &&content) { Expects(_editScope != nullptr); Expects(_editDocumentIndex >= 0 @@ -858,6 +867,14 @@ void PanelController::showBox(object_ptr box) { _panel->showBox(std::move(box)); } +void PanelController::showToast(const QString &text) { + Expects(_panel != nullptr); + + auto toast = Ui::Toast::Config(); + toast.text = text; + Ui::Toast::Show(_panel.get(), toast); +} + rpl::lifetime &PanelController::lifetime() { return _lifetime; } diff --git a/Telegram/SourceFiles/passport/passport_panel_controller.h b/Telegram/SourceFiles/passport/passport_panel_controller.h index b03c64284..bdbd94b3b 100644 --- a/Telegram/SourceFiles/passport/passport_panel_controller.h +++ b/Telegram/SourceFiles/passport/passport_panel_controller.h @@ -60,6 +60,7 @@ public: rpl::producer passwordError() const; QString passwordHint() const; + bool canAddScan() const; void uploadScan(QByteArray &&content); void deleteScan(int fileIndex); void restoreScan(int fileIndex); @@ -89,6 +90,7 @@ public: void cancelEditScope(); void showBox(object_ptr box) override; + void showToast(const QString &text) override; void cancelAuth(); diff --git a/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp b/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp index 47e888552..990d3c094 100644 --- a/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp +++ b/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp @@ -300,6 +300,13 @@ PanelEditDocument::Result PanelEditDocument::collect() const { } bool PanelEditDocument::validate() { + if (const auto error = _editScans->validateGetErrorTop()) { + const auto errortop = _editScans->mapToGlobal(QPoint(0, *error)); + const auto scrolltop = _scroll->mapToGlobal(QPoint(0, 0)); + const auto scrolldelta = errortop.y() - scrolltop.y(); + _scroll->scrollToY(_scroll->scrollTop() + scrolldelta); + return false; + } auto first = QPointer(); for (const auto [i, field] : base::reversed(_details)) { const auto &row = _scheme.rows[i]; diff --git a/Telegram/SourceFiles/passport/passport_panel_edit_scans.cpp b/Telegram/SourceFiles/passport/passport_panel_edit_scans.cpp index c046d77ff..679b6ef01 100644 --- a/Telegram/SourceFiles/passport/passport_panel_edit_scans.cpp +++ b/Telegram/SourceFiles/passport/passport_panel_edit_scans.cpp @@ -196,6 +196,22 @@ EditScans::EditScans( setupContent(header); } +base::optional EditScans::validateGetErrorTop() { + const auto exists = ranges::find( + _files, + false, + [](const ScanInfo &file) { return file.deleted; }) != end(_files); + if (!exists) { + toggleError(true); + return (_files.size() > 5) ? _upload->y() : _header->y(); + } + if (_selfie && (!_selfie->key.id || _selfie->deleted)) { + toggleSelfieError(true); + return _selfieHeader->y(); + } + return base::none; +} + void EditScans::setupContent(const QString &header) { const auto inner = _content.data(); inner->move(0, 0); @@ -307,6 +323,9 @@ void EditScans::updateScan(ScanInfo &&info) { Assert(_selfie != nullptr); if (_selfie->key.id) { updateRow(_selfieRow->entity(), info); + if (!info.deleted) { + hideSelfieError(); + } } else { createSelfieRow(info); _selfieWrap->resizeToWidth(width()); @@ -325,6 +344,9 @@ void EditScans::updateScan(ScanInfo &&info) { scan->setStatus(i->status); scan->setImage(i->thumb); scan->setDeleted(i->deleted); + if (!i->deleted) { + hideError(); + } } else { _files.push_back(std::move(info)); pushScan(_files.back()); @@ -352,6 +374,8 @@ void EditScans::createSelfieRow(const ScanInfo &info) { ) | rpl::start_with_next([=] { _controller->restoreSelfie(); }, row->lifetime()); + + hideSelfieError(); } void EditScans::pushScan(const ScanInfo &info) { @@ -373,6 +397,8 @@ void EditScans::pushScan(const ScanInfo &info) { ) | rpl::start_with_next([=] { _controller->restoreScan(index); }, scan->lifetime()); + + hideError(); } base::unique_qptr> EditScans::createScan( @@ -393,6 +419,10 @@ base::unique_qptr> EditScans::createScan( } void EditScans::chooseScan() { + if (!_controller->canAddScan()) { + _controller->showToast(lang(lng_passport_scans_limit_reached)); + return; + } ChooseScan(base::lambda_guarded(this, [=](QByteArray &&content) { _controller->uploadScan(std::move(content)); })); @@ -437,4 +467,59 @@ rpl::producer EditScans::uploadButtonText() const { : lng_passport_upload_more) | Info::Profile::ToUpperValue(); } +void EditScans::hideError() { + toggleError(false); +} + +void EditScans::toggleError(bool shown) { + if (_errorShown != shown) { + _errorShown = shown; + _errorAnimation.start( + [=] { errorAnimationCallback(); }, + _errorShown ? 0. : 1., + _errorShown ? 1. : 0., + st::passportDetailsField.duration); + } +} + +void EditScans::errorAnimationCallback() { + const auto error = _errorAnimation.current(_errorShown ? 1. : 0.); + if (error == 0.) { + _upload->setColorOverride(base::none); + } else { + _upload->setColorOverride(anim::color( + st::passportUploadButton.textFg, + st::boxTextFgError, + error)); + } +} + +void EditScans::hideSelfieError() { + toggleSelfieError(false); +} + +void EditScans::toggleSelfieError(bool shown) { + if (_selfieErrorShown != shown) { + _selfieErrorShown = shown; + _selfieErrorAnimation.start( + [=] { selfieErrorAnimationCallback(); }, + _selfieErrorShown ? 0. : 1., + _selfieErrorShown ? 1. : 0., + st::passportDetailsField.duration); + } +} + +void EditScans::selfieErrorAnimationCallback() { + const auto error = _selfieErrorAnimation.current( + _selfieErrorShown ? 1. : 0.); + if (error == 0.) { + _selfieUpload->setColorOverride(base::none); + } else { + _selfieUpload->setColorOverride(anim::color( + st::passportUploadButton.textFg, + st::boxTextFgError, + error)); + } +} + } // namespace Passport diff --git a/Telegram/SourceFiles/passport/passport_panel_edit_scans.h b/Telegram/SourceFiles/passport/passport_panel_edit_scans.h index f11161b93..e9da863ee 100644 --- a/Telegram/SourceFiles/passport/passport_panel_edit_scans.h +++ b/Telegram/SourceFiles/passport/passport_panel_edit_scans.h @@ -39,6 +39,8 @@ public: std::vector &&files, std::unique_ptr &&selfie); + base::optional validateGetErrorTop(); + static void ChooseScan(base::lambda callback); private: @@ -55,6 +57,14 @@ private: rpl::producer uploadButtonText() const; + void toggleError(bool shown); + void hideError(); + void errorAnimationCallback(); + + void toggleSelfieError(bool shown); + void hideSelfieError(); + void selfieErrorAnimationCallback(); + not_null _controller; std::vector _files; std::unique_ptr _selfie; @@ -66,11 +76,15 @@ private: std::vector>> _rows; QPointer _upload; rpl::event_stream> _uploadTexts; + bool _errorShown = false; + Animation _errorAnimation; QPointer> _selfieHeader; QPointer _selfieWrap; base::unique_qptr> _selfieRow; QPointer _selfieUpload; + bool _selfieErrorShown = false; + Animation _selfieErrorAnimation; };