From 22bdf158252eaae58831fc7a3ae803865a04f8d1 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 17 Apr 2018 21:54:52 +0400 Subject: [PATCH] Display scope errors in passport. --- Telegram/SourceFiles/messenger.cpp | 4 +- Telegram/SourceFiles/passport/passport.style | 1 + .../passport/passport_encryption.cpp | 72 +++++++++++++++++ .../passport/passport_encryption.h | 11 +++ .../passport/passport_form_controller.cpp | 51 ++++++++++-- .../passport/passport_form_controller.h | 5 +- .../passport_form_view_controller.cpp | 30 +++++-- .../passport/passport_panel_controller.cpp | 3 +- .../passport/passport_panel_details_row.cpp | 16 +++- .../passport/passport_panel_details_row.h | 1 + .../passport/passport_panel_edit_document.cpp | 23 ++++-- .../passport/passport_panel_edit_document.h | 3 + .../passport/passport_panel_edit_scans.cpp | 78 ++++++++++++++++--- .../passport/passport_panel_edit_scans.h | 5 ++ .../passport/passport_panel_form.cpp | 9 ++- 15 files changed, 274 insertions(+), 38 deletions(-) diff --git a/Telegram/SourceFiles/messenger.cpp b/Telegram/SourceFiles/messenger.cpp index 7001b2390..561203bfd 100644 --- a/Telegram/SourceFiles/messenger.cpp +++ b/Telegram/SourceFiles/messenger.cpp @@ -844,6 +844,7 @@ bool Messenger::openLocalUrl(const QString &url) { const auto callback = params.value("callback_url", QString()); const auto publicKey = params.value("public_key", QString()); const auto payload = params.value("payload", QString()); + const auto errors = params.value("errors", QString()); if (const auto window = App::wnd()) { if (const auto controller = window->controller()) { controller->showPassportForm(Passport::FormRequest( @@ -851,7 +852,8 @@ bool Messenger::openLocalUrl(const QString &url) { scope, callback, publicKey, - payload)); + payload, + errors)); return true; } } diff --git a/Telegram/SourceFiles/passport/passport.style b/Telegram/SourceFiles/passport/passport.style index 9fa1d40ce..625a01ed7 100644 --- a/Telegram/SourceFiles/passport/passport.style +++ b/Telegram/SourceFiles/passport/passport.style @@ -172,6 +172,7 @@ passportUploadButton: InfoProfileButton { } passportUploadButtonPadding: margins(0px, 10px, 0px, 10px); passportUploadHeaderPadding: margins(22px, 14px, 22px, 3px); +passportUploadErrorPadding: margins(22px, 5px, 22px, 5px); passportDeleteButton: InfoProfileButton(passportUploadButton) { textFg: attentionButtonFg; textFgOver: attentionButtonFgOver; diff --git a/Telegram/SourceFiles/passport/passport_encryption.cpp b/Telegram/SourceFiles/passport/passport_encryption.cpp index 8dc17cade..a1e4b5383 100644 --- a/Telegram/SourceFiles/passport/passport_encryption.cpp +++ b/Telegram/SourceFiles/passport/passport_encryption.cpp @@ -238,6 +238,78 @@ std::map DeserializeData(bytes::const_span bytes) { return result; } +std::vector DeserializeErrors(bytes::const_span json) { + const auto serialized = QByteArray::fromRawData( + reinterpret_cast(json.data()), + json.size()); + auto error = QJsonParseError(); + auto document = QJsonDocument::fromJson(serialized, &error); + if (error.error != QJsonParseError::NoError) { + LOG(("API Error: Could not deserialize errors JSON, error %1" + ).arg(error.errorString())); + return {}; + } else if (!document.isArray()) { + LOG(("API Error: Errors JSON root is not an array.")); + return {}; + } + auto array = document.array(); + auto result = std::vector(); + for (const auto &error : array) { + if (!error.isObject()) { + LOG(("API Error: Not an object inside errors JSON.")); + continue; + } + auto fields = error.toObject(); + const auto typeIt = fields.constFind("type"); + if (typeIt == fields.constEnd()) { + LOG(("API Error: type was not found in an error.")); + continue; + } else if (!typeIt->isString()) { + LOG(("API Error: type was not a string in an error.")); + continue; + } + const auto descriptionIt = fields.constFind("description"); + if (descriptionIt == fields.constEnd()) { + LOG(("API Error: description was not found in an error.")); + continue; + } else if (!typeIt->isString()) { + LOG(("API Error: description was not a string in an error.")); + continue; + } + const auto targetIt = fields.constFind("target"); + if (targetIt == fields.constEnd()) { + LOG(("API Error: target aws not found in an error.")); + continue; + } else if (!targetIt->isString()) { + LOG(("API Error: target was not as string in an error.")); + continue; + } + auto next = DataError(); + next.type = typeIt->toString(); + next.text = descriptionIt->toString(); + const auto fieldIt = fields.constFind("field"); + const auto fileHashIt = fields.constFind("file_hash"); + if (fieldIt != fields.constEnd()) { + if (!fieldIt->isString()) { + LOG(("API Error: field was not a string in an error.")); + continue; + } + next.key = fieldIt->toString(); + } else if (fileHashIt != fields.constEnd()) { + if (!fileHashIt->isString()) { + LOG(("API Error: file_hash was not a string in an error.")); + continue; + } + next.key = QByteArray::fromBase64( + fileHashIt->toString().toUtf8()); + } else if (targetIt->toString() == "selfie") { + next.key = QByteArray(); + } + result.push_back(std::move(next)); + } + return result; +} + EncryptedData EncryptData(bytes::const_span bytes) { return EncryptData(bytes, GenerateSecretBytes()); } diff --git a/Telegram/SourceFiles/passport/passport_encryption.h b/Telegram/SourceFiles/passport/passport_encryption.h index ee758a029..71443ccde 100644 --- a/Telegram/SourceFiles/passport/passport_encryption.h +++ b/Telegram/SourceFiles/passport/passport_encryption.h @@ -23,6 +23,17 @@ bytes::vector DecryptSecureSecret( bytes::vector SerializeData(const std::map &data); std::map DeserializeData(bytes::const_span bytes); +struct DataError { + // QByteArray - bad existing scan with such file_hash + // QString - bad data field value with such key + // base::none - additional scan required + base::optional_variant key; + QString type; // personal_details, passport, etc. + QString text; + +}; +std::vector DeserializeErrors(bytes::const_span json); + struct EncryptedData { bytes::vector secret; bytes::vector hash; diff --git a/Telegram/SourceFiles/passport/passport_form_controller.cpp b/Telegram/SourceFiles/passport/passport_form_controller.cpp index b17f5a1e2..8ae70592d 100644 --- a/Telegram/SourceFiles/passport/passport_form_controller.cpp +++ b/Telegram/SourceFiles/passport/passport_form_controller.cpp @@ -156,12 +156,14 @@ FormRequest::FormRequest( const QString &scope, const QString &callbackUrl, const QString &publicKey, - const QString &payload) + const QString &payload, + const QString &errors) : botId(botId) , scope(scope) , callbackUrl(callbackUrl) , publicKey(publicKey) -, payload(payload) { +, payload(payload) +, errors(errors) { } EditFile::EditFile( @@ -247,8 +249,8 @@ auto FormController::prepareFinalData() -> FinalData { auto hashes = QVector(); auto secureData = QJsonObject(); const auto addValueToJSON = [&]( - const QString &key, - not_null value) { + const QString &key, + not_null value) { auto object = QJsonObject(); if (!value->data.parsed.fields.empty()) { object.insert("data", GetJSONFromMap({ @@ -279,8 +281,8 @@ auto FormController::prepareFinalData() -> FinalData { }; const auto scopes = ComputeScopes(this); for (const auto &scope : scopes) { - const auto ready = ComputeScopeRowReadyString(scope); - if (ready.isEmpty()) { + const auto row = ComputeScopeRow(scope); + if (row.ready.isEmpty() || !row.error.isEmpty()) { errors.push_back(scope.fields); continue; } @@ -489,6 +491,43 @@ void FormController::decryptValues() { for (auto &[type, value] : _form.values) { decryptValue(value); } + fillErrors(); +} + +void FormController::fillErrors() { + const auto errors = _request.errors.toUtf8(); + const auto list = DeserializeErrors(bytes::make_span(errors)); + for (const auto &error : list) { + for (auto &[type, value] : _form.values) { + if (ValueCredentialsKey(type) != error.type) { + continue; + } + if (!error.key.has_value()) { + value.scanMissingError = error.text; + } else if (const auto key = base::get_if(&error.key)) { + value.data.parsed.fields[(*key)].error = error.text; + } else if (auto hash = base::get_if(&error.key)) { + const auto check = [&](const File &file) { + return *hash == QByteArray::fromRawData( + reinterpret_cast(file.hash.data()), + file.hash.size()); + }; + for (auto &scan : value.scans) { + if (check(scan)) { + scan.error = error.text; + break; + } + } + if (value.selfie) { + if (check(*value.selfie) || hash->isEmpty()) { + value.selfie->error = error.text; + } + } + } + break; + } + + } } void FormController::decryptValue(Value &value) { diff --git a/Telegram/SourceFiles/passport/passport_form_controller.h b/Telegram/SourceFiles/passport/passport_form_controller.h index 09eb415e9..05b2df7d6 100644 --- a/Telegram/SourceFiles/passport/passport_form_controller.h +++ b/Telegram/SourceFiles/passport/passport_form_controller.h @@ -32,13 +32,15 @@ struct FormRequest { const QString &scope, const QString &callbackUrl, const QString &publicKey, - const QString &payload); + const QString &payload, + const QString &errors); UserId botId; QString scope; QString callbackUrl; QString publicKey; QString payload; + QString errors; }; @@ -317,6 +319,7 @@ private: void decryptValue(Value &value); bool validateValueSecrets(Value &value); void resetValue(Value &value); + void fillErrors(); void loadFile(File &file); void fileLoadDone(FileKey key, const QByteArray &bytes); diff --git a/Telegram/SourceFiles/passport/passport_form_view_controller.cpp b/Telegram/SourceFiles/passport/passport_form_view_controller.cpp index 83187c167..9ea0ba8d6 100644 --- a/Telegram/SourceFiles/passport/passport_form_view_controller.cpp +++ b/Telegram/SourceFiles/passport/passport_form_view_controller.cpp @@ -188,14 +188,28 @@ ScopeRow ComputeScopeRow(const Scope &scope) { const auto addReadyError = [&](ScopeRow &&row) { const auto ready = ComputeScopeRowReadyString(scope); row.ready = ready; - // #TODO passport bot errors - //row.error = scope.fields->error.has_value() - // ? (!scope.fields->error->isEmpty() - // ? *scope.fields->error - // : !ready.isEmpty() - // ? ready - // : row.description) - // : QString(); + auto errors = QStringList(); + const auto addValueErrors = [&](not_null value) { + for (const auto &scan : value->scans) { + if (!scan.error.isEmpty()) { + errors.push_back(scan.error); + } + } + if (value->selfie && !value->selfie->error.isEmpty()) { + errors.push_back(value->selfie->error); + } + if (!value->scanMissingError.isEmpty()) { + errors.push_back(value->scanMissingError); + } + for (const auto &[key, value] : value->data.parsed.fields) { + if (!value.error.isEmpty()) { + errors.push_back(value.error); + } + } + }; + ranges::for_each(scope.documents, addValueErrors); + addValueErrors(scope.fields); + row.error = errors.join('\n'); return row; }; switch (scope.type) { diff --git a/Telegram/SourceFiles/passport/passport_panel_controller.cpp b/Telegram/SourceFiles/passport/passport_panel_controller.cpp index ee468839b..f32011a72 100644 --- a/Telegram/SourceFiles/passport/passport_panel_controller.cpp +++ b/Telegram/SourceFiles/passport/passport_panel_controller.cpp @@ -565,7 +565,7 @@ ScanInfo PanelController::collectScanInfo(const EditFile &file) const { && (&file == &*_editDocument->selfieInEdit); return { FileKey{ file.fields.id, file.fields.dcId }, - status, + !file.fields.error.isEmpty() ? file.fields.error : status, file.fields.image, file.deleted, isSelfie, @@ -871,6 +871,7 @@ void PanelController::editScope(int index, int documentIndex) { _editDocument->type), _editValue->data.parsedInEdit, _editDocument->data.parsedInEdit, + _editDocument->scanMissingError, valueFiles(*_editDocument), (_editScope->selfieRequired ? valueSelfie(*_editDocument) diff --git a/Telegram/SourceFiles/passport/passport_panel_details_row.cpp b/Telegram/SourceFiles/passport/passport_panel_details_row.cpp index 5fc447ec5..6db34120c 100644 --- a/Telegram/SourceFiles/passport/passport_panel_details_row.cpp +++ b/Telegram/SourceFiles/passport/passport_panel_details_row.cpp @@ -930,10 +930,14 @@ int PanelDetailsRow::resizeGetHeight(int newWidth) { const auto inputRight = padding.right(); const auto inputWidth = std::max(newWidth - inputLeft - inputRight, 0); const auto innerHeight = resizeInner(inputLeft, inputTop, inputWidth); - return padding.top() + const auto result = padding.top() + innerHeight + (_error ? _error->height() : 0) + padding.bottom(); + if (_error) { + _error->moveToLeft(inputLeft, result - _error->height()); + } + return result; } void PanelDetailsRow::showError(const QString &error) { @@ -959,12 +963,18 @@ void PanelDetailsRow::showError(const QString &error) { } else { _error->entity()->setText(error); } + _error->heightValue( + ) | rpl::start_with_next([=] { + resizeToWidth(width()); + }, _error->lifetime()); _error->show(anim::type::normal); - } else if (_error) { - _error->hide(anim::type::normal); } } +bool PanelDetailsRow::errorShown() const { + return _errorShown; +} + void PanelDetailsRow::hideError() { startErrorAnimation(false); if (_error) { diff --git a/Telegram/SourceFiles/passport/passport_panel_details_row.h b/Telegram/SourceFiles/passport/passport_panel_details_row.h index 47fb6010b..7d206c12b 100644 --- a/Telegram/SourceFiles/passport/passport_panel_details_row.h +++ b/Telegram/SourceFiles/passport/passport_panel_details_row.h @@ -66,6 +66,7 @@ public: virtual rpl::producer value() const = 0; virtual QString valueCurrent() const = 0; void showError(const QString &error); + bool errorShown() const; void hideError(); void finishAnimating(); diff --git a/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp b/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp index 76adb22c2..2590a7b89 100644 --- a/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp +++ b/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp @@ -211,6 +211,7 @@ PanelEditDocument::PanelEditDocument( Scheme scheme, const ValueMap &data, const ValueMap &scanData, + const QString &missingScansError, std::vector &&files, std::unique_ptr &&selfie) : _controller(controller) @@ -222,7 +223,12 @@ PanelEditDocument::PanelEditDocument( this, langFactory(lng_passport_save_value), st::passportPanelSaveValue) { - setupControls(data, &scanData, std::move(files), std::move(selfie)); + setupControls( + data, + &scanData, + missingScansError, + std::move(files), + std::move(selfie)); } PanelEditDocument::PanelEditDocument( @@ -239,17 +245,19 @@ PanelEditDocument::PanelEditDocument( this, langFactory(lng_passport_save_value), st::passportPanelSaveValue) { - setupControls(data, nullptr, {}, nullptr); + setupControls(data, nullptr, QString(), {}, nullptr); } void PanelEditDocument::setupControls( const ValueMap &data, const ValueMap *scanData, + const QString &missingScansError, std::vector &&files, std::unique_ptr &&selfie) { const auto inner = setupContent( data, scanData, + missingScansError, std::move(files), std::move(selfie)); @@ -267,6 +275,7 @@ void PanelEditDocument::setupControls( not_null PanelEditDocument::setupContent( const ValueMap &data, const ValueMap *scanData, + const QString &missingScansError, std::vector &&files, std::unique_ptr &&selfie) { const auto inner = _scroll->setOwnedWidget( @@ -282,6 +291,7 @@ not_null PanelEditDocument::setupContent( inner, _controller, _scheme.scansHeader, + missingScansError, std::move(files), std::move(selfie))); } else { @@ -401,13 +411,16 @@ bool PanelEditDocument::validate() { auto first = QPointer(); for (const auto [i, field] : base::reversed(_details)) { const auto &row = _scheme.rows[i]; - if (row.validate && !row.validate(field->valueCurrent())) { + if (field->errorShown() + || (row.validate && !row.validate(field->valueCurrent()))) { field->showError(QString()); first = field; } } - if (!first) { - return !error; + if (error) { + return false; + } else if (!first) { + return true; } const auto firsttop = first->mapToGlobal(QPoint(0, 0)); const auto scrolltop = _scroll->mapToGlobal(QPoint(0, 0)); diff --git a/Telegram/SourceFiles/passport/passport_panel_edit_document.h b/Telegram/SourceFiles/passport/passport_panel_edit_document.h index a9c393ae8..e185feb70 100644 --- a/Telegram/SourceFiles/passport/passport_panel_edit_document.h +++ b/Telegram/SourceFiles/passport/passport_panel_edit_document.h @@ -62,6 +62,7 @@ public: Scheme scheme, const ValueMap &data, const ValueMap &scanData, + const QString &missingScansError, std::vector &&files, std::unique_ptr &&selfie); PanelEditDocument( @@ -81,11 +82,13 @@ private: void setupControls( const ValueMap &data, const ValueMap *scanData, + const QString &missingScansError, std::vector &&files, std::unique_ptr &&selfie); not_null setupContent( const ValueMap &data, const ValueMap *scanData, + const QString &missingScansError, std::vector &&files, std::unique_ptr &&selfie); void updateControlsGeometry(); diff --git a/Telegram/SourceFiles/passport/passport_panel_edit_scans.cpp b/Telegram/SourceFiles/passport/passport_panel_edit_scans.cpp index b7945415f..74da8a8c3 100644 --- a/Telegram/SourceFiles/passport/passport_panel_edit_scans.cpp +++ b/Telegram/SourceFiles/passport/passport_panel_edit_scans.cpp @@ -31,11 +31,13 @@ public: const style::PassportScanRow &st, const QString &name, const QString &status, - bool deleted); + bool deleted, + bool error); void setImage(const QImage &image); void setStatus(const QString &status); void setDeleted(bool deleted); + void setError(bool error); rpl::producer<> deleteClicks() const { return _delete->entity()->clicks(); @@ -57,6 +59,7 @@ private: Text _status; int _nameHeight = 0; int _statusHeight = 0; + bool _error = false; QImage _image; object_ptr> _delete; object_ptr> _restore; @@ -68,7 +71,8 @@ ScanButton::ScanButton( const style::PassportScanRow &st, const QString &name, const QString &status, - bool deleted) + bool deleted, + bool error) : AbstractButton(parent) , _st(st) , _name( @@ -79,6 +83,7 @@ ScanButton::ScanButton( st::defaultTextStyle, status, Ui::NameTextOptions()) +, _error(error) , _delete(this, object_ptr(this, _st.remove)) , _restore( this, @@ -109,6 +114,11 @@ void ScanButton::setDeleted(bool deleted) { update(); } +void ScanButton::setError(bool error) { + _error = error; + update(); +} + int ScanButton::resizeGetHeight(int newWidth) { _nameHeight = st::semiboldFont->height; _statusHeight = st::normalFont->height; @@ -145,7 +155,8 @@ void ScanButton::paintEvent(QPaintEvent *e) { _st.border, _st.borderFg); - if (_restore->toggled()) { + const auto deleted = _restore->toggled(); + if (deleted) { p.setOpacity(st::passportScanDeletedOpacity); } @@ -173,7 +184,9 @@ void ScanButton::paintEvent(QPaintEvent *e) { top + _st.nameTop, availableWidth, width()); - p.setPen(st::windowSubTextFg); + p.setPen((_error && !deleted) + ? st::boxTextFgError + : st::windowSubTextFg); _status.drawLeftElided( p, left + _st.textLeft, @@ -186,26 +199,56 @@ EditScans::EditScans( QWidget *parent, not_null controller, const QString &header, + const QString &errorMissing, std::vector &&files, std::unique_ptr &&selfie) : RpWidget(parent) , _controller(controller) , _files(std::move(files)) , _selfie(std::move(selfie)) +, _initialCount(_files.size()) +, _errorMissing(errorMissing) , _content(this) { setupContent(header); } +bool EditScans::uploadedSomeMore() const { + const auto from = begin(_files) + _initialCount; + const auto till = end(_files); + return std::find_if(from, till, [](const ScanInfo &file) { + return !file.deleted; + }) != till; +} + base::optional EditScans::validateGetErrorTop() { - const auto exists = ranges::find( + const auto exists = ranges::find_if( _files, - false, - [](const ScanInfo &file) { return file.deleted; }) != end(_files); - if (!exists) { + [](const ScanInfo &file) { return !file.deleted; }) != end(_files); + const auto errorExists = ranges::find_if( + _files, + [](const ScanInfo &file) { return !file.error.isEmpty(); } + ) != end(_files); + + if (!exists + || ((errorExists || _uploadMoreError) && !uploadedSomeMore())) { toggleError(true); return (_files.size() > 5) ? _upload->y() : _header->y(); } - if (_selfie && (!_selfie->key.id || _selfie->deleted)) { + + const auto nonDeletedErrorIt = ranges::find_if( + _files, + [](const ScanInfo &file) { + return !file.error.isEmpty() && !file.deleted; + }); + if (nonDeletedErrorIt != end(_files)) { + const auto index = (nonDeletedErrorIt - begin(_files)); + toggleError(true); + return _rows[index]->y(); + } + if (_selfie + && (!_selfie->key.id + || _selfie->deleted + || !_selfie->error.isEmpty())) { toggleSelfieError(true); return _selfieHeader->y(); } @@ -234,7 +277,18 @@ void EditScans::setupContent(const QString &header) { st::passportFormHeader), st::passportUploadHeaderPadding)); _header->toggle(!_files.empty(), anim::type::instant); - + if (!_errorMissing.isEmpty()) { + _uploadMoreError = inner->add( + object_ptr>( + inner, + object_ptr( + inner, + _errorMissing, + Ui::FlatLabel::InitType::Simple, + st::passportVerifyErrorLabel), + st::passportUploadErrorPadding)); + _uploadMoreError->toggle(true, anim::type::instant); + } _wrap = inner->add(object_ptr(inner)); for (const auto &scan : _files) { pushScan(scan); @@ -317,6 +371,7 @@ void EditScans::updateScan(ScanInfo &&info) { button->setStatus(info.status); button->setImage(info.thumb); button->setDeleted(info.deleted); + button->setError(!info.error.isEmpty()); }; if (info.selfie) { Assert(info.key.id != 0); @@ -413,7 +468,8 @@ base::unique_qptr> EditScans::createScan( st::passportScanRow, name, info.status, - info.deleted)))); + info.deleted, + !info.error.isEmpty())))); result->entity()->setImage(info.thumb); return result; } diff --git a/Telegram/SourceFiles/passport/passport_panel_edit_scans.h b/Telegram/SourceFiles/passport/passport_panel_edit_scans.h index 6e29701d9..89cbdf3b8 100644 --- a/Telegram/SourceFiles/passport/passport_panel_edit_scans.h +++ b/Telegram/SourceFiles/passport/passport_panel_edit_scans.h @@ -36,6 +36,7 @@ public: QWidget *parent, not_null controller, const QString &header, + const QString &errorMissing, std::vector &&files, std::unique_ptr &&selfie); @@ -62,6 +63,7 @@ private: void toggleError(bool shown); void hideError(); void errorAnimationCallback(); + bool uploadedSomeMore() const; void toggleSelfieError(bool shown); void hideSelfieError(); @@ -70,10 +72,13 @@ private: not_null _controller; std::vector _files; std::unique_ptr _selfie; + int _initialCount = 0; + QString _errorMissing; object_ptr _content; QPointer> _divider; QPointer> _header; + QPointer> _uploadMoreError; QPointer _wrap; std::vector>> _rows; QPointer _upload; diff --git a/Telegram/SourceFiles/passport/passport_panel_form.cpp b/Telegram/SourceFiles/passport/passport_panel_form.cpp index f52dbad9a..83f4d9f4c 100644 --- a/Telegram/SourceFiles/passport/passport_panel_form.cpp +++ b/Telegram/SourceFiles/passport/passport_panel_form.cpp @@ -74,8 +74,13 @@ void PanelForm::Row::updateContent( _description.setText( st::defaultTextStyle, description, - Ui::NameTextOptions()); - _ready = ready; + TextParseOptions { + TextParseMultiline, + 0, + 0, + Qt::LayoutDirectionAuto + }); + _ready = ready && !error; if (_error != error) { _error = error; if (animated == anim::type::instant) {