Support common error for the whole value.

It is removed (considered fixed) if anything changes in the data.
This commit is contained in:
John Preston 2018-08-14 14:37:03 +03:00
parent cb827406ca
commit b935d54fe7
11 changed files with 328 additions and 130 deletions

View File

@ -139,6 +139,7 @@ passportUploadButton: InfoProfileButton {
passportUploadButtonPadding: margins(0px, 10px, 0px, 10px); passportUploadButtonPadding: margins(0px, 10px, 0px, 10px);
passportUploadHeaderPadding: margins(22px, 14px, 22px, 3px); passportUploadHeaderPadding: margins(22px, 14px, 22px, 3px);
passportUploadErrorPadding: margins(22px, 5px, 22px, 5px); passportUploadErrorPadding: margins(22px, 5px, 22px, 5px);
passportValueErrorPadding: passportUploadHeaderPadding;
passportDeleteButton: InfoProfileButton(passportUploadButton) { passportDeleteButton: InfoProfileButton(passportUploadButton) {
textFg: attentionButtonFg; textFg: attentionButtonFg;
textFgOver: attentionButtonFgOver; textFgOver: attentionButtonFgOver;

View File

@ -302,6 +302,20 @@ bool Value::requiresSpecialScan(SpecialFile type) const {
Unexpected("Special scan type in requiresSpecialScan."); Unexpected("Special scan type in requiresSpecialScan.");
} }
void Value::fillDataFrom(Value &&other) {
const auto savedSelfieRequired = selfieRequired;
const auto savedTranslationRequired = translationRequired;
const auto savedNativeNames = nativeNames;
const auto savedEditScreens = editScreens;
*this = std::move(other);
selfieRequired = savedSelfieRequired;
translationRequired = savedTranslationRequired;
nativeNames = savedNativeNames;
editScreens = savedEditScreens;
}
bool Value::scansAreFilled() const { bool Value::scansAreFilled() const {
if (!requiresSpecialScan(SpecialFile::FrontSide) && scans.empty()) { if (!requiresSpecialScan(SpecialFile::FrontSide) && scans.empty()) {
return false; return false;
@ -876,12 +890,15 @@ void FormController::fillErrors() {
for (const auto &error : _form.pendingErrors) { for (const auto &error : _form.pendingErrors) {
error.match([&](const MTPDsecureValueError &data) { error.match([&](const MTPDsecureValueError &data) {
if (const auto value = find(data.vtype)) { if (const auto value = find(data.vtype)) {
value->error = qs(data.vtext); if (CanHaveErrors(value->type)) {
value->error = qs(data.vtext);
}
} }
}, [&](const MTPDsecureValueErrorData &data) { }, [&](const MTPDsecureValueErrorData &data) {
if (const auto value = find(data.vtype)) { if (const auto value = find(data.vtype)) {
const auto key = qs(data.vfield); const auto key = qs(data.vfield);
if (!SkipFieldCheck(value, key)) { if (CanHaveErrors(value->type)
&& !SkipFieldCheck(value, key)) {
value->data.parsed.fields[key].error = qs(data.vtext); value->data.parsed.fields[key].error = qs(data.vtext);
} }
} }
@ -894,7 +911,9 @@ void FormController::fillErrors() {
} }
}, [&](const MTPDsecureValueErrorFiles &data) { }, [&](const MTPDsecureValueErrorFiles &data) {
if (const auto value = find(data.vtype)) { if (const auto value = find(data.vtype)) {
value->scanMissingError = qs(data.vtext); if (CanRequireScans(value->type)) {
value->scanMissingError = qs(data.vtext);
}
} }
}, [&](const MTPDsecureValueErrorTranslationFile &data) { }, [&](const MTPDsecureValueErrorTranslationFile &data) {
const auto hash = bytes::make_span(data.vfile_hash.v); const auto hash = bytes::make_span(data.vfile_hash.v);
@ -921,7 +940,7 @@ void FormController::fillErrors() {
} }
} }
void FormController::decryptValue(Value &value) { void FormController::decryptValue(Value &value) const {
Expects(!_secret.empty()); Expects(!_secret.empty());
if (!validateValueSecrets(value)) { if (!validateValueSecrets(value)) {
@ -946,7 +965,7 @@ void FormController::decryptValue(Value &value) {
} }
} }
bool FormController::validateValueSecrets(Value &value) { bool FormController::validateValueSecrets(Value &value) const {
if (!value.data.original.isEmpty()) { if (!value.data.original.isEmpty()) {
value.data.secret = DecryptValueSecret( value.data.secret = DecryptValueSecret(
value.data.encryptedSecret, value.data.encryptedSecret,
@ -981,8 +1000,8 @@ bool FormController::validateValueSecrets(Value &value) {
return true; return true;
} }
void FormController::resetValue(Value &value) { void FormController::resetValue(Value &value) const {
value = Value(value.type); value.fillDataFrom(Value(value.type));
} }
rpl::producer<QString> FormController::passwordError() const { rpl::producer<QString> FormController::passwordError() const {
@ -1600,6 +1619,9 @@ void FormController::saveValueEdit(
return; return;
} }
// If we didn't change anything, we don't send save request
// and we don't reset value->error/[scan|translation]MissingError.
// Otherwise we reset them after save by re-parsing the value.
const auto nonconst = findValue(value); const auto nonconst = findValue(value);
if (!editValueChanged(nonconst, data)) { if (!editValueChanged(nonconst, data)) {
nonconst->saveRequestId = -1; nonconst->saveRequestId = -1;
@ -1632,10 +1654,7 @@ void FormController::deleteValueEdit(not_null<const Value*> value) {
nonconst->saveRequestId = request(MTPaccount_DeleteSecureValue( nonconst->saveRequestId = request(MTPaccount_DeleteSecureValue(
MTP_vector<MTPSecureValueType>(1, ConvertType(nonconst->type)) MTP_vector<MTPSecureValueType>(1, ConvertType(nonconst->type))
)).done([=](const MTPBool &result) { )).done([=](const MTPBool &result) {
const auto editScreens = value->editScreens; resetValue(*nonconst);
*nonconst = Value(nonconst->type);
nonconst->editScreens = editScreens;
_valueSaveFinished.fire_copy(value); _valueSaveFinished.fire_copy(value);
}).fail([=](const RPCError &error) { }).fail([=](const RPCError &error) {
nonconst->saveRequestId = 0; nonconst->saveRequestId = 0;
@ -1791,10 +1810,9 @@ void FormController::sendSaveRequest(
scansInEdit.push_back(std::move(scan)); scansInEdit.push_back(std::move(scan));
} }
const auto editScreens = value->editScreens; auto refreshed = parseValue(result, scansInEdit);
*value = parseValue(result, scansInEdit); decryptValue(refreshed);
decryptValue(*value); value->fillDataFrom(std::move(refreshed));
value->editScreens = editScreens;
_valueSaveFinished.fire_copy(value); _valueSaveFinished.fire_copy(value);
}).fail([=](const RPCError &error) { }).fail([=](const RPCError &error) {
@ -2257,13 +2275,13 @@ bool FormController::parseForm(const MTPaccount_AuthorizationForm &result) {
} }
for (const auto &required : data.vrequired_types.v) { for (const auto &required : data.vrequired_types.v) {
const auto row = CollectRequestedRow(required); const auto row = CollectRequestedRow(required);
for (const auto value : row.values) { for (const auto requested : row.values) {
const auto [i, ok] = _form.values.emplace( const auto type = requested.type;
value.type, const auto [i, ok] = _form.values.emplace(type, Value(type));
Value(value.type)); auto &value = i->second;
i->second.selfieRequired = value.selfieRequired; value.translationRequired = requested.translationRequired;
i->second.translationRequired = value.translationRequired; value.selfieRequired = requested.selfieRequired;
i->second.nativeNames = value.nativeNames; value.nativeNames = requested.nativeNames;
} }
_form.request.push_back(row.values _form.request.push_back(row.values
| ranges::view::transform([](const RequestedValue &value) { | ranges::view::transform([](const RequestedValue &value) {

View File

@ -164,10 +164,14 @@ struct Value {
Email, Email,
}; };
explicit Value(Type type); explicit Value(Type type);
Value(Value &&other) = default; Value(Value &&other) = default;
Value &operator=(Value &&other) = default;
// Some data is not parsed from server-provided values.
// It should be preserved through re-parsing (for example when saving).
// So we hide "operator=(Value&&)" in private and instead provide this.
void fillDataFrom(Value &&other);
bool requiresSpecialScan(SpecialFile type) const; bool requiresSpecialScan(SpecialFile type) const;
bool scansAreFilled() const; bool scansAreFilled() const;
@ -188,10 +192,13 @@ struct Value {
bool selfieRequired = false; bool selfieRequired = false;
bool translationRequired = false; bool translationRequired = false;
bool nativeNames = false; bool nativeNames = false;
int editScreens = 0; int editScreens = 0;
mtpRequestId saveRequestId = 0; mtpRequestId saveRequestId = 0;
private:
Value &operator=(Value &&other) = default;
}; };
struct RequestedValue { struct RequestedValue {
@ -395,9 +402,9 @@ private:
bytes::const_span passwordBytes, bytes::const_span passwordBytes,
uint64 serverSecretId); uint64 serverSecretId);
void decryptValues(); void decryptValues();
void decryptValue(Value &value); void decryptValue(Value &value) const;
bool validateValueSecrets(Value &value); bool validateValueSecrets(Value &value) const;
void resetValue(Value &value); void resetValue(Value &value) const;
void fillErrors(); void fillErrors();
void loadFile(File &file); void loadFile(File &file);

View File

@ -98,6 +98,31 @@ bool InlineDetails(const Form::Request &request, Value::Type details) {
Scope::Scope(Type type) : type(type) { Scope::Scope(Type type) : type(type) {
} }
bool CanRequireSelfie(Value::Type type) {
const auto scope = ScopeTypeForValueType(type);
return (scope == Scope::Type::Address)
|| (scope == Scope::Type::Identity);
}
bool CanRequireScans(Value::Type type) {
const auto scope = ScopeTypeForValueType(type);
return (scope == Scope::Type::Address);
}
bool CanRequireTranslation(Value::Type type) {
const auto scope = ScopeTypeForValueType(type);
return (scope == Scope::Type::Address)
|| (scope == Scope::Type::Identity);
}
bool CanRequireNativeNames(Value::Type type) {
return (type == Value::Type::PersonalDetails);
}
bool CanHaveErrors(Value::Type type) {
return (type != Value::Type::Phone) && (type != Value::Type::Email);
}
bool ValidateForm(const Form &form) { bool ValidateForm(const Form &form) {
base::flat_set<Value::Type> values; base::flat_set<Value::Type> values;
for (const auto &requested : form.request) { for (const auto &requested : form.request) {
@ -120,30 +145,36 @@ bool ValidateForm(const Form &form) {
values.emplace(type); values.emplace(type);
} }
} }
// Invalid errors should be skipped while parsing the form.
for (const auto &[type, value] : form.values) { for (const auto &[type, value] : form.values) {
if (value.selfieRequired && !CanRequireSelfie(type)) {
LOG(("API Error: Bad value requiring selfie."));
return false;
} else if (value.translationRequired
&& !CanRequireTranslation(type)) {
LOG(("API Error: Bad value requiring translation."));
return false;
} else if (value.nativeNames && !CanRequireNativeNames(type)) {
LOG(("API Error: Bad value requiring native names."));
return false;
}
if (!CanRequireScans(value.type)) {
Assert(value.scanMissingError.isEmpty());
}
if (!value.translationRequired) { if (!value.translationRequired) {
for (const auto &scan : value.translations) { for (const auto &scan : value.translations) {
if (!scan.error.isEmpty()) { Assert(scan.error.isEmpty());
LOG(("API Error: "
"Translation error in authorization form value."));
return false;
}
}
if (!value.translationMissingError.isEmpty()) {
LOG(("API Error: "
"Translations error in authorization form value."));
return false;
} }
Assert(value.translationMissingError.isEmpty());
} }
for (const auto &[type, specialScan] : value.specialScans) { for (const auto &[type, specialScan] : value.specialScans) {
if (!value.requiresSpecialScan(type) if (!value.requiresSpecialScan(type)) {
&& !specialScan.error.isEmpty()) { Assert(specialScan.error.isEmpty());
LOG(("API Error: "
"Special scan error in authorization form value."));
return false;
} }
} }
} }
return true; return true;
} }

View File

@ -34,6 +34,8 @@ struct ScopeRow {
QString error; QString error;
}; };
bool CanRequireScans(Value::Type type);
bool CanHaveErrors(Value::Type type);
bool ValidateForm(const Form &form); bool ValidateForm(const Form &form);
std::vector<Scope> ComputeScopes(const Form &form); std::vector<Scope> ComputeScopes(const Form &form);
QString ComputeScopeRowReadyString(const Scope &scope); QString ComputeScopeRowReadyString(const Scope &scope);

View File

@ -105,8 +105,8 @@ EditDocumentScheme GetDocumentScheme(
return NameValidate(value); return NameValidate(value);
}; };
// #TODO passport scheme
switch (type) { switch (type) {
case Scope::Type::PersonalDetails:
case Scope::Type::Identity: { case Scope::Type::Identity: {
auto result = Scheme(); auto result = Scheme();
result.detailsHeader = lang(lng_passport_personal_details); result.detailsHeader = lang(lng_passport_personal_details);
@ -212,6 +212,7 @@ EditDocumentScheme GetDocumentScheme(
return result; return result;
} break; } break;
case Scope::Type::AddressDetails:
case Scope::Type::Address: { case Scope::Type::Address: {
auto result = Scheme(); auto result = Scheme();
result.detailsHeader = lang(lng_passport_address); result.detailsHeader = lang(lng_passport_address);
@ -341,7 +342,7 @@ EditContactScheme GetContactScheme(Scope::Type type) {
Unexpected("Type in GetContactScheme()."); Unexpected("Type in GetContactScheme().");
} }
const std::map<QString, QString> &NativeNameKeys() { const std::map<QString, QString> &LatinToNativeMap() {
static const auto result = std::map<QString, QString> { static const auto result = std::map<QString, QString> {
{ qsl("first_name"), qsl("first_name_native") }, { qsl("first_name"), qsl("first_name_native") },
{ qsl("last_name"), qsl("last_name_native") }, { qsl("last_name"), qsl("last_name_native") },
@ -350,11 +351,11 @@ const std::map<QString, QString> &NativeNameKeys() {
return result; return result;
} }
const std::map<QString, QString> &LatinNameKeys() { const std::map<QString, QString> &NativeToLatinMap() {
static const auto result = std::map<QString, QString> { static const auto result = std::map<QString, QString> {
{ qsl("first_name_native"), qsl("first_name") }, { qsl("first_name_native"), qsl("first_name") },
{ qsl("last_name_native"), qsl("last_name") }, { qsl("last_name_native"), qsl("last_name") },
{ qsl("_nativemiddle_name"), qsl("middle_name") }, { qsl("middle_name_native"), qsl("middle_name") },
}; };
return result; return result;
} }
@ -363,10 +364,10 @@ bool SkipFieldCheck(not_null<const Value*> value, const QString &key) {
if (value->type != Value::Type::PersonalDetails) { if (value->type != Value::Type::PersonalDetails) {
return false; return false;
} }
const auto &namesMap = value->nativeNames const auto &dontCheckNames = value->nativeNames
? NativeNameKeys() ? LatinToNativeMap()
: LatinNameKeys(); : NativeToLatinMap();
return namesMap.find(key) == end(namesMap); return dontCheckNames.find(key) != end(dontCheckNames);
} }
BoxPointer::BoxPointer(QPointer<BoxContent> value) BoxPointer::BoxPointer(QPointer<BoxContent> value)
@ -679,37 +680,11 @@ ScanInfo PanelController::collectScanInfo(const EditFile &file) const {
file.fields.error }; file.fields.error };
} }
std::vector<ScopeError> PanelController::collectErrors( std::vector<ScopeError> PanelController::collectSaveErrors(
not_null<const Value*> value) const { not_null<const Value*> value) const {
using General = ScopeError::General; using General = ScopeError::General;
auto result = std::vector<ScopeError>(); auto result = std::vector<ScopeError>();
if (!value->error.isEmpty()) {
result.push_back({ General::WholeValue, value->error });
}
if (!value->scanMissingError.isEmpty()) {
result.push_back({ General::ScanMissing, value->scanMissingError });
}
if (!value->translationMissingError.isEmpty()) {
result.push_back({
General::TranslationMissing,
value->translationMissingError });
}
const auto addFileError = [&](const EditFile &file) {
if (!file.fields.error.isEmpty()) {
const auto key = FileKey{ file.fields.id, file.fields.dcId };
result.push_back({ key, file.fields.error });
}
};
for (const auto &scan : value->scansInEdit) {
addFileError(scan);
}
for (const auto &scan : value->translationsInEdit) {
addFileError(scan);
}
for (const auto &[type, scan] : value->specialScansInEdit) {
addFileError(scan);
}
for (const auto &[key, value] : value->data.parsedInEdit.fields) { for (const auto &[key, value] : value->data.parsedInEdit.fields) {
if (!value.error.isEmpty()) { if (!value.error.isEmpty()) {
result.push_back({ key, value.error }); result.push_back({ key, value.error });
@ -912,12 +887,10 @@ void PanelController::editScope(int index) {
editScope(index, -1); editScope(index, -1);
} else { } else {
const auto documentIndex = findNonEmptyDocumentIndex(scope); const auto documentIndex = findNonEmptyDocumentIndex(scope);
if (documentIndex >= 0) { if (documentIndex >= 0 || scope.documents.size() == 1) {
editScope(index, documentIndex); editScope(index, (documentIndex >= 0) ? documentIndex : 0);
} else if (scope.documents.size() > 1) {
requestScopeFilesType(index);
} else { } else {
editWithUpload(index, 0); requestScopeFilesType(index);
} }
} }
} }
@ -988,7 +961,7 @@ void PanelController::editWithUpload(int index, int documentIndex) {
Expects(documentIndex >= 0 Expects(documentIndex >= 0
&& documentIndex < _scopes[index].documents.size()); && documentIndex < _scopes[index].documents.size());
const auto &document = _scopes[index].documents[documentIndex]; const auto document = _scopes[index].documents[documentIndex];
const auto requiresSpecialScan = document->requiresSpecialScan( const auto requiresSpecialScan = document->requiresSpecialScan(
SpecialFile::FrontSide); SpecialFile::FrontSide);
const auto allowMany = !requiresSpecialScan; const auto allowMany = !requiresSpecialScan;
@ -998,7 +971,7 @@ void PanelController::editWithUpload(int index, int documentIndex) {
_scopeDocumentTypeBox = BoxPointer(); _scopeDocumentTypeBox = BoxPointer();
} }
if (!_editScope || !_editDocument) { if (!_editScope || !_editDocument) {
editScope(index, documentIndex); startScopeEdit(index, documentIndex);
} }
if (requiresSpecialScan) { if (requiresSpecialScan) {
uploadSpecialScan(SpecialFile::FrontSide, std::move(content)); uploadSpecialScan(SpecialFile::FrontSide, std::move(content));
@ -1026,9 +999,37 @@ void PanelController::readScanError(ReadScanError error) {
}())); }()));
} }
bool PanelController::editRequiresScanUpload(
int index,
int documentIndex) const {
Expects(index >= 0 && index < _scopes.size());
Expects((documentIndex < 0)
|| (documentIndex >= 0
&& documentIndex < _scopes[index].documents.size()));
if (documentIndex < 0) {
return false;
}
const auto document = _scopes[index].documents[documentIndex];
if (document->requiresSpecialScan(SpecialFile::FrontSide)) {
const auto &scans = document->specialScans;
return (scans.find(SpecialFile::FrontSide) == end(scans));
}
return document->scans.empty();
}
void PanelController::editScope(int index, int documentIndex) { void PanelController::editScope(int index, int documentIndex) {
if (editRequiresScanUpload(index, documentIndex)) {
editWithUpload(index, documentIndex);
} else {
startScopeEdit(index, documentIndex);
}
}
void PanelController::startScopeEdit(int index, int documentIndex) {
Expects(_panel != nullptr); Expects(_panel != nullptr);
Expects(index >= 0 && index < _scopes.size()); Expects(index >= 0 && index < _scopes.size());
Expects(_scopes[index].details != 0 || documentIndex >= 0);
Expects((documentIndex < 0) Expects((documentIndex < 0)
|| (documentIndex >= 0 || (documentIndex >= 0
&& documentIndex < _scopes[index].documents.size())); && documentIndex < _scopes[index].documents.size()));
@ -1038,7 +1039,6 @@ void PanelController::editScope(int index, int documentIndex) {
_editDocument = (documentIndex >= 0) _editDocument = (documentIndex >= 0)
? _scopes[index].documents[documentIndex].get() ? _scopes[index].documents[documentIndex].get()
: nullptr; : nullptr;
Assert(_editValue || _editDocument);
if (_editValue) { if (_editValue) {
_form->startValueEdit(_editValue); _form->startValueEdit(_editValue);
@ -1048,7 +1048,6 @@ void PanelController::editScope(int index, int documentIndex) {
} }
auto content = [&]() -> object_ptr<Ui::RpWidget> { auto content = [&]() -> object_ptr<Ui::RpWidget> {
// #TODO passport pass and display value->error
switch (_editScope->type) { switch (_editScope->type) {
case Scope::Type::Identity: case Scope::Type::Identity:
case Scope::Type::Address: { case Scope::Type::Address: {
@ -1060,7 +1059,9 @@ void PanelController::editScope(int index, int documentIndex) {
GetDocumentScheme( GetDocumentScheme(
_editScope->type, _editScope->type,
_editDocument->type), _editDocument->type),
_editValue->error,
_editValue->data.parsedInEdit, _editValue->data.parsedInEdit,
_editDocument->error,
_editDocument->data.parsedInEdit, _editDocument->data.parsedInEdit,
_editDocument->scanMissingError, _editDocument->scanMissingError,
valueFiles(*_editDocument), valueFiles(*_editDocument),
@ -1071,6 +1072,7 @@ void PanelController::editScope(int index, int documentIndex) {
GetDocumentScheme( GetDocumentScheme(
_editScope->type, _editScope->type,
_editDocument->type), _editDocument->type),
_editDocument->error,
_editDocument->data.parsedInEdit, _editDocument->data.parsedInEdit,
_editDocument->scanMissingError, _editDocument->scanMissingError,
valueFiles(*_editDocument), valueFiles(*_editDocument),
@ -1085,10 +1087,11 @@ void PanelController::editScope(int index, int documentIndex) {
case Scope::Type::AddressDetails: { case Scope::Type::AddressDetails: {
Assert(_editValue != nullptr); Assert(_editValue != nullptr);
auto result = object_ptr<PanelEditDocument>( auto result = object_ptr<PanelEditDocument>(
_panel->widget(), _panel->widget(),
this, this,
GetDocumentScheme(_editScope->type), GetDocumentScheme(_editScope->type),
_editValue->data.parsedInEdit); _editValue->error,
_editValue->data.parsedInEdit);
const auto weak = make_weak(result.data()); const auto weak = make_weak(result.data());
_panelHasUnsavedChanges = [=] { _panelHasUnsavedChanges = [=] {
return weak ? weak->hasUnsavedChanges() : false; return weak ? weak->hasUnsavedChanges() : false;
@ -1148,7 +1151,7 @@ void PanelController::processValueSaveFinished(
} }
if ((_editValue == value || _editDocument == value) && !savingScope()) { if ((_editValue == value || _editDocument == value) && !savingScope()) {
if (auto errors = collectErrors(value); !errors.empty()) { if (auto errors = collectSaveErrors(value); !errors.empty()) {
for (auto &&error : errors) { for (auto &&error : errors) {
_saveErrors.fire(std::move(error)); _saveErrors.fire(std::move(error));
} }

View File

@ -25,8 +25,8 @@ EditDocumentScheme GetDocumentScheme(
base::optional<Value::Type> scansType = base::none); base::optional<Value::Type> scansType = base::none);
EditContactScheme GetContactScheme(Scope::Type type); EditContactScheme GetContactScheme(Scope::Type type);
const std::map<QString, QString> &NativeNameKeys(); const std::map<QString, QString> &LatinToNativeMap();
const std::map<QString, QString> &LatinNameKeys(); const std::map<QString, QString> &NativeToLatinMap();
bool SkipFieldCheck(not_null<const Value*> value, const QString &key); bool SkipFieldCheck(not_null<const Value*> value, const QString &key);
struct ScanInfo { struct ScanInfo {
@ -144,6 +144,8 @@ private:
void editScope(int index, int documentIndex); void editScope(int index, int documentIndex);
void editWithUpload(int index, int documentIndex); void editWithUpload(int index, int documentIndex);
bool editRequiresScanUpload(int index, int documentIndex) const;
void startScopeEdit(int index, int documentIndex);
int findNonEmptyDocumentIndex(const Scope &scope) const; int findNonEmptyDocumentIndex(const Scope &scope) const;
void requestScopeFilesType(int index); void requestScopeFilesType(int index);
void cancelValueEdit(); void cancelValueEdit();
@ -158,7 +160,7 @@ private:
bool hasValueDocument() const; bool hasValueDocument() const;
bool hasValueFields() const; bool hasValueFields() const;
ScanInfo collectScanInfo(const EditFile &file) const; ScanInfo collectScanInfo(const EditFile &file) const;
std::vector<ScopeError> collectErrors( std::vector<ScopeError> collectSaveErrors(
not_null<const Value*> value) const; not_null<const Value*> value) const;
QString getDefaultContactValue(Scope::Type type) const; QString getDefaultContactValue(Scope::Type type) const;
void deleteValueSure(bool withDetails); void deleteValueSure(bool withDetails);

View File

@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/widgets/checkbox.h" #include "ui/widgets/checkbox.h"
#include "ui/wrap/vertical_layout.h" #include "ui/wrap/vertical_layout.h"
#include "ui/wrap/fade_wrap.h" #include "ui/wrap/fade_wrap.h"
#include "ui/wrap/slide_wrap.h"
#include "boxes/abstract_box.h" #include "boxes/abstract_box.h"
#include "boxes/confirm_box.h" #include "boxes/confirm_box.h"
#include "lang/lang_keys.h" #include "lang/lang_keys.h"
@ -209,8 +210,10 @@ PanelEditDocument::PanelEditDocument(
QWidget*, QWidget*,
not_null<PanelController*> controller, not_null<PanelController*> controller,
Scheme scheme, Scheme scheme,
const QString &error,
const ValueMap &data, const ValueMap &data,
const ValueMap &scanData, const QString &scansError,
const ValueMap &scansData,
const QString &missingScansError, const QString &missingScansError,
std::vector<ScanInfo> &&files, std::vector<ScanInfo> &&files,
std::map<SpecialFile, ScanInfo> &&specialFiles) std::map<SpecialFile, ScanInfo> &&specialFiles)
@ -224,8 +227,10 @@ PanelEditDocument::PanelEditDocument(
langFactory(lng_passport_save_value), langFactory(lng_passport_save_value),
st::passportPanelSaveValue) { st::passportPanelSaveValue) {
setupControls( setupControls(
&error,
&data, &data,
&scanData, &scansError,
&scansData,
missingScansError, missingScansError,
std::move(files), std::move(files),
std::move(specialFiles)); std::move(specialFiles));
@ -235,7 +240,8 @@ PanelEditDocument::PanelEditDocument(
QWidget*, QWidget*,
not_null<PanelController*> controller, not_null<PanelController*> controller,
Scheme scheme, Scheme scheme,
const ValueMap &scanData, const QString &scansError,
const ValueMap &scansData,
const QString &missingScansError, const QString &missingScansError,
std::vector<ScanInfo> &&files, std::vector<ScanInfo> &&files,
std::map<SpecialFile, ScanInfo> &&specialFiles) std::map<SpecialFile, ScanInfo> &&specialFiles)
@ -250,7 +256,9 @@ PanelEditDocument::PanelEditDocument(
st::passportPanelSaveValue) { st::passportPanelSaveValue) {
setupControls( setupControls(
nullptr, nullptr,
&scanData, nullptr,
&scansError,
&scansData,
missingScansError, missingScansError,
std::move(files), std::move(files),
std::move(specialFiles)); std::move(specialFiles));
@ -260,6 +268,7 @@ PanelEditDocument::PanelEditDocument(
QWidget*, QWidget*,
not_null<PanelController*> controller, not_null<PanelController*> controller,
Scheme scheme, Scheme scheme,
const QString &error,
const ValueMap &data) const ValueMap &data)
: _controller(controller) : _controller(controller)
, _scheme(std::move(scheme)) , _scheme(std::move(scheme))
@ -270,18 +279,22 @@ PanelEditDocument::PanelEditDocument(
this, this,
langFactory(lng_passport_save_value), langFactory(lng_passport_save_value),
st::passportPanelSaveValue) { st::passportPanelSaveValue) {
setupControls(&data, nullptr, QString(), {}, {}); setupControls(&error, &data, nullptr, nullptr, QString(), {}, {});
} }
void PanelEditDocument::setupControls( void PanelEditDocument::setupControls(
const QString *error,
const ValueMap *data, const ValueMap *data,
const ValueMap *scanData, const QString *scansError,
const ValueMap *scansData,
const QString &missingScansError, const QString &missingScansError,
std::vector<ScanInfo> &&files, std::vector<ScanInfo> &&files,
std::map<SpecialFile, ScanInfo> &&specialFiles) { std::map<SpecialFile, ScanInfo> &&specialFiles) {
const auto inner = setupContent( const auto inner = setupContent(
error,
data, data,
scanData, scansError,
scansData,
missingScansError, missingScansError,
std::move(files), std::move(files),
std::move(specialFiles)); std::move(specialFiles));
@ -298,8 +311,10 @@ void PanelEditDocument::setupControls(
} }
not_null<Ui::RpWidget*> PanelEditDocument::setupContent( not_null<Ui::RpWidget*> PanelEditDocument::setupContent(
const QString *error,
const ValueMap *data, const ValueMap *data,
const ValueMap *scanData, const QString *scansError,
const ValueMap *scansData,
const QString &missingScansError, const QString &missingScansError,
std::vector<ScanInfo> &&files, std::vector<ScanInfo> &&files,
std::map<SpecialFile, ScanInfo> &&specialFiles) { std::map<SpecialFile, ScanInfo> &&specialFiles) {
@ -315,22 +330,17 @@ not_null<Ui::RpWidget*> PanelEditDocument::setupContent(
object_ptr<EditScans>( object_ptr<EditScans>(
inner, inner,
_controller, _controller,
// _scheme.scansHeader, *scansError,
// missingScansError,
// std::move(files),
std::move(specialFiles))); std::move(specialFiles)));
} else if (scanData) { } else if (scansData) {
_editScans = inner->add( _editScans = inner->add(
object_ptr<EditScans>( object_ptr<EditScans>(
inner, inner,
_controller, _controller,
_scheme.scansHeader, _scheme.scansHeader,
*scansError,
missingScansError, missingScansError,
std::move(files))); std::move(files)));
} else {
inner->add(object_ptr<BoxContentDivider>(
inner,
st::passportFormDividerHeight));
} }
const auto valueOrEmpty = [&]( const auto valueOrEmpty = [&](
@ -348,7 +358,7 @@ not_null<Ui::RpWidget*> PanelEditDocument::setupContent(
const auto &row = _scheme.rows[i]; const auto &row = _scheme.rows[i];
auto fields = (row.valueClass == Scheme::ValueClass::Fields) auto fields = (row.valueClass == Scheme::ValueClass::Fields)
? data ? data
: scanData; : scansData;
if (!fields) { if (!fields) {
continue; continue;
} }
@ -365,6 +375,18 @@ not_null<Ui::RpWidget*> PanelEditDocument::setupContent(
PanelDetailsRow::LabelWidth(row.label)); PanelDetailsRow::LabelWidth(row.label));
}); });
if (maxLabelWidth > 0) { if (maxLabelWidth > 0) {
if (error && !error->isEmpty()) {
_commonError = inner->add(
object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
inner,
object_ptr<Ui::FlatLabel>(
inner,
*error,
Ui::FlatLabel::InitType::Simple,
st::passportVerifyErrorLabel),
st::passportValueErrorPadding));
_commonError->toggle(true, anim::type::instant);
}
inner->add( inner->add(
object_ptr<Ui::FlatLabel>( object_ptr<Ui::FlatLabel>(
inner, inner,
@ -377,15 +399,28 @@ not_null<Ui::RpWidget*> PanelEditDocument::setupContent(
const EditDocumentScheme::Row &row, const EditDocumentScheme::Row &row,
const ValueMap &fields) { const ValueMap &fields) {
const auto current = valueOrEmpty(fields, row.key); const auto current = valueOrEmpty(fields, row.key);
_details.emplace(i, inner->add(PanelDetailsRow::Create( const auto [it, ok] = _details.emplace(
inner, i,
row.inputType, inner->add(PanelDetailsRow::Create(
_controller, inner,
row.label, row.inputType,
maxLabelWidth, _controller,
current.text, row.label,
current.error, maxLabelWidth,
row.lengthLimit))); current.text,
current.error,
row.lengthLimit)));
const bool details = (&fields == data);
it->second->value(
) | rpl::skip(1) | rpl::start_with_next([=] {
if (details) {
_fieldsChanged = true;
updateCommonError();
} else {
Assert(_editScans != nullptr);
_editScans->scanFieldsChanged(true);
}
}, it->second->lifetime());
}); });
inner->add( inner->add(
@ -406,6 +441,12 @@ not_null<Ui::RpWidget*> PanelEditDocument::setupContent(
return inner; return inner;
} }
void PanelEditDocument::updateCommonError() {
if (_commonError) {
_commonError->toggle(!_fieldsChanged, anim::type::normal);
}
}
void PanelEditDocument::focusInEvent(QFocusEvent *e) { void PanelEditDocument::focusInEvent(QFocusEvent *e) {
crl::on_main(this, [=] { crl::on_main(this, [=] {
for (const auto [index, row] : _details) { for (const auto [index, row] : _details) {
@ -451,7 +492,7 @@ PanelEditDocument::Result PanelEditDocument::collect() const {
} }
bool PanelEditDocument::validate() { bool PanelEditDocument::validate() {
const auto error = _editScans auto error = _editScans
? _editScans->validateGetErrorTop() ? _editScans->validateGetErrorTop()
: base::none; : base::none;
if (error) { if (error) {
@ -459,6 +500,12 @@ bool PanelEditDocument::validate() {
const auto scrolltop = _scroll->mapToGlobal(QPoint(0, 0)); const auto scrolltop = _scroll->mapToGlobal(QPoint(0, 0));
const auto scrolldelta = errortop.y() - scrolltop.y(); const auto scrolldelta = errortop.y() - scrolltop.y();
_scroll->scrollToY(_scroll->scrollTop() + scrolldelta); _scroll->scrollToY(_scroll->scrollTop() + scrolldelta);
} else if (_commonError && !_fieldsChanged) {
const auto firsttop = _commonError->mapToGlobal(QPoint(0, 0));
const auto scrolltop = _scroll->mapToGlobal(QPoint(0, 0));
const auto scrolldelta = firsttop.y() - scrolltop.y();
_scroll->scrollToY(_scroll->scrollTop() + scrolldelta);
error = firsttop.y();
} }
auto first = QPointer<PanelDetailsRow>(); auto first = QPointer<PanelDetailsRow>();
for (const auto [i, field] : base::reversed(_details)) { for (const auto [i, field] : base::reversed(_details)) {

View File

@ -14,7 +14,10 @@ class InputField;
class ScrollArea; class ScrollArea;
class FadeShadow; class FadeShadow;
class PlainShadow; class PlainShadow;
class FlatLabel;
class RoundButton; class RoundButton;
template <typename Widget>
class SlideWrap;
} // namespace Ui } // namespace Ui
namespace Info { namespace Info {
@ -63,8 +66,10 @@ public:
QWidget *parent, QWidget *parent,
not_null<PanelController*> controller, not_null<PanelController*> controller,
Scheme scheme, Scheme scheme,
const QString &error,
const ValueMap &data, const ValueMap &data,
const ValueMap &scanData, const QString &scansError,
const ValueMap &scansData,
const QString &missingScansError, const QString &missingScansError,
std::vector<ScanInfo> &&files, std::vector<ScanInfo> &&files,
std::map<SpecialFile, ScanInfo> &&specialFiles); std::map<SpecialFile, ScanInfo> &&specialFiles);
@ -72,7 +77,8 @@ public:
QWidget *parent, QWidget *parent,
not_null<PanelController*> controller, not_null<PanelController*> controller,
Scheme scheme, Scheme scheme,
const ValueMap &scanData, const QString &scansError,
const ValueMap &scansData,
const QString &missingScansError, const QString &missingScansError,
std::vector<ScanInfo> &&files, std::vector<ScanInfo> &&files,
std::map<SpecialFile, ScanInfo> &&specialFiles); std::map<SpecialFile, ScanInfo> &&specialFiles);
@ -80,6 +86,7 @@ public:
QWidget *parent, QWidget *parent,
not_null<PanelController*> controller, not_null<PanelController*> controller,
Scheme scheme, Scheme scheme,
const QString &error,
const ValueMap &data); const ValueMap &data);
bool hasUnsavedChanges() const; bool hasUnsavedChanges() const;
@ -91,18 +98,23 @@ protected:
private: private:
struct Result; struct Result;
void setupControls( void setupControls(
const QString *error,
const ValueMap *data, const ValueMap *data,
const ValueMap *scanData, const QString *scansError,
const ValueMap *scansData,
const QString &missingScansError, const QString &missingScansError,
std::vector<ScanInfo> &&files, std::vector<ScanInfo> &&files,
std::map<SpecialFile, ScanInfo> &&specialFiles); std::map<SpecialFile, ScanInfo> &&specialFiles);
not_null<Ui::RpWidget*> setupContent( not_null<Ui::RpWidget*> setupContent(
const QString *error,
const ValueMap *data, const ValueMap *data,
const ValueMap *scanData, const QString *scansError,
const ValueMap *scansData,
const QString &missingScansError, const QString &missingScansError,
std::vector<ScanInfo> &&files, std::vector<ScanInfo> &&files,
std::map<SpecialFile, ScanInfo> &&specialFiles); std::map<SpecialFile, ScanInfo> &&specialFiles);
void updateControlsGeometry(); void updateControlsGeometry();
void updateCommonError();
Result collect() const; Result collect() const;
bool validate(); bool validate();
@ -116,7 +128,9 @@ private:
object_ptr<Ui::PlainShadow> _bottomShadow; object_ptr<Ui::PlainShadow> _bottomShadow;
QPointer<EditScans> _editScans; QPointer<EditScans> _editScans;
QPointer<Ui::SlideWrap<Ui::FlatLabel>> _commonError;
std::map<int, QPointer<PanelDetailsRow>> _details; std::map<int, QPointer<PanelDetailsRow>> _details;
bool _fieldsChanged = false;
QPointer<Info::Profile::Button> _delete; QPointer<Info::Profile::Button> _delete;

View File

@ -257,12 +257,14 @@ EditScans::EditScans(
QWidget *parent, QWidget *parent,
not_null<PanelController*> controller, not_null<PanelController*> controller,
const QString &header, const QString &header,
const QString &error,
const QString &errorMissing, const QString &errorMissing,
std::vector<ScanInfo> &&files) std::vector<ScanInfo> &&files)
: RpWidget(parent) : RpWidget(parent)
, _controller(controller) , _controller(controller)
, _files(std::move(files)) , _files(std::move(files))
, _initialCount(_files.size()) , _initialCount(_files.size())
, _error(error)
, _errorMissing(errorMissing) , _errorMissing(errorMissing)
, _content(this) { , _content(this) {
setupScans(header); setupScans(header);
@ -271,15 +273,20 @@ EditScans::EditScans(
EditScans::EditScans( EditScans::EditScans(
QWidget *parent, QWidget *parent,
not_null<PanelController*> controller, not_null<PanelController*> controller,
const QString &error,
std::map<SpecialFile, ScanInfo> &&specialFiles) std::map<SpecialFile, ScanInfo> &&specialFiles)
: RpWidget(parent) : RpWidget(parent)
, _controller(controller) , _controller(controller)
, _initialCount(-1) , _initialCount(-1)
, _error(error)
, _content(this) { , _content(this) {
setupSpecialScans(std::move(specialFiles)); setupSpecialScans(std::move(specialFiles));
} }
bool EditScans::uploadedSomeMore() const { bool EditScans::uploadedSomeMore() const {
if (_initialCount < 0) {
return false;
}
const auto from = begin(_files) + _initialCount; const auto from = begin(_files) + _initialCount;
const auto till = end(_files); const auto till = end(_files);
return std::find_if(from, till, [](const ScanInfo &file) { return std::find_if(from, till, [](const ScanInfo &file) {
@ -303,6 +310,9 @@ base::optional<int> EditScans::validateGetErrorTop() {
[](const ScanInfo &file) { return !file.error.isEmpty(); } [](const ScanInfo &file) { return !file.error.isEmpty(); }
) != end(_files); ) != end(_files);
if (_commonError && !somethingChanged()) {
suggestResult(_commonError->y());
}
if (_upload && (!exists if (_upload && (!exists
|| ((errorExists || _uploadMoreError) && !uploadedSomeMore()))) { || ((errorExists || _uploadMoreError) && !uploadedSomeMore()))) {
toggleError(true); toggleError(true);
@ -334,6 +344,19 @@ void EditScans::setupScans(const QString &header) {
const auto inner = _content.data(); const auto inner = _content.data();
inner->move(0, 0); inner->move(0, 0);
if (!_error.isEmpty()) {
_commonError = inner->add(
object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
inner,
object_ptr<Ui::FlatLabel>(
inner,
_error,
Ui::FlatLabel::InitType::Simple,
st::passportVerifyErrorLabel),
st::passportValueErrorPadding));
_commonError->toggle(true, anim::type::instant);
}
_divider = inner->add( _divider = inner->add(
object_ptr<Ui::SlideWrap<BoxContentDivider>>( object_ptr<Ui::SlideWrap<BoxContentDivider>>(
inner, inner,
@ -442,6 +465,20 @@ void EditScans::setupSpecialScans(std::map<SpecialFile, ScanInfo> &&files) {
const auto inner = _content.data(); const auto inner = _content.data();
inner->move(0, 0); inner->move(0, 0);
if (!_error.isEmpty()) {
_commonError = inner->add(
object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
inner,
object_ptr<Ui::FlatLabel>(
inner,
_error,
Ui::FlatLabel::InitType::Simple,
st::passportVerifyErrorLabel),
st::passportValueErrorPadding));
_commonError->toggle(true, anim::type::instant);
}
for (auto &[type, info] : files) { for (auto &[type, info] : files) {
const auto i = _specialScans.emplace( const auto i = _specialScans.emplace(
type, type,
@ -531,9 +568,27 @@ void EditScans::updateScan(ScanInfo &&info) {
_header->show(anim::type::normal); _header->show(anim::type::normal);
_uploadTexts.fire(uploadButtonText()); _uploadTexts.fire(uploadButtonText());
} }
updateErrorLabels();
}
void EditScans::scanFieldsChanged(bool changed) {
if (_scanFieldsChanged != changed) {
_scanFieldsChanged = changed;
updateErrorLabels();
}
}
void EditScans::updateErrorLabels() {
if (_uploadMoreError) { if (_uploadMoreError) {
_uploadMoreError->toggle(!uploadedSomeMore(), anim::type::normal); _uploadMoreError->toggle(!uploadedSomeMore(), anim::type::normal);
} }
if (_commonError) {
_commonError->toggle(!somethingChanged(), anim::type::normal);
}
}
bool EditScans::somethingChanged() const {
return uploadedSomeMore() || _scanFieldsChanged || _specialScanChanged;
} }
void EditScans::updateSpecialScan(SpecialFile type, ScanInfo &&info) { void EditScans::updateSpecialScan(SpecialFile type, ScanInfo &&info) {
@ -547,8 +602,8 @@ void EditScans::updateSpecialScan(SpecialFile type, ScanInfo &&info) {
if (scan.file.key.id) { if (scan.file.key.id) {
updateFileRow(scan.row->entity(), info); updateFileRow(scan.row->entity(), info);
scan.rowCreated = !info.deleted; scan.rowCreated = !info.deleted;
if (!info.deleted) { if (scan.file.key.id != info.key.id) {
hideSpecialScanError(type); specialScanChanged(type, true);
} }
} else { } else {
const auto requiresBothSides const auto requiresBothSides
@ -558,6 +613,7 @@ void EditScans::updateSpecialScan(SpecialFile type, ScanInfo &&info) {
scan.wrap->resizeToWidth(width()); scan.wrap->resizeToWidth(width());
scan.row->show(anim::type::normal); scan.row->show(anim::type::normal);
scan.header->show(anim::type::normal); scan.header->show(anim::type::normal);
specialScanChanged(type, true);
} }
scan.file = std::move(info); scan.file = std::move(info);
} }
@ -569,8 +625,7 @@ void EditScans::updateFileRow(
button->setImage(info.thumb); button->setImage(info.thumb);
button->setDeleted(info.deleted); button->setDeleted(info.deleted);
button->setError(!info.error.isEmpty()); button->setError(!info.error.isEmpty());
}; }
void EditScans::createSpecialScanRow( void EditScans::createSpecialScanRow(
SpecialScan &scan, SpecialScan &scan,
@ -606,7 +661,6 @@ void EditScans::createSpecialScanRow(
}, row->lifetime()); }, row->lifetime());
scan.rowCreated = !info.deleted; scan.rowCreated = !info.deleted;
hideSpecialScanError(type);
} }
void EditScans::pushScan(const ScanInfo &info) { void EditScans::pushScan(const ScanInfo &info) {
@ -793,6 +847,14 @@ void EditScans::hideSpecialScanError(SpecialFile type) {
toggleSpecialScanError(type, false); toggleSpecialScanError(type, false);
} }
void EditScans::specialScanChanged(SpecialFile type, bool changed) {
hideSpecialScanError(type);
if (_specialScanChanged != changed) {
_specialScanChanged = changed;
updateErrorLabels();
}
}
auto EditScans::findSpecialScan(SpecialFile type) -> SpecialScan& { auto EditScans::findSpecialScan(SpecialFile type) -> SpecialScan& {
const auto i = _specialScans.find(type); const auto i = _specialScans.find(type);
Assert(i != end(_specialScans)); Assert(i != end(_specialScans));

View File

@ -44,15 +44,19 @@ public:
QWidget *parent, QWidget *parent,
not_null<PanelController*> controller, not_null<PanelController*> controller,
const QString &header, const QString &header,
const QString &error,
const QString &errorMissing, const QString &errorMissing,
std::vector<ScanInfo> &&files); std::vector<ScanInfo> &&files);
EditScans( EditScans(
QWidget *parent, QWidget *parent,
not_null<PanelController*> controller, not_null<PanelController*> controller,
const QString &error,
std::map<SpecialFile, ScanInfo> &&specialFiles); std::map<SpecialFile, ScanInfo> &&specialFiles);
base::optional<int> validateGetErrorTop(); base::optional<int> validateGetErrorTop();
void scanFieldsChanged(bool changed);
static void ChooseScan( static void ChooseScan(
QPointer<QWidget> parent, QPointer<QWidget> parent,
Fn<void(QByteArray&&)> doneCallback, Fn<void(QByteArray&&)> doneCallback,
@ -88,28 +92,35 @@ private:
rpl::producer<QString> uploadButtonText() const; rpl::producer<QString> uploadButtonText() const;
void updateErrorLabels();
void toggleError(bool shown); void toggleError(bool shown);
void hideError(); void hideError();
void errorAnimationCallback(); void errorAnimationCallback();
bool uploadedSomeMore() const; bool uploadedSomeMore() const;
bool somethingChanged() const;
void toggleSpecialScanError(SpecialFile type, bool shown); void toggleSpecialScanError(SpecialFile type, bool shown);
void hideSpecialScanError(SpecialFile type); void hideSpecialScanError(SpecialFile type);
void specialScanErrorAnimationCallback(SpecialFile type); void specialScanErrorAnimationCallback(SpecialFile type);
void specialScanChanged(SpecialFile type, bool changed);
not_null<PanelController*> _controller; not_null<PanelController*> _controller;
std::vector<ScanInfo> _files; std::vector<ScanInfo> _files;
int _initialCount = 0; int _initialCount = 0;
QString _error;
QString _errorMissing; QString _errorMissing;
object_ptr<Ui::VerticalLayout> _content; object_ptr<Ui::VerticalLayout> _content;
QPointer<Ui::SlideWrap<BoxContentDivider>> _divider; QPointer<Ui::SlideWrap<BoxContentDivider>> _divider;
QPointer<Ui::SlideWrap<Ui::FlatLabel>> _header; QPointer<Ui::SlideWrap<Ui::FlatLabel>> _header;
QPointer<Ui::SlideWrap<Ui::FlatLabel>> _commonError;
QPointer<Ui::SlideWrap<Ui::FlatLabel>> _uploadMoreError; QPointer<Ui::SlideWrap<Ui::FlatLabel>> _uploadMoreError;
QPointer<Ui::VerticalLayout> _wrap; QPointer<Ui::VerticalLayout> _wrap;
std::vector<base::unique_qptr<Ui::SlideWrap<ScanButton>>> _rows; std::vector<base::unique_qptr<Ui::SlideWrap<ScanButton>>> _rows;
QPointer<Info::Profile::Button> _upload; QPointer<Info::Profile::Button> _upload;
rpl::event_stream<rpl::producer<QString>> _uploadTexts; rpl::event_stream<rpl::producer<QString>> _uploadTexts;
bool _scanFieldsChanged = false;
bool _specialScanChanged = false;
bool _errorShown = false; bool _errorShown = false;
Animation _errorAnimation; Animation _errorAnimation;