/* 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/passport_panel_edit_scans.h" #include "passport/passport_panel_controller.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" #include "ui/wrap/fade_wrap.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" #include "ui/chat/attach/attach_prepare.h" #include "ui/text/text_utilities.h" // Ui::Text::ToUpper #include "ui/text/text_options.h" #include "ui/image/image_prepare.h" #include "core/file_utilities.h" #include "lang/lang_keys.h" #include "boxes/abstract_box.h" #include "storage/storage_media_prepare.h" #include "storage/file_upload.h" // For Storage::kUseBigFilesFrom. #include "styles/style_layers.h" #include "styles/style_passport.h" #include namespace Passport { namespace { constexpr auto kMaxDimensions = 2048; constexpr auto kMaxSize = 10 * 1024 * 1024; constexpr auto kJpegQuality = 89; static_assert(kMaxSize <= Storage::kUseBigFilesFrom); std::variant ProcessImage(QByteArray &&bytes) { auto read = Images::Read({ .content = base::take(bytes), .forceOpaque = true, }); auto &image = read.image; if (image.isNull()) { return ReadScanError::CantReadImage; } else if (!Ui::ValidateThumbDimensions(image.width(), image.height())) { return ReadScanError::BadImageSize; } if (std::max(image.width(), image.height()) > kMaxDimensions) { image = std::move(image).scaled( kMaxDimensions, kMaxDimensions, Qt::KeepAspectRatio, Qt::SmoothTransformation); } auto result = QByteArray(); { QBuffer buffer(&result); if (!image.save(&buffer, "JPG", kJpegQuality)) { return ReadScanError::Unknown; } base::take(image); } if (result.isEmpty()) { return ReadScanError::Unknown; } else if (result.size() > kMaxSize) { return ReadScanError::FileTooLarge; } return result; } } // namespace class ScanButton : public Ui::AbstractButton { public: ScanButton( QWidget *parent, const style::PassportScanRow &st, const QString &name, const QString &status, 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() | rpl::to_empty; } rpl::producer<> restoreClicks() const { return _restore->entity()->clicks() | rpl::to_empty; } protected: int resizeGetHeight(int newWidth) override; void paintEvent(QPaintEvent *e) override; private: int countAvailableWidth() const; const style::PassportScanRow &_st; Ui::Text::String _name; Ui::Text::String _status; int _nameHeight = 0; int _statusHeight = 0; bool _error = false; QImage _image; object_ptr> _delete; object_ptr> _restore; }; struct EditScans::SpecialScan { SpecialScan(ScanInfo &&file); ScanInfo file; QPointer> header; QPointer wrap; base::unique_qptr> row; QPointer upload; bool errorShown = false; Ui::Animations::Simple errorAnimation; rpl::variable rowCreated; }; void UpdateFileRow( not_null button, const ScanInfo &info) { button->setStatus(info.status); button->setImage(info.thumb); button->setDeleted(info.deleted); button->setError(!info.error.isEmpty()); } base::unique_qptr> CreateScan( not_null parent, const ScanInfo &info, const QString &name) { auto result = base::unique_qptr>( parent->add(object_ptr>( parent, object_ptr( parent, st::passportScanRow, name, info.status, info.deleted, !info.error.isEmpty())))); result->entity()->setImage(info.thumb); return result; } EditScans::List::List( not_null controller, ScanListData &&data) : controller(controller) , files(std::move(data.files)) , initialCount(int(files.size())) , errorMissing(data.errorMissing) { } EditScans::List::List( not_null controller, std::optional &&data) : controller(controller) , files(data ? std::move(data->files) : std::vector()) , initialCount(data ? base::make_optional(int(files.size())) : std::nullopt) , errorMissing(data ? std::move(data->errorMissing) : QString()) { } bool EditScans::List::uploadedSomeMore() const { if (!initialCount) { return false; } const auto from = begin(files) + *initialCount; const auto till = end(files); return std::find_if(from, till, [](const ScanInfo &file) { return !file.deleted; }) != till; } bool EditScans::List::uploadMoreRequired() const { if (!upload) { return false; } const auto exists = ranges::any_of( files, [](const ScanInfo &file) { return !file.deleted; }); if (!exists) { return true; } const auto errorExists = ranges::any_of( files, [](const ScanInfo &file) { return !file.error.isEmpty(); }); return (errorExists || uploadMoreError) && !uploadedSomeMore(); } Ui::SlideWrap *EditScans::List::nonDeletedErrorRow() const { const auto nonDeletedErrorIt = ranges::find_if( files, [](const ScanInfo &file) { return !file.error.isEmpty() && !file.deleted; }); if (nonDeletedErrorIt == end(files)) { return nullptr; } const auto index = (nonDeletedErrorIt - begin(files)); return rows[index].get(); } rpl::producer EditScans::List::uploadButtonText() const { return (files.empty() ? tr::lng_passport_upload_scans : tr::lng_passport_upload_more)() | Ui::Text::ToUpper(); } void EditScans::List::hideError() { toggleError(false); } void EditScans::List::toggleError(bool shown) { if (errorShown != shown) { errorShown = shown; errorAnimation.start( [=] { errorAnimationCallback(); }, errorShown ? 0. : 1., errorShown ? 1. : 0., st::passportDetailsField.duration); } } void EditScans::List::errorAnimationCallback() { const auto error = errorAnimation.value(errorShown ? 1. : 0.); if (error == 0.) { upload->setColorOverride(std::nullopt); } else { upload->setColorOverride(anim::color( st::passportUploadButton.textFg, st::boxTextFgError, error)); } } void EditScans::List::updateScan(ScanInfo &&info, int width) { const auto i = ranges::find(files, info.key, [](const ScanInfo &file) { return file.key; }); if (i != files.end()) { *i = std::move(info); const auto scan = rows[i - files.begin()]->entity(); UpdateFileRow(scan, *i); if (!i->deleted) { hideError(); } } else { files.push_back(std::move(info)); pushScan(files.back()); wrap->resizeToWidth(width); rows.back()->show(anim::type::normal); if (divider) { divider->hide(anim::type::normal); } header->show(anim::type::normal); uploadTexts.fire(uploadButtonText()); } } void EditScans::List::pushScan(const ScanInfo &info) { const auto index = rows.size(); const auto type = info.type; rows.push_back(CreateScan( wrap, info, tr::lng_passport_scan_index(tr::now, lt_index, QString::number(index + 1)))); rows.back()->hide(anim::type::instant); const auto scan = rows.back()->entity(); scan->deleteClicks( ) | rpl::start_with_next([=] { controller->deleteScan(type, index); }, scan->lifetime()); scan->restoreClicks( ) | rpl::start_with_next([=] { controller->restoreScan(type, index); }, scan->lifetime()); hideError(); } ScanButton::ScanButton( QWidget *parent, const style::PassportScanRow &st, const QString &name, const QString &status, bool deleted, bool error) : AbstractButton(parent) , _st(st) , _name( st::passportScanNameStyle, name, Ui::NameTextOptions()) , _status( st::defaultTextStyle, status, Ui::NameTextOptions()) , _error(error) , _delete(this, object_ptr(this, _st.remove)) , _restore( this, object_ptr( this, tr::lng_passport_delete_scan_undo(), _st.restore)) { _delete->toggle(!deleted, anim::type::instant); _restore->toggle(deleted, anim::type::instant); } void ScanButton::setImage(const QImage &image) { _image = image; update(); } void ScanButton::setStatus(const QString &status) { _status.setText( st::defaultTextStyle, status, Ui::NameTextOptions()); update(); } void ScanButton::setDeleted(bool deleted) { _delete->toggle(!deleted, anim::type::instant); _restore->toggle(deleted, anim::type::instant); update(); } void ScanButton::setError(bool error) { _error = error; update(); } int ScanButton::resizeGetHeight(int newWidth) { _nameHeight = st::semiboldFont->height; _statusHeight = st::normalFont->height; const auto result = _st.padding.top() + _st.size + _st.padding.bottom(); const auto right = _st.padding.right(); _delete->moveToRight( right, (result - _delete->height()) / 2, newWidth); _restore->moveToRight( right, (result - _restore->height()) / 2, newWidth); return result + st::lineWidth; } int ScanButton::countAvailableWidth() const { return width() - _st.padding.left() - _st.textLeft - _st.padding.right() - std::max(_delete->width(), _restore->width()); } void ScanButton::paintEvent(QPaintEvent *e) { Painter p(this); const auto left = _st.padding.left(); const auto top = _st.padding.top(); p.fillRect( left, height() - _st.border, width() - left, _st.border, _st.borderFg); const auto deleted = _restore->toggled(); if (deleted) { p.setOpacity(st::passportScanDeletedOpacity); } if (_image.isNull()) { p.fillRect(left, top, _st.size, _st.size, Qt::black); } else { PainterHighQualityEnabler hq(p); const auto fromRect = [&] { if (_image.width() > _image.height()) { const auto shift = (_image.width() - _image.height()) / 2; return QRect(shift, 0, _image.height(), _image.height()); } else { const auto shift = (_image.height() - _image.width()) / 2; return QRect(0, shift, _image.width(), _image.width()); } }(); p.drawImage(QRect(left, top, _st.size, _st.size), _image, fromRect); } const auto availableWidth = countAvailableWidth(); p.setPen(st::windowFg); _name.drawLeftElided( p, left + _st.textLeft, top + _st.nameTop, availableWidth, width()); p.setPen((_error && !deleted) ? st::boxTextFgError : st::windowSubTextFg); _status.drawLeftElided( p, left + _st.textLeft, top + _st.statusTop, availableWidth, width()); } EditScans::SpecialScan::SpecialScan(ScanInfo &&file) : file(std::move(file)) { } EditScans::EditScans( QWidget *parent, not_null controller, const QString &header, const QString &error, ScanListData &&scans, std::optional &&translations) : RpWidget(parent) , _controller(controller) , _error(error) , _content(this) , _scansList(_controller, std::move(scans)) , _translationsList(_controller, std::move(translations)) { setupScans(header); } EditScans::EditScans( QWidget *parent, not_null controller, const QString &header, const QString &error, std::map &&specialFiles, std::optional &&translations) : RpWidget(parent) , _controller(controller) , _error(error) , _content(this) , _scansList(_controller) , _translationsList(_controller, std::move(translations)) { setupSpecialScans(header, std::move(specialFiles)); } std::optional EditScans::validateGetErrorTop() { auto result = std::optional(); const auto suggestResult = [&](int value) { if (!result || *result > value) { result = value; } }; if (_commonError && !somethingChanged()) { suggestResult(_commonError->y()); } const auto suggestList = [&](FileType type) { auto &list = this->list(type); if (list.uploadMoreRequired()) { list.toggleError(true); suggestResult((list.files.size() > 5) ? list.upload->y() : list.header->y()); } if (const auto row = list.nonDeletedErrorRow()) { //toggleError(true); suggestResult(row->y()); } }; suggestList(FileType::Scan); for (const auto &[type, scan] : _specialScans) { if (!scan.file.key.id || scan.file.deleted || !scan.file.error.isEmpty()) { toggleSpecialScanError(type, true); suggestResult(scan.header ? scan.header->y() : scan.wrap->y()); } } suggestList(FileType::Translation); return result; } EditScans::List &EditScans::list(FileType type) { switch (type) { case FileType::Scan: return _scansList; case FileType::Translation: return _translationsList; } Unexpected("Type in EditScans::list()."); } const EditScans::List &EditScans::list(FileType type) const { switch (type) { case FileType::Scan: return _scansList; case FileType::Translation: return _translationsList; } Unexpected("Type in EditScans::list() const."); } void EditScans::setupScans(const QString &header) { const auto inner = _content.data(); inner->move(0, 0); if (!_error.isEmpty()) { _commonError = inner->add( object_ptr>( inner, object_ptr( inner, _error, st::passportVerifyErrorLabel), st::passportValueErrorPadding)); _commonError->toggle(true, anim::type::instant); } setupList(inner, FileType::Scan, header); setupList(inner, FileType::Translation, tr::lng_passport_translation(tr::now)); init(); } void EditScans::setupList( not_null container, FileType type, const QString &header) { auto &list = this->list(type); if (!list.initialCount) { return; } if (type == FileType::Scan) { list.divider = container->add( object_ptr>( container, object_ptr( container, st::passportFormDividerHeight))); list.divider->toggle(list.files.empty(), anim::type::instant); } list.header = container->add( object_ptr>( container, object_ptr( container, header, st::passportFormHeader), st::passportUploadHeaderPadding)); list.header->toggle( !list.divider || !list.files.empty(), anim::type::instant); if (!list.errorMissing.isEmpty()) { list.uploadMoreError = container->add( object_ptr>( container, object_ptr( container, list.errorMissing, st::passportVerifyErrorLabel), st::passportUploadErrorPadding)); list.uploadMoreError->toggle(true, anim::type::instant); } list.wrap = container->add(object_ptr(container)); for (const auto &scan : list.files) { list.pushScan(scan); list.rows.back()->show(anim::type::instant); } list.upload = container->add( object_ptr( container, list.uploadTexts.events_starting_with( list.uploadButtonText() ) | rpl::flatten_latest(), st::passportUploadButton), st::passportUploadButtonPadding); list.upload->addClickHandler([=] { chooseScan(type); }); container->add(object_ptr( container, st::passportFormDividerHeight)); } void EditScans::setupSpecialScans( const QString &header, std::map &&files) { const auto requiresBothSides = files.find(FileType::ReverseSide) != end(files); const auto uploadText = [=](FileType type, bool hasScan) { switch (type) { case FileType::FrontSide: return requiresBothSides ? (hasScan ? tr::lng_passport_reupload_front_side : tr::lng_passport_upload_front_side) : (hasScan ? tr::lng_passport_reupload_main_page : tr::lng_passport_upload_main_page); case FileType::ReverseSide: return hasScan ? tr::lng_passport_reupload_reverse_side : tr::lng_passport_upload_reverse_side; case FileType::Selfie: return hasScan ? tr::lng_passport_reupload_selfie : tr::lng_passport_upload_selfie; } Unexpected("Type in special row upload key."); }; const auto description = [&](FileType type) { switch (type) { case FileType::FrontSide: return requiresBothSides ? tr::lng_passport_front_side_description : tr::lng_passport_main_page_description; case FileType::ReverseSide: return tr::lng_passport_reverse_side_description; case FileType::Selfie: return tr::lng_passport_selfie_description; } Unexpected("Type in special row upload key."); }; const auto inner = _content.data(); inner->move(0, 0); if (!_error.isEmpty()) { _commonError = inner->add( object_ptr>( inner, object_ptr( inner, _error, st::passportVerifyErrorLabel), st::passportValueErrorPadding)); _commonError->toggle(true, anim::type::instant); } for (auto &[type, info] : files) { const auto i = _specialScans.emplace( type, SpecialScan(std::move(info))).first; auto &scan = i->second; if (_specialScans.size() == 1) { scan.header = inner->add( object_ptr>( inner, object_ptr( inner, header, st::passportFormHeader), st::passportUploadHeaderPadding)); scan.header->toggle(scan.file.key.id != 0, anim::type::instant); } scan.wrap = inner->add(object_ptr(inner)); if (scan.file.key.id) { createSpecialScanRow(scan, scan.file, requiresBothSides); } auto label = scan.rowCreated.value( ) | rpl::map([=, type = type](bool created) { return uploadText(type, created)(); }) | rpl::flatten_latest( ) | Ui::Text::ToUpper(); scan.upload = inner->add( object_ptr( inner, std::move(label), st::passportUploadButton), st::passportUploadButtonPadding); scan.upload->addClickHandler([=, type = type] { chooseScan(type); }); inner->add(object_ptr( inner, object_ptr( inner, description(type)(tr::now), st::boxDividerLabel), st::passportFormLabelPadding)); } setupList(inner, FileType::Translation, tr::lng_passport_translation(tr::now)); init(); } void EditScans::init() { _controller->scanUpdated( ) | rpl::start_with_next([=](ScanInfo &&info) { updateScan(std::move(info)); }, lifetime()); widthValue( ) | rpl::start_with_next([=](int width) { _content->resizeToWidth(width); }, _content->lifetime()); _content->heightValue( ) | rpl::start_with_next([=](int height) { resize(width(), height); }, _content->lifetime()); } void EditScans::updateScan(ScanInfo &&info) { if (info.type != FileType::Scan && info.type != FileType::Translation) { updateSpecialScan(std::move(info)); return; } list(info.type).updateScan(std::move(info), width()); updateErrorLabels(); } void EditScans::scanFieldsChanged(bool changed) { if (_scanFieldsChanged != changed) { _scanFieldsChanged = changed; updateErrorLabels(); } } void EditScans::updateErrorLabels() { const auto updateList = [&](FileType type) { auto &list = this->list(type); if (list.uploadMoreError) { list.uploadMoreError->toggle( !list.uploadedSomeMore(), anim::type::normal); } }; updateList(FileType::Scan); updateList(FileType::Translation); if (_commonError) { _commonError->toggle(!somethingChanged(), anim::type::normal); } } bool EditScans::somethingChanged() const { return list(FileType::Scan).uploadedSomeMore() || list(FileType::Translation).uploadedSomeMore() || _scanFieldsChanged || _specialScanChanged; } void EditScans::updateSpecialScan(ScanInfo &&info) { Expects(info.key.id != 0); const auto type = info.type; const auto i = _specialScans.find(type); if (i == end(_specialScans)) { return; } auto &scan = i->second; if (scan.file.key.id) { UpdateFileRow(scan.row->entity(), info); scan.rowCreated = !info.deleted; if (scan.file.key.id != info.key.id) { specialScanChanged(type, true); } } else { const auto requiresBothSides = (_specialScans.find(FileType::ReverseSide) != end(_specialScans)); createSpecialScanRow(scan, info, requiresBothSides); scan.wrap->resizeToWidth(width()); scan.row->show(anim::type::normal); if (scan.header) { scan.header->show(anim::type::normal); } specialScanChanged(type, true); } scan.file = std::move(info); } void EditScans::createSpecialScanRow( SpecialScan &scan, const ScanInfo &info, bool requiresBothSides) { Expects(scan.file.type != FileType::Scan && scan.file.type != FileType::Translation); const auto type = scan.file.type; const auto name = [&] { switch (type) { case FileType::FrontSide: return requiresBothSides ? tr::lng_passport_front_side_title(tr::now) : tr::lng_passport_main_page_title(tr::now); case FileType::ReverseSide: return tr::lng_passport_reverse_side_title(tr::now); case FileType::Selfie: return tr::lng_passport_selfie_title(tr::now); } Unexpected("Type in special file name."); }(); scan.row = CreateScan(scan.wrap, info, name); const auto row = scan.row->entity(); row->deleteClicks( ) | rpl::start_with_next([=] { _controller->deleteScan(type, std::nullopt); }, row->lifetime()); row->restoreClicks( ) | rpl::start_with_next([=] { _controller->restoreScan(type, std::nullopt); }, row->lifetime()); scan.rowCreated = !info.deleted; } void EditScans::chooseScan(FileType type) { if (!_controller->canAddScan(type)) { _controller->showToast(tr::lng_passport_scans_limit_reached(tr::now)); return; } ChooseScan(this, type, [=](QByteArray &&content) { _controller->uploadScan(type, std::move(content)); }, [=](ReadScanError error) { _controller->readScanError(error); }); } void EditScans::ChooseScan( QPointer parent, FileType type, Fn doneCallback, Fn errorCallback) { Expects(parent != nullptr); const auto filter = FileDialog::AllOrImagesFilter(); const auto guardedCallback = crl::guard(parent, doneCallback); const auto guardedError = crl::guard(parent, errorCallback); const auto onMainError = [=](ReadScanError error) { crl::on_main([=] { guardedError(error); }); }; const auto processFiles = [=]( QStringList &&files, const auto &handleImage) -> void { while (!files.isEmpty()) { auto file = files.front(); files.removeAt(0); auto content = [&] { QFile f(file); if (f.size() > Images::kReadBytesLimit) { guardedError(ReadScanError::FileTooLarge); return QByteArray(); } else if (!f.open(QIODevice::ReadOnly)) { guardedError(ReadScanError::CantReadImage); return QByteArray(); } return f.readAll(); }(); if (!content.isEmpty()) { handleImage( std::move(content), std::move(files), handleImage); return; } } }; const auto processImage = [=]( QByteArray &&content, QStringList &&remainingFiles, const auto &repeatProcessImage) -> void { crl::async([ =, bytes = std::move(content), remainingFiles = std::move(remainingFiles) ]() mutable { auto result = ProcessImage(std::move(bytes)); if (const auto error = std::get_if(&result)) { onMainError(*error); } else { auto content = std::get_if(&result); Assert(content != nullptr); crl::on_main([ =, bytes = std::move(*content), remainingFiles = std::move(remainingFiles) ]() mutable { guardedCallback(std::move(bytes)); processFiles( std::move(remainingFiles), repeatProcessImage); }); } }); }; const auto processOpened = [=](FileDialog::OpenResult &&result) { if (result.paths.size() > 0) { processFiles(std::move(result.paths), processImage); } else if (!result.remoteContent.isEmpty()) { processImage(std::move(result.remoteContent), {}, processImage); } }; const auto allowMany = (type == FileType::Scan) || (type == FileType::Translation); (allowMany ? FileDialog::GetOpenPaths : FileDialog::GetOpenPath)( parent, tr::lng_passport_choose_image(tr::now), filter, processOpened, nullptr); } void EditScans::hideSpecialScanError(FileType type) { toggleSpecialScanError(type, false); } void EditScans::specialScanChanged(FileType type, bool changed) { hideSpecialScanError(type); if (_specialScanChanged != changed) { _specialScanChanged = changed; updateErrorLabels(); } } auto EditScans::findSpecialScan(FileType type) -> SpecialScan& { const auto i = _specialScans.find(type); Assert(i != end(_specialScans)); return i->second; } void EditScans::toggleSpecialScanError(FileType type, bool shown) { auto &scan = findSpecialScan(type); if (scan.errorShown != shown) { scan.errorShown = shown; scan.errorAnimation.start( [=] { specialScanErrorAnimationCallback(type); }, scan.errorShown ? 0. : 1., scan.errorShown ? 1. : 0., st::passportDetailsField.duration); } } void EditScans::specialScanErrorAnimationCallback(FileType type) { auto &scan = findSpecialScan(type); const auto error = scan.errorAnimation.value( scan.errorShown ? 1. : 0.); if (error == 0.) { scan.upload->setColorOverride(std::nullopt); } else { scan.upload->setColorOverride(anim::color( st::passportUploadButton.textFg, st::boxTextFgError, error)); } } EditScans::~EditScans() = default; } // namespace Passport