init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Close stale issues and PRs / stale (push) Successful in 13s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Close stale issues and PRs / stale (push) Successful in 13s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
This commit is contained in:
197
Telegram/SourceFiles/passport/passport.style
Normal file
197
Telegram/SourceFiles/passport/passport.style
Normal file
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
using "ui/basic.style";
|
||||
|
||||
using "ui/widgets/widgets.style";
|
||||
using "boxes/boxes.style";
|
||||
using "info/info.style";
|
||||
using "chat_helpers/chat_helpers.style";
|
||||
|
||||
passportPasswordPadding: margins(20px, 30px, 20px, 40px);
|
||||
passportPasswordLabel: FlatLabel(boxLabel) {
|
||||
minWidth: 275px;
|
||||
align: align(top);
|
||||
}
|
||||
passportPasswordLabelBold: FlatLabel(passportPasswordLabel) {
|
||||
style: TextStyle(boxLabelStyle) {
|
||||
font: font(boxFontSize semibold);
|
||||
}
|
||||
}
|
||||
passportPasswordSetupLabel: FlatLabel(passportPasswordLabel) {
|
||||
minWidth: 0px;
|
||||
}
|
||||
passportPasswordHintLabel: passportPasswordLabel;
|
||||
passportErrorLabel: FlatLabel(passportPasswordLabel) {
|
||||
textFg: boxTextFgError;
|
||||
}
|
||||
passportVerifyErrorLabel: FlatLabel(passportErrorLabel) {
|
||||
minWidth: 128px;
|
||||
align: align(topleft);
|
||||
}
|
||||
|
||||
passportPanelSize: size(392px, 600px);
|
||||
|
||||
passportPasswordFieldBottom: 306px;
|
||||
passportPasswordFieldSkip: 29px;
|
||||
passportPasswordHintSkip: 10px;
|
||||
passportPasswordUserpicSkip: 14px;
|
||||
passportPasswordUserpic: UserpicButton(defaultUserpicButton) {
|
||||
size: size(80px, 80px);
|
||||
photoSize: 80px;
|
||||
photoPosition: point(0px, 0px);
|
||||
}
|
||||
passportPasswordSubmit: RoundButton(defaultActiveButton) {
|
||||
width: 200px;
|
||||
height: 44px;
|
||||
textTop: 12px;
|
||||
style: TextStyle(semiboldTextStyle) {
|
||||
font: font(semibold 15px);
|
||||
}
|
||||
}
|
||||
passportPasswordSubmitBottom: 72px;
|
||||
passportPasswordForgotBottom: 36px;
|
||||
|
||||
passportPanelScroll: ScrollArea(defaultScrollArea) {
|
||||
deltat: 6px;
|
||||
deltab: 6px;
|
||||
}
|
||||
|
||||
passportPanelAuthorize: RoundButton(passportPasswordSubmit) {
|
||||
width: 0px;
|
||||
height: 49px;
|
||||
padding: margins(0px, -3px, 0px, 0px);
|
||||
textTop: 16px;
|
||||
icon: icon {{ "passport_authorize", activeButtonFg }};
|
||||
iconPosition: point(-8px, 9px);
|
||||
}
|
||||
passportPanelSaveValue: RoundButton(passportPanelAuthorize) {
|
||||
textFg: windowActiveTextFg;
|
||||
textFgOver: windowActiveTextFg;
|
||||
textBg: windowBg;
|
||||
textBgOver: windowBgOver;
|
||||
ripple: defaultRippleAnimation;
|
||||
icon: icon {};
|
||||
}
|
||||
passportFormAbout1Padding: margins(10px, 4px, 10px, 0px);
|
||||
passportFormAbout2Padding: margins(10px, 0px, 10px, 22px);
|
||||
passportFormHeader: FlatLabel(boxLabel) {
|
||||
textFg: windowActiveTextFg;
|
||||
style: semiboldTextStyle;
|
||||
}
|
||||
passportFormHeaderPadding: margins(22px, 20px, 22px, 9px);
|
||||
passportFormUserpic: UserpicButton(passportPasswordUserpic) {
|
||||
size: size(60px, 60px);
|
||||
photoSize: 60px;
|
||||
}
|
||||
passportFormUserpicPadding: margins(0px, 5px, 0px, 10px);
|
||||
passportFormDividerHeight: 13px;
|
||||
passportFormLabelPadding: margins(22px, 7px, 22px, 14px);
|
||||
passportFormPolicy: FlatLabel(boxDividerLabel) {
|
||||
style: TextStyle(defaultTextStyle) {
|
||||
linkUnderline: kLinkUnderlineAlways;
|
||||
}
|
||||
palette: TextPalette(defaultTextPalette) {
|
||||
linkFg: windowSubTextFg;
|
||||
}
|
||||
}
|
||||
passportFormPolicyPadding: margins(22px, 7px, 22px, 28px);
|
||||
passportContactNewFieldPadding: margins(22px, 0px, 22px, 14px);
|
||||
passportContactFieldPadding: margins(22px, 14px, 22px, 14px);
|
||||
passportContactErrorPadding: margins(22px, 0px, 22px, 0px);
|
||||
passportContactErrorMargin: margins(0px, 0px, 0px, 14px);
|
||||
|
||||
passportRowPadding: margins(22px, 8px, 25px, 8px);
|
||||
passportRowIconSkip: 10px;
|
||||
passportRowSkip: 2px;
|
||||
passportRowRipple: defaultRippleAnimationBgOver;
|
||||
passportRowReadyIcon: icon {{ "passport_ready", windowActiveTextFg }};
|
||||
passportRowEmptyIcon: icon {{ "passport_empty", menuIconFgOver }};
|
||||
passportRowTitleFg: windowFg;
|
||||
passportRowDescriptionFg: windowSubTextFg;
|
||||
|
||||
passportUploadButton: SettingsButton(defaultSettingsButton) {
|
||||
textFg: windowActiveTextFg;
|
||||
textFgOver: windowActiveTextFg;
|
||||
textBg: windowBg;
|
||||
textBgOver: windowBgOver;
|
||||
|
||||
style: semiboldTextStyle;
|
||||
|
||||
height: 18px;
|
||||
padding: margins(22px, 14px, 22px, 12px);
|
||||
|
||||
ripple: defaultRippleAnimation;
|
||||
}
|
||||
passportUploadButtonPadding: margins(0px, 10px, 0px, 10px);
|
||||
passportUploadHeaderPadding: margins(22px, 14px, 22px, 3px);
|
||||
passportUploadErrorPadding: margins(22px, 5px, 22px, 5px);
|
||||
passportValueErrorPadding: passportUploadHeaderPadding;
|
||||
passportDeleteButton: SettingsButton(passportUploadButton) {
|
||||
textFg: attentionButtonFg;
|
||||
textFgOver: attentionButtonFgOver;
|
||||
}
|
||||
|
||||
passportScanNameStyle: TextStyle(defaultTextStyle) {
|
||||
font: font(boxFontSize semibold);
|
||||
}
|
||||
passportScanRow: PassportScanRow {
|
||||
padding: margins(22px, 10px, 10px, 10px);
|
||||
size: 40px;
|
||||
textLeft: 53px;
|
||||
nameTop: 1px;
|
||||
statusTop: 22px;
|
||||
border: 1px;
|
||||
borderFg: inputBorderFg;
|
||||
remove: stickersRemove;
|
||||
restore: stickersUndoRemove;
|
||||
}
|
||||
passportScanDeletedOpacity: stickersRowDisabledOpacity;
|
||||
|
||||
passportDetailsHeaderPadding: margins(22px, 20px, 33px, 10px);
|
||||
passportDetailsPadding: margins(22px, 10px, 28px, 10px);
|
||||
passportDetailsField: InputField(defaultInputField) {
|
||||
textMargins: margins(2px, 8px, 2px, 0px);
|
||||
placeholderScale: 0.;
|
||||
placeholderFont: normalFont;
|
||||
heightMin: 32px;
|
||||
style: defaultTextStyle;
|
||||
}
|
||||
passportDetailsDateField: InputField(passportDetailsField) {
|
||||
textMargins: margins(2px, 8px, 2px, 0px);
|
||||
border: 0px;
|
||||
borderActive: 0px;
|
||||
heightMin: 30px;
|
||||
placeholderFont: semiboldFont;
|
||||
placeholderFgActive: placeholderFgActive;
|
||||
}
|
||||
passportDetailsSeparator: FlatLabel(passportPasswordLabelBold) {
|
||||
style: TextStyle(defaultTextStyle) {
|
||||
font: font(semibold 14px);
|
||||
}
|
||||
textFg: windowSubTextFg;
|
||||
align: align(topleft);
|
||||
}
|
||||
passportDetailsSeparatorPadding: margins(5px, 8px, 5px, 0px);
|
||||
passportContactField: InputField(defaultInputField) {
|
||||
style: defaultTextStyle;
|
||||
}
|
||||
passportDetailsFieldLeft: 116px;
|
||||
passportDetailsFieldTop: 2px;
|
||||
passportDetailsFieldSkipMin: 12px;
|
||||
passportDetailsSkip: 30px;
|
||||
passportDetailsGenderSkip: 20px;
|
||||
|
||||
passportRequestTypeSkip: 16px;
|
||||
|
||||
passportPasswordAbout1Padding: margins(10px, 28px, 10px, 0px);
|
||||
passportPasswordAbout2Padding: margins(10px, 0px, 10px, 28px);
|
||||
passportPasswordIconHeight: 224px;
|
||||
passportPasswordIcon: icon {{ "passport_password_setup", windowSubTextFg }};
|
||||
|
||||
passportNativeNameAboutMargin: margins(0px, 16px, 0px, 0px);
|
||||
passportNativeNameHeaderPadding: margins(22px, 28px, 33px, 10px);
|
||||
307
Telegram/SourceFiles/passport/passport_edit_identity_box.cpp
Normal file
307
Telegram/SourceFiles/passport/passport_edit_identity_box.cpp
Normal file
@@ -0,0 +1,307 @@
|
||||
/*
|
||||
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_edit_identity_box.h"
|
||||
|
||||
#include "passport/passport_panel_controller.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/text_options.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "core/file_utilities.h"
|
||||
#include "styles/style_widgets.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_passport.h"
|
||||
|
||||
namespace Passport {
|
||||
|
||||
class ScanButton : public Ui::RippleButton {
|
||||
public:
|
||||
ScanButton(
|
||||
QWidget *parent,
|
||||
const QString &title,
|
||||
const QString &description);
|
||||
|
||||
void setImage(const QImage &image);
|
||||
void setDescription(const QString &description);
|
||||
|
||||
rpl::producer<> deleteClicks() const {
|
||||
return _delete->clicks();
|
||||
}
|
||||
|
||||
protected:
|
||||
int resizeGetHeight(int newWidth) override;
|
||||
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
private:
|
||||
int countAvailableWidth() const;
|
||||
int countAvailableWidth(int newWidth) const;
|
||||
|
||||
Text _title;
|
||||
Text _description;
|
||||
int _titleHeight = 0;
|
||||
int _descriptionHeight = 0;
|
||||
QImage _image;
|
||||
object_ptr<Ui::IconButton> _delete = { nullptr };
|
||||
|
||||
};
|
||||
|
||||
ScanButton::ScanButton(
|
||||
QWidget *parent,
|
||||
const QString &title,
|
||||
const QString &description)
|
||||
: RippleButton(parent, st::passportRowRipple)
|
||||
, _title(
|
||||
st::semiboldTextStyle,
|
||||
title,
|
||||
Ui::NameTextOptions())
|
||||
, _description(
|
||||
st::defaultTextStyle,
|
||||
description,
|
||||
Ui::NameTextOptions())
|
||||
, _delete(this, st::passportScanDelete) {
|
||||
}
|
||||
|
||||
void ScanButton::setImage(const QImage &image) {
|
||||
_image = image;
|
||||
update();
|
||||
}
|
||||
|
||||
void ScanButton::setDescription(const QString &description) {
|
||||
_description.setText(
|
||||
st::defaultTextStyle,
|
||||
description,
|
||||
Ui::NameTextOptions());
|
||||
update();
|
||||
}
|
||||
|
||||
int ScanButton::resizeGetHeight(int newWidth) {
|
||||
const auto availableWidth = countAvailableWidth(newWidth);
|
||||
_titleHeight = st::semiboldFont->height;
|
||||
_descriptionHeight = st::normalFont->height;
|
||||
const auto result = st::passportRowPadding.top()
|
||||
+ _titleHeight
|
||||
+ st::passportRowSkip
|
||||
+ _descriptionHeight
|
||||
+ st::passportRowPadding.bottom();
|
||||
const auto right = st::passportRowPadding.right();
|
||||
_delete->moveToRight(
|
||||
right,
|
||||
(result - _delete->height()) / 2,
|
||||
newWidth);
|
||||
return result;
|
||||
}
|
||||
|
||||
int ScanButton::countAvailableWidth(int newWidth) const {
|
||||
return newWidth
|
||||
- st::passportRowPadding.left()
|
||||
- st::passportRowPadding.right()
|
||||
- _delete->width();
|
||||
}
|
||||
|
||||
int ScanButton::countAvailableWidth() const {
|
||||
return countAvailableWidth(width());
|
||||
}
|
||||
|
||||
void ScanButton::paintEvent(QPaintEvent *e) {
|
||||
Painter p(this);
|
||||
|
||||
const auto ms = getms();
|
||||
paintRipple(p, 0, 0, ms);
|
||||
|
||||
auto left = st::passportRowPadding.left();
|
||||
auto availableWidth = countAvailableWidth();
|
||||
auto top = st::passportRowPadding.top();
|
||||
const auto size = height() - top - st::passportRowPadding.bottom();
|
||||
if (_image.isNull()) {
|
||||
p.fillRect(left, top, size, size, Qt::black);
|
||||
} else {
|
||||
PainterHighQualityEnabler hq(p);
|
||||
if (_image.width() > _image.height()) {
|
||||
auto newheight = size * _image.height() / _image.width();
|
||||
p.drawImage(QRect(left, top + (size - newheight) / 2, size, newheight), _image);
|
||||
} else {
|
||||
auto newwidth = size * _image.width() / _image.height();
|
||||
p.drawImage(QRect(left + (size - newwidth) / 2, top, newwidth, size), _image);
|
||||
}
|
||||
}
|
||||
left += size + st::passportRowPadding.left();
|
||||
availableWidth -= size + st::passportRowPadding.left();
|
||||
|
||||
_title.drawLeftElided(p, left, top, availableWidth, width());
|
||||
top += _titleHeight + st::passportRowSkip;
|
||||
|
||||
_description.drawLeftElided(p, left, top, availableWidth, width());
|
||||
top += _descriptionHeight + st::passportRowPadding.bottom();
|
||||
}
|
||||
|
||||
IdentityBox::IdentityBox(
|
||||
QWidget*,
|
||||
not_null<PanelController*> controller,
|
||||
int valueIndex,
|
||||
const IdentityData &data,
|
||||
std::vector<ScanInfo> &&files)
|
||||
: _controller(controller)
|
||||
, _valueIndex(valueIndex)
|
||||
, _files(std::move(files))
|
||||
, _uploadScan(this, "Upload scans") // #TODO langs
|
||||
, _name(
|
||||
this,
|
||||
st::defaultInputField,
|
||||
langFactory(lng_signup_firstname),
|
||||
data.name)
|
||||
, _surname(
|
||||
this,
|
||||
st::defaultInputField,
|
||||
langFactory(lng_signup_lastname),
|
||||
data.surname) {
|
||||
}
|
||||
|
||||
void IdentityBox::prepare() {
|
||||
setTitle(langFactory(lng_passport_identity_title));
|
||||
|
||||
auto index = 0;
|
||||
for (const auto &scan : _files) {
|
||||
_scans.push_back(object_ptr<ScanButton>(
|
||||
this,
|
||||
QString("Scan %1").arg(++index), // #TODO langs
|
||||
scan.status));
|
||||
_scans.back()->setImage(scan.thumb);
|
||||
_scans.back()->resizeToWidth(st::boxWideWidth);
|
||||
_scans.back()->deleteClicks(
|
||||
) | rpl::on_next([=] {
|
||||
_controller->deleteScan(_valueIndex, index - 1);
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
addButton(langFactory(lng_settings_save), [=] {
|
||||
save();
|
||||
});
|
||||
addButton(langFactory(lng_cancel), [=] {
|
||||
closeBox();
|
||||
});
|
||||
_controller->scanUpdated(
|
||||
) | rpl::on_next([=](ScanInfo &&info) {
|
||||
updateScan(std::move(info));
|
||||
}, lifetime());
|
||||
|
||||
_uploadScan->addClickHandler([=] {
|
||||
chooseScan();
|
||||
});
|
||||
setDimensions(st::boxWideWidth, countHeight());
|
||||
}
|
||||
|
||||
int IdentityBox::countHeight() const {
|
||||
auto height = st::contactPadding.top();
|
||||
for (const auto &scan : _scans) {
|
||||
height += scan->height();
|
||||
}
|
||||
height += st::contactPadding.top()
|
||||
+ _uploadScan->height()
|
||||
+ st::contactSkip
|
||||
+ _name->height()
|
||||
+ st::contactSkip
|
||||
+ _surname->height()
|
||||
+ st::contactPadding.bottom()
|
||||
+ st::boxPadding.bottom();
|
||||
return height;
|
||||
}
|
||||
|
||||
void IdentityBox::updateScan(ScanInfo &&info) {
|
||||
const auto i = ranges::find(_files, info.key, [](const ScanInfo &file) {
|
||||
return file.key;
|
||||
});
|
||||
if (i != _files.end()) {
|
||||
*i = info;
|
||||
_scans[i - _files.begin()]->setDescription(i->status);
|
||||
_scans[i - _files.begin()]->setImage(i->thumb);
|
||||
} else {
|
||||
_files.push_back(std::move(info));
|
||||
_scans.push_back(object_ptr<ScanButton>(
|
||||
this,
|
||||
QString("Scan %1").arg(_files.size()),
|
||||
_files.back().status));
|
||||
_scans.back()->setImage(_files.back().thumb);
|
||||
_scans.back()->resizeToWidth(st::boxWideWidth);
|
||||
_scans.back()->show();
|
||||
updateControlsPosition();
|
||||
setDimensions(st::boxWideWidth, countHeight());
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void IdentityBox::setInnerFocus() {
|
||||
_name->setFocusFast();
|
||||
}
|
||||
|
||||
void IdentityBox::resizeEvent(QResizeEvent *e) {
|
||||
BoxContent::resizeEvent(e);
|
||||
|
||||
_name->resize((width()
|
||||
- st::boxPadding.left()
|
||||
- st::boxPadding.right()),
|
||||
_name->height());
|
||||
_surname->resize(_name->width(), _surname->height());
|
||||
|
||||
updateControlsPosition();
|
||||
}
|
||||
|
||||
void IdentityBox::updateControlsPosition() {
|
||||
auto top = st::contactPadding.top();
|
||||
for (const auto &scan : _scans) {
|
||||
scan->moveToLeft(0, top);
|
||||
top += scan->height();
|
||||
}
|
||||
top += st::contactPadding.top();
|
||||
_uploadScan->moveToLeft(st::boxPadding.left(), top);
|
||||
top += _uploadScan->height() + st::contactSkip;
|
||||
_name->moveToLeft(st::boxPadding.left(), top);
|
||||
top += _name->height() + st::contactSkip;
|
||||
_surname->moveToLeft(st::boxPadding.left(), top);
|
||||
}
|
||||
|
||||
void IdentityBox::chooseScan() {
|
||||
const auto filter = FileDialog::AllFilesFilter()
|
||||
+ u";;Image files (*"_q
|
||||
+ cImgExtensions().join(u" *"_q)
|
||||
+ u")"_q;
|
||||
const auto callback = [=](FileDialog::OpenResult &&result) {
|
||||
if (result.paths.size() == 1) {
|
||||
encryptScan(result.paths.front());
|
||||
} else if (!result.remoteContent.isEmpty()) {
|
||||
encryptScanContent(std::move(result.remoteContent));
|
||||
}
|
||||
};
|
||||
FileDialog::GetOpenPath(
|
||||
"Choose scan image",
|
||||
filter,
|
||||
base::lambda_guarded(this, callback));
|
||||
}
|
||||
|
||||
void IdentityBox::encryptScan(const QString &path) {
|
||||
encryptScanContent([&] {
|
||||
QFile f(path);
|
||||
if (!f.open(QIODevice::ReadOnly)) {
|
||||
return QByteArray();
|
||||
}
|
||||
return f.readAll();
|
||||
}());
|
||||
}
|
||||
|
||||
void IdentityBox::encryptScanContent(QByteArray &&content) {
|
||||
_controller->uploadScan(_valueIndex, std::move(content));
|
||||
}
|
||||
|
||||
void IdentityBox::save() {
|
||||
auto data = IdentityData();
|
||||
data.name = _name->getLastText();
|
||||
data.surname = _surname->getLastText();
|
||||
_controller->saveValueIdentity(_valueIndex, data);
|
||||
}
|
||||
|
||||
} // namespace Passport
|
||||
64
Telegram/SourceFiles/passport/passport_edit_identity_box.h
Normal file
64
Telegram/SourceFiles/passport/passport_edit_identity_box.h
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/layers/box_content.h"
|
||||
|
||||
namespace Ui {
|
||||
class LinkButton;
|
||||
class InputField;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Passport {
|
||||
|
||||
class PanelController;
|
||||
struct ScanInfo;
|
||||
class ScanButton;
|
||||
|
||||
struct IdentityData {
|
||||
QString name;
|
||||
QString surname;
|
||||
};
|
||||
|
||||
class IdentityBox : public BoxContent {
|
||||
public:
|
||||
IdentityBox(
|
||||
QWidget*,
|
||||
not_null<PanelController*> controller,
|
||||
int valueIndex,
|
||||
const IdentityData &data,
|
||||
std::vector<ScanInfo> &&files);
|
||||
|
||||
protected:
|
||||
void prepare() override;
|
||||
void setInnerFocus() override;
|
||||
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
|
||||
private:
|
||||
void chooseScan();
|
||||
void encryptScan(const QString &path);
|
||||
void encryptScanContent(QByteArray &&content);
|
||||
void updateScan(ScanInfo &&info);
|
||||
int countHeight() const;
|
||||
void updateControlsPosition();
|
||||
void save();
|
||||
|
||||
not_null<PanelController*> _controller;
|
||||
int _valueIndex = -1;
|
||||
|
||||
std::vector<ScanInfo> _files;
|
||||
|
||||
std::vector<object_ptr<ScanButton>> _scans;
|
||||
object_ptr<Ui::LinkButton> _uploadScan;
|
||||
object_ptr<Ui::InputField> _name;
|
||||
object_ptr<Ui::InputField> _surname;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Passport
|
||||
437
Telegram/SourceFiles/passport/passport_encryption.cpp
Normal file
437
Telegram/SourceFiles/passport/passport_encryption.cpp
Normal file
@@ -0,0 +1,437 @@
|
||||
/*
|
||||
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_encryption.h"
|
||||
|
||||
#include "base/openssl_help.h"
|
||||
#include "base/random.h"
|
||||
#include "mtproto/details/mtproto_rsa_public_key.h"
|
||||
|
||||
#include <QtCore/QJsonDocument>
|
||||
#include <QtCore/QJsonArray>
|
||||
#include <QtCore/QJsonObject>
|
||||
|
||||
namespace Passport {
|
||||
namespace {
|
||||
|
||||
constexpr auto kAesKeyLength = 32;
|
||||
constexpr auto kAesIvLength = 16;
|
||||
constexpr auto kSecretSize = 32;
|
||||
constexpr auto kAesParamsHashSize = 64;
|
||||
constexpr auto kMinPadding = 32;
|
||||
constexpr auto kMaxPadding = 255;
|
||||
constexpr auto kAlignTo = 16;
|
||||
|
||||
} // namespace
|
||||
|
||||
struct AesParams {
|
||||
bytes::vector key;
|
||||
bytes::vector iv;
|
||||
};
|
||||
|
||||
AesParams PrepareAesParamsWithHash(bytes::const_span hashForEncryptionKey) {
|
||||
Expects(hashForEncryptionKey.size() == kAesParamsHashSize);
|
||||
|
||||
auto result = AesParams();
|
||||
result.key = bytes::make_vector(
|
||||
hashForEncryptionKey.subspan(0, kAesKeyLength));
|
||||
result.iv = bytes::make_vector(
|
||||
hashForEncryptionKey.subspan(kAesKeyLength, kAesIvLength));
|
||||
return result;
|
||||
}
|
||||
|
||||
AesParams PrepareAesParams(bytes::const_span bytesForEncryptionKey) {
|
||||
return PrepareAesParamsWithHash(openssl::Sha512(bytesForEncryptionKey));
|
||||
}
|
||||
|
||||
bytes::vector EncryptOrDecrypt(
|
||||
bytes::const_span initial,
|
||||
AesParams &¶ms,
|
||||
int encryptOrDecrypt) {
|
||||
Expects((initial.size() & 0x0F) == 0);
|
||||
Expects(params.key.size() == kAesKeyLength);
|
||||
Expects(params.iv.size() == kAesIvLength);
|
||||
|
||||
auto aesKey = AES_KEY();
|
||||
const auto error = (encryptOrDecrypt == AES_ENCRYPT)
|
||||
? AES_set_encrypt_key(
|
||||
reinterpret_cast<const uchar*>(params.key.data()),
|
||||
params.key.size() * CHAR_BIT,
|
||||
&aesKey)
|
||||
: AES_set_decrypt_key(
|
||||
reinterpret_cast<const uchar*>(params.key.data()),
|
||||
params.key.size() * CHAR_BIT,
|
||||
&aesKey);
|
||||
if (error != 0) {
|
||||
LOG(("App Error: Could not AES_set_encrypt_key, result %1"
|
||||
).arg(error));
|
||||
return {};
|
||||
}
|
||||
auto result = bytes::vector(initial.size());
|
||||
AES_cbc_encrypt(
|
||||
reinterpret_cast<const uchar*>(initial.data()),
|
||||
reinterpret_cast<uchar*>(result.data()),
|
||||
initial.size(),
|
||||
&aesKey,
|
||||
reinterpret_cast<uchar*>(params.iv.data()),
|
||||
encryptOrDecrypt);
|
||||
return result;
|
||||
}
|
||||
|
||||
bytes::vector Encrypt(
|
||||
bytes::const_span decrypted,
|
||||
AesParams &¶ms) {
|
||||
return EncryptOrDecrypt(decrypted, std::move(params), AES_ENCRYPT);
|
||||
}
|
||||
|
||||
bytes::vector Decrypt(
|
||||
bytes::const_span encrypted,
|
||||
AesParams &¶ms) {
|
||||
return EncryptOrDecrypt(encrypted, std::move(params), AES_DECRYPT);
|
||||
}
|
||||
|
||||
bool CheckBytesMod255(bytes::const_span bytes) {
|
||||
const auto full = ranges::accumulate(
|
||||
bytes,
|
||||
0ULL,
|
||||
[](uint64 sum, gsl::byte value) { return sum + uchar(value); });
|
||||
const auto mod = (full % 255ULL);
|
||||
return (mod == 239);
|
||||
}
|
||||
|
||||
bool CheckSecretBytes(bytes::const_span secret) {
|
||||
return CheckBytesMod255(secret);
|
||||
}
|
||||
|
||||
bytes::vector GenerateSecretBytes() {
|
||||
auto result = bytes::vector(kSecretSize);
|
||||
bytes::set_random(result);
|
||||
const auto full = ranges::accumulate(
|
||||
result,
|
||||
0ULL,
|
||||
[](uint64 sum, gsl::byte value) { return sum + uchar(value); });
|
||||
const auto mod = (full % 255ULL);
|
||||
const auto add = 255ULL + 239 - mod;
|
||||
auto first = (static_cast<uchar>(result[0]) + add) % 255ULL;
|
||||
result[0] = static_cast<gsl::byte>(first);
|
||||
return result;
|
||||
}
|
||||
|
||||
bytes::vector DecryptSecretBytesWithHash(
|
||||
bytes::const_span encryptedSecret,
|
||||
bytes::const_span hashForEncryptionKey) {
|
||||
if (encryptedSecret.empty()) {
|
||||
return {};
|
||||
} else if (encryptedSecret.size() != kSecretSize) {
|
||||
LOG(("API Error: Wrong secret size %1"
|
||||
).arg(encryptedSecret.size()));
|
||||
return {};
|
||||
}
|
||||
auto params = PrepareAesParamsWithHash(hashForEncryptionKey);
|
||||
auto result = Decrypt(encryptedSecret, std::move(params));
|
||||
if (!CheckSecretBytes(result)) {
|
||||
LOG(("API Error: Bad secret bytes."));
|
||||
return {};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bytes::vector DecryptSecretBytes(
|
||||
bytes::const_span encryptedSecret,
|
||||
bytes::const_span bytesForEncryptionKey) {
|
||||
return DecryptSecretBytesWithHash(
|
||||
encryptedSecret,
|
||||
openssl::Sha512(bytesForEncryptionKey));
|
||||
}
|
||||
|
||||
bytes::vector EncryptSecretBytesWithHash(
|
||||
bytes::const_span secret,
|
||||
bytes::const_span hashForEncryptionKey) {
|
||||
Expects(secret.size() == kSecretSize);
|
||||
Expects(CheckSecretBytes(secret) == true);
|
||||
|
||||
auto params = PrepareAesParamsWithHash(hashForEncryptionKey);
|
||||
return Encrypt(secret, std::move(params));
|
||||
}
|
||||
|
||||
bytes::vector EncryptSecretBytes(
|
||||
bytes::const_span secret,
|
||||
bytes::const_span bytesForEncryptionKey) {
|
||||
Expects(secret.size() == kSecretSize);
|
||||
Expects(CheckSecretBytes(secret) == true);
|
||||
|
||||
auto params = PrepareAesParams(bytesForEncryptionKey);
|
||||
return Encrypt(secret, std::move(params));
|
||||
}
|
||||
|
||||
bytes::vector DecryptSecureSecret(
|
||||
bytes::const_span encryptedSecret,
|
||||
bytes::const_span passwordHashForSecret) {
|
||||
Expects(!encryptedSecret.empty());
|
||||
|
||||
return DecryptSecretBytesWithHash(
|
||||
encryptedSecret,
|
||||
passwordHashForSecret);
|
||||
}
|
||||
|
||||
bytes::vector EncryptSecureSecret(
|
||||
bytes::const_span secret,
|
||||
bytes::const_span passwordHashForSecret) {
|
||||
Expects(secret.size() == kSecretSize);
|
||||
|
||||
return EncryptSecretBytesWithHash(secret, passwordHashForSecret);
|
||||
}
|
||||
|
||||
bytes::vector SerializeData(const std::map<QString, QString> &data) {
|
||||
auto root = QJsonObject();
|
||||
for (const auto &[key, value] : data) {
|
||||
root.insert(key, value);
|
||||
}
|
||||
auto document = QJsonDocument(root);
|
||||
const auto result = document.toJson(QJsonDocument::Compact);
|
||||
return bytes::make_vector(result);
|
||||
}
|
||||
|
||||
std::map<QString, QString> DeserializeData(bytes::const_span bytes) {
|
||||
const auto serialized = QByteArray::fromRawData(
|
||||
reinterpret_cast<const char*>(bytes.data()),
|
||||
bytes.size());
|
||||
auto error = QJsonParseError();
|
||||
auto document = QJsonDocument::fromJson(serialized, &error);
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
LOG(("API Error: Could not deserialize decrypted JSON, error %1"
|
||||
).arg(error.errorString()));
|
||||
return {};
|
||||
} else if (!document.isObject()) {
|
||||
LOG(("API Error: decrypted JSON root is not an object."));
|
||||
return {};
|
||||
}
|
||||
auto object = document.object();
|
||||
auto result = std::map<QString, QString>();
|
||||
for (auto i = object.constBegin(), e = object.constEnd(); i != e; ++i) {
|
||||
const auto key = i.key();
|
||||
switch ((*i).type()) {
|
||||
case QJsonValue::Null: {
|
||||
LOG(("API Error: null found inside decrypted JSON root. "
|
||||
"Defaulting to empty string value."));
|
||||
result[key] = QString();
|
||||
} break;
|
||||
case QJsonValue::Undefined: {
|
||||
LOG(("API Error: undefined found inside decrypted JSON root. "
|
||||
"Defaulting to empty string value."));
|
||||
result[key] = QString();
|
||||
} break;
|
||||
case QJsonValue::Bool: {
|
||||
LOG(("API Error: bool found inside decrypted JSON root. "
|
||||
"Aborting."));
|
||||
return {};
|
||||
} break;
|
||||
case QJsonValue::Double: {
|
||||
LOG(("API Error: double found inside decrypted JSON root. "
|
||||
"Converting to string."));
|
||||
result[key] = QString::number((*i).toDouble());
|
||||
} break;
|
||||
case QJsonValue::String: {
|
||||
result[key] = (*i).toString();
|
||||
} break;
|
||||
case QJsonValue::Array: {
|
||||
LOG(("API Error: array found inside decrypted JSON root. "
|
||||
"Aborting."));
|
||||
return {};
|
||||
} break;
|
||||
case QJsonValue::Object: {
|
||||
LOG(("API Error: object found inside decrypted JSON root. "
|
||||
"Aborting."));
|
||||
return {};
|
||||
} break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<DataError> DeserializeErrors(bytes::const_span json) {
|
||||
const auto serialized = QByteArray::fromRawData(
|
||||
reinterpret_cast<const char*>(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<DataError>();
|
||||
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());
|
||||
}
|
||||
|
||||
EncryptedData EncryptData(
|
||||
bytes::const_span bytes,
|
||||
bytes::const_span dataSecret) {
|
||||
constexpr auto kFromPadding = kMinPadding + kAlignTo - 1;
|
||||
constexpr auto kPaddingDelta = kMaxPadding - kFromPadding;
|
||||
const auto randomPadding = kFromPadding
|
||||
+ (base::RandomValue<uint32>() % kPaddingDelta);
|
||||
const auto padding = randomPadding
|
||||
- ((bytes.size() + randomPadding) % kAlignTo);
|
||||
Assert(padding >= kMinPadding && padding <= kMaxPadding);
|
||||
|
||||
auto unencrypted = bytes::vector(padding + bytes.size());
|
||||
Assert(unencrypted.size() % kAlignTo == 0);
|
||||
|
||||
unencrypted[0] = static_cast<gsl::byte>(padding);
|
||||
base::RandomFill(unencrypted.data() + 1, padding - 1);
|
||||
bytes::copy(
|
||||
gsl::make_span(unencrypted).subspan(padding),
|
||||
bytes);
|
||||
const auto dataHash = openssl::Sha256(unencrypted);
|
||||
const auto bytesForEncryptionKey = bytes::concatenate(
|
||||
dataSecret,
|
||||
dataHash);
|
||||
|
||||
auto params = PrepareAesParams(bytesForEncryptionKey);
|
||||
return {
|
||||
{ dataSecret.begin(), dataSecret.end() },
|
||||
{ dataHash.begin(), dataHash.end() },
|
||||
Encrypt(unencrypted, std::move(params))
|
||||
};
|
||||
}
|
||||
|
||||
bytes::vector DecryptData(
|
||||
bytes::const_span encrypted,
|
||||
bytes::const_span dataHash,
|
||||
bytes::const_span dataSecret) {
|
||||
constexpr auto kDataHashSize = 32;
|
||||
if (encrypted.empty()) {
|
||||
return {};
|
||||
} else if (dataHash.size() != kDataHashSize) {
|
||||
LOG(("API Error: Bad data hash size %1").arg(dataHash.size()));
|
||||
return {};
|
||||
} else if (dataSecret.size() != kSecretSize) {
|
||||
LOG(("API Error: Bad data secret size %1").arg(dataSecret.size()));
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto bytesForEncryptionKey = bytes::concatenate(
|
||||
dataSecret,
|
||||
dataHash);
|
||||
auto params = PrepareAesParams(bytesForEncryptionKey);
|
||||
const auto decrypted = Decrypt(encrypted, std::move(params));
|
||||
if (bytes::compare(openssl::Sha256(decrypted), dataHash) != 0) {
|
||||
LOG(("API Error: Bad data hash."));
|
||||
return {};
|
||||
}
|
||||
const auto padding = static_cast<uint32>(decrypted[0]);
|
||||
if (padding < kMinPadding
|
||||
|| padding > kMaxPadding
|
||||
|| padding > decrypted.size()) {
|
||||
LOG(("API Error: Bad padding value %1").arg(padding));
|
||||
return {};
|
||||
}
|
||||
const auto bytes = gsl::make_span(decrypted).subspan(padding);
|
||||
return { bytes.begin(), bytes.end() };
|
||||
}
|
||||
|
||||
bytes::vector PrepareValueHash(
|
||||
bytes::const_span dataHash,
|
||||
bytes::const_span valueSecret) {
|
||||
return openssl::Sha256(dataHash, valueSecret);
|
||||
}
|
||||
|
||||
bytes::vector EncryptValueSecret(
|
||||
bytes::const_span valueSecret,
|
||||
bytes::const_span secret,
|
||||
bytes::const_span valueHash) {
|
||||
const auto bytesForEncryptionKey = bytes::concatenate(
|
||||
secret,
|
||||
valueHash);
|
||||
return EncryptSecretBytes(valueSecret, bytesForEncryptionKey);
|
||||
}
|
||||
|
||||
bytes::vector DecryptValueSecret(
|
||||
bytes::const_span encrypted,
|
||||
bytes::const_span secret,
|
||||
bytes::const_span valueHash) {
|
||||
const auto bytesForEncryptionKey = bytes::concatenate(
|
||||
secret,
|
||||
valueHash);
|
||||
return DecryptSecretBytes(encrypted, bytesForEncryptionKey);
|
||||
}
|
||||
|
||||
uint64 CountSecureSecretId(bytes::const_span secret) {
|
||||
const auto full = openssl::Sha256(secret);
|
||||
return *reinterpret_cast<const uint64*>(full.data());
|
||||
}
|
||||
|
||||
bytes::vector EncryptCredentialsSecret(
|
||||
bytes::const_span secret,
|
||||
bytes::const_span publicKey) {
|
||||
const auto key = MTP::details::RSAPublicKey(publicKey);
|
||||
return key.encryptOAEPpadding(secret);
|
||||
}
|
||||
|
||||
} // namespace Passport
|
||||
72
Telegram/SourceFiles/passport/passport_encryption.h
Normal file
72
Telegram/SourceFiles/passport/passport_encryption.h
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace Passport {
|
||||
|
||||
bytes::vector GenerateSecretBytes();
|
||||
|
||||
bytes::vector EncryptSecureSecret(
|
||||
bytes::const_span secret,
|
||||
bytes::const_span passwordHashForSecret);
|
||||
bytes::vector DecryptSecureSecret(
|
||||
bytes::const_span encryptedSecret,
|
||||
bytes::const_span passwordHashForSecret);
|
||||
|
||||
bytes::vector SerializeData(const std::map<QString, QString> &data);
|
||||
std::map<QString, QString> DeserializeData(bytes::const_span bytes);
|
||||
|
||||
struct DataError {
|
||||
// QByteArray - bad existing scan with such file_hash
|
||||
// QString - bad data field value with such key
|
||||
// std::nullopt - additional scan required
|
||||
std::variant<v::null_t, QByteArray, QString> key;
|
||||
QString type; // personal_details, passport, etc.
|
||||
QString text;
|
||||
|
||||
};
|
||||
std::vector<DataError> DeserializeErrors(bytes::const_span json);
|
||||
|
||||
struct EncryptedData {
|
||||
bytes::vector secret;
|
||||
bytes::vector hash;
|
||||
bytes::vector bytes;
|
||||
};
|
||||
|
||||
EncryptedData EncryptData(bytes::const_span bytes);
|
||||
|
||||
EncryptedData EncryptData(
|
||||
bytes::const_span bytes,
|
||||
bytes::const_span dataSecret);
|
||||
|
||||
bytes::vector DecryptData(
|
||||
bytes::const_span encrypted,
|
||||
bytes::const_span dataHash,
|
||||
bytes::const_span dataSecret);
|
||||
|
||||
bytes::vector PrepareValueHash(
|
||||
bytes::const_span dataHash,
|
||||
bytes::const_span valueSecret);
|
||||
|
||||
bytes::vector EncryptValueSecret(
|
||||
bytes::const_span valueSecret,
|
||||
bytes::const_span secret,
|
||||
bytes::const_span valueHash);
|
||||
|
||||
bytes::vector DecryptValueSecret(
|
||||
bytes::const_span encrypted,
|
||||
bytes::const_span secret,
|
||||
bytes::const_span valueHash);
|
||||
|
||||
uint64 CountSecureSecretId(bytes::const_span secret);
|
||||
|
||||
bytes::vector EncryptCredentialsSecret(
|
||||
bytes::const_span secret,
|
||||
bytes::const_span publicKey);
|
||||
|
||||
} // namespace Passport
|
||||
2768
Telegram/SourceFiles/passport/passport_form_controller.cpp
Normal file
2768
Telegram/SourceFiles/passport/passport_form_controller.cpp
Normal file
File diff suppressed because it is too large
Load Diff
603
Telegram/SourceFiles/passport/passport_form_controller.h
Normal file
603
Telegram/SourceFiles/passport/passport_form_controller.h
Normal file
@@ -0,0 +1,603 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "mtproto/sender.h"
|
||||
#include "base/timer.h"
|
||||
#include "base/weak_ptr.h"
|
||||
#include "core/core_cloud_password.h"
|
||||
|
||||
class mtpFileLoader;
|
||||
|
||||
namespace Storage {
|
||||
struct UploadSecureDone;
|
||||
struct UploadSecureProgress;
|
||||
} // namespace Storage
|
||||
|
||||
namespace Window {
|
||||
class SessionController;
|
||||
} // namespace Window
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Ui {
|
||||
class SentCodeCall;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Passport {
|
||||
|
||||
struct EditDocumentCountry;
|
||||
|
||||
struct SavedCredentials {
|
||||
bytes::vector hashForAuth;
|
||||
bytes::vector hashForSecret;
|
||||
uint64 secretId = 0;
|
||||
};
|
||||
|
||||
QString NonceNameByScope(const QString &scope);
|
||||
|
||||
class ViewController;
|
||||
|
||||
struct FormRequest {
|
||||
FormRequest(
|
||||
UserId botId,
|
||||
const QString &scope,
|
||||
const QString &callbackUrl,
|
||||
const QString &publicKey,
|
||||
const QString &nonce);
|
||||
|
||||
UserId botId;
|
||||
QString scope;
|
||||
QString callbackUrl;
|
||||
QString publicKey;
|
||||
QString nonce;
|
||||
|
||||
};
|
||||
|
||||
class LoadStatus final {
|
||||
public:
|
||||
enum class Status {
|
||||
Done,
|
||||
InProgress,
|
||||
Failed,
|
||||
};
|
||||
|
||||
LoadStatus() = default;
|
||||
|
||||
void set(Status status, int offset = 0) {
|
||||
if (!offset) {
|
||||
offset = _offset;
|
||||
}
|
||||
_offset = (status == Status::InProgress) ? offset : 0;
|
||||
_status = status;
|
||||
}
|
||||
|
||||
int offset() const {
|
||||
return _offset;
|
||||
}
|
||||
Status status() const {
|
||||
return _status;
|
||||
}
|
||||
private:
|
||||
int _offset = 0;
|
||||
Status _status = Status::Done;
|
||||
};
|
||||
|
||||
struct UploadScanData {
|
||||
FullMsgId fullId;
|
||||
uint64 fileId = 0;
|
||||
int partsCount = 0;
|
||||
QByteArray md5checksum;
|
||||
bytes::vector hash;
|
||||
bytes::vector bytes;
|
||||
|
||||
LoadStatus status;
|
||||
};
|
||||
|
||||
class UploadScanDataPointer {
|
||||
public:
|
||||
UploadScanDataPointer(
|
||||
not_null<Main::Session*> session,
|
||||
std::unique_ptr<UploadScanData> &&value);
|
||||
UploadScanDataPointer(UploadScanDataPointer &&other);
|
||||
UploadScanDataPointer &operator=(UploadScanDataPointer &&other);
|
||||
~UploadScanDataPointer();
|
||||
|
||||
UploadScanData *get() const;
|
||||
operator UploadScanData*() const;
|
||||
explicit operator bool() const;
|
||||
UploadScanData *operator->() const;
|
||||
|
||||
private:
|
||||
not_null<Main::Session*> _session;
|
||||
std::unique_ptr<UploadScanData> _value;
|
||||
|
||||
};
|
||||
|
||||
struct Value;
|
||||
|
||||
enum class FileType {
|
||||
Scan,
|
||||
Translation,
|
||||
FrontSide,
|
||||
ReverseSide,
|
||||
Selfie,
|
||||
};
|
||||
|
||||
struct File {
|
||||
uint64 id = 0;
|
||||
uint64 accessHash = 0;
|
||||
int32 size = 0;
|
||||
int32 dcId = 0;
|
||||
TimeId date = 0;
|
||||
bytes::vector hash;
|
||||
bytes::vector secret;
|
||||
bytes::vector encryptedSecret;
|
||||
|
||||
LoadStatus downloadStatus;
|
||||
QImage image;
|
||||
QString error;
|
||||
};
|
||||
|
||||
struct EditFile {
|
||||
EditFile(
|
||||
not_null<Main::Session*> session,
|
||||
not_null<const Value*> value,
|
||||
FileType type,
|
||||
const File &fields,
|
||||
std::unique_ptr<UploadScanData> &&uploadData);
|
||||
|
||||
not_null<const Value*> value;
|
||||
FileType type;
|
||||
File fields;
|
||||
UploadScanDataPointer uploadData;
|
||||
std::shared_ptr<bool> guard;
|
||||
bool deleted = false;
|
||||
};
|
||||
|
||||
struct ValueField {
|
||||
QString text;
|
||||
QString error;
|
||||
};
|
||||
|
||||
struct ValueMap {
|
||||
std::map<QString, ValueField> fields;
|
||||
};
|
||||
|
||||
struct ValueData {
|
||||
QByteArray original;
|
||||
bytes::vector secret;
|
||||
ValueMap parsed;
|
||||
bytes::vector hash;
|
||||
bytes::vector encryptedSecret;
|
||||
ValueMap parsedInEdit;
|
||||
bytes::vector hashInEdit;
|
||||
bytes::vector encryptedSecretInEdit;
|
||||
};
|
||||
|
||||
struct Verification {
|
||||
mtpRequestId requestId = 0;
|
||||
QString phoneCodeHash;
|
||||
int codeLength = 0;
|
||||
QString fragmentUrl;
|
||||
std::unique_ptr<Ui::SentCodeCall> call;
|
||||
|
||||
QString error;
|
||||
|
||||
};
|
||||
|
||||
struct Form;
|
||||
|
||||
struct Value {
|
||||
enum class Type {
|
||||
PersonalDetails,
|
||||
Passport,
|
||||
DriverLicense,
|
||||
IdentityCard,
|
||||
InternalPassport,
|
||||
Address,
|
||||
UtilityBill,
|
||||
BankStatement,
|
||||
RentalAgreement,
|
||||
PassportRegistration,
|
||||
TemporaryRegistration,
|
||||
Phone,
|
||||
Email,
|
||||
};
|
||||
|
||||
|
||||
explicit Value(Type type);
|
||||
Value(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(FileType type) const;
|
||||
bool requiresScan(FileType type) const;
|
||||
bool scansAreFilled() const;
|
||||
void saveInEdit(not_null<Main::Session*> session);
|
||||
void clearEditData();
|
||||
bool uploadingScan() const;
|
||||
bool saving() const;
|
||||
|
||||
static constexpr auto kNothingFilled = 0x100;
|
||||
static constexpr auto kNoTranslationFilled = 0x10;
|
||||
static constexpr auto kNoSelfieFilled = 0x001;
|
||||
int whatNotFilled() const;
|
||||
|
||||
std::vector<File> &files(FileType type);
|
||||
const std::vector<File> &files(FileType type) const;
|
||||
QString &fileMissingError(FileType type);
|
||||
const QString &fileMissingError(FileType type) const;
|
||||
std::vector<EditFile> &filesInEdit(FileType type);
|
||||
const std::vector<EditFile> &filesInEdit(FileType type) const;
|
||||
EditFile &fileInEdit(FileType type, std::optional<int> fileIndex);
|
||||
const EditFile &fileInEdit(
|
||||
FileType type,
|
||||
std::optional<int> fileIndex) const;
|
||||
|
||||
std::vector<EditFile> takeAllFilesInEdit();
|
||||
|
||||
Type type;
|
||||
ValueData data;
|
||||
std::map<FileType, File> specialScans;
|
||||
QString error;
|
||||
std::map<FileType, EditFile> specialScansInEdit;
|
||||
Verification verification;
|
||||
bytes::vector submitHash;
|
||||
|
||||
bool selfieRequired = false;
|
||||
bool translationRequired = false;
|
||||
bool nativeNames = false;
|
||||
int editScreens = 0;
|
||||
|
||||
mtpRequestId saveRequestId = 0;
|
||||
|
||||
private:
|
||||
Value &operator=(Value &&other) = default;
|
||||
|
||||
std::vector<File> _scans;
|
||||
std::vector<File> _translations;
|
||||
std::vector<EditFile> _scansInEdit;
|
||||
std::vector<EditFile> _translationsInEdit;
|
||||
QString _scanMissingError;
|
||||
QString _translationMissingError;
|
||||
|
||||
};
|
||||
|
||||
bool ValueChanged(not_null<const Value*> value, const ValueMap &data);
|
||||
|
||||
struct RequestedValue {
|
||||
explicit RequestedValue(Value::Type type);
|
||||
|
||||
Value::Type type;
|
||||
bool selfieRequired = false;
|
||||
bool translationRequired = false;
|
||||
bool nativeNames = false;
|
||||
};
|
||||
|
||||
struct RequestedRow {
|
||||
std::vector<RequestedValue> values;
|
||||
};
|
||||
|
||||
struct Form {
|
||||
using Request = std::vector<std::vector<Value::Type>>;
|
||||
|
||||
std::map<Value::Type, Value> values;
|
||||
Request request;
|
||||
QString privacyPolicyUrl;
|
||||
QVector<MTPSecureValueError> pendingErrors;
|
||||
};
|
||||
|
||||
struct PasswordSettings {
|
||||
Core::CloudPasswordCheckRequest request;
|
||||
Core::CloudPasswordAlgo newAlgo;
|
||||
Core::SecureSecretAlgo newSecureAlgo;
|
||||
QString hint;
|
||||
QString unconfirmedPattern;
|
||||
QString confirmedEmail;
|
||||
bool hasRecovery = false;
|
||||
bool notEmptyPassport = false;
|
||||
bool unknownAlgo = false;
|
||||
TimeId pendingResetDate = 0;
|
||||
|
||||
bool operator==(const PasswordSettings &other) const {
|
||||
return (request == other.request)
|
||||
// newAlgo and newSecureAlgo are always different, because they have
|
||||
// different random parts added on the client to the server salts.
|
||||
// && (newAlgo == other.newAlgo)
|
||||
// && (newSecureAlgo == other.newSecureAlgo)
|
||||
&& ((v::is_null(newAlgo) && v::is_null(other.newAlgo))
|
||||
|| (!v::is_null(newAlgo) && !v::is_null(other.newAlgo)))
|
||||
&& ((v::is_null(newSecureAlgo) && v::is_null(other.newSecureAlgo))
|
||||
|| (!v::is_null(newSecureAlgo)
|
||||
&& !v::is_null(other.newSecureAlgo)))
|
||||
&& (hint == other.hint)
|
||||
&& (unconfirmedPattern == other.unconfirmedPattern)
|
||||
&& (confirmedEmail == other.confirmedEmail)
|
||||
&& (hasRecovery == other.hasRecovery)
|
||||
&& (unknownAlgo == other.unknownAlgo)
|
||||
&& (pendingResetDate == other.pendingResetDate);
|
||||
}
|
||||
bool operator!=(const PasswordSettings &other) const {
|
||||
return !(*this == other);
|
||||
}
|
||||
};
|
||||
|
||||
struct FileKey {
|
||||
uint64 id = 0;
|
||||
|
||||
inline bool operator==(const FileKey &other) const {
|
||||
return (id == other.id);
|
||||
}
|
||||
inline bool operator!=(const FileKey &other) const {
|
||||
return !(*this == other);
|
||||
}
|
||||
inline bool operator<(const FileKey &other) const {
|
||||
return (id < other.id);
|
||||
}
|
||||
inline bool operator>(const FileKey &other) const {
|
||||
return (other < *this);
|
||||
}
|
||||
inline bool operator<=(const FileKey &other) const {
|
||||
return !(other < *this);
|
||||
}
|
||||
inline bool operator>=(const FileKey &other) const {
|
||||
return !(*this < other);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
class FormController : public base::has_weak_ptr {
|
||||
public:
|
||||
FormController(
|
||||
not_null<Window::SessionController*> controller,
|
||||
const FormRequest &request);
|
||||
|
||||
[[nodiscard]] not_null<Window::SessionController*> window() const {
|
||||
return _controller;
|
||||
}
|
||||
[[nodiscard]] Main::Session &session() const;
|
||||
|
||||
void show();
|
||||
UserData *bot() const;
|
||||
QString privacyPolicyUrl() const;
|
||||
std::vector<not_null<const Value*>> submitGetErrors();
|
||||
void submitPassword(const QByteArray &password);
|
||||
void recoverPassword();
|
||||
rpl::producer<QString> passwordError() const;
|
||||
const PasswordSettings &passwordSettings() const;
|
||||
void reloadPassword();
|
||||
void reloadAndSubmitPassword(const QByteArray &password);
|
||||
void cancelPassword();
|
||||
|
||||
bool canAddScan(not_null<const Value*> value, FileType type) const;
|
||||
void uploadScan(
|
||||
not_null<const Value*> value,
|
||||
FileType type,
|
||||
QByteArray &&content);
|
||||
void deleteScan(
|
||||
not_null<const Value*> value,
|
||||
FileType type,
|
||||
std::optional<int> fileIndex);
|
||||
void restoreScan(
|
||||
not_null<const Value*> value,
|
||||
FileType type,
|
||||
std::optional<int> fileIndex);
|
||||
|
||||
rpl::producer<> secretReadyEvents() const;
|
||||
|
||||
QString defaultEmail() const;
|
||||
QString defaultPhoneNumber() const;
|
||||
|
||||
rpl::producer<not_null<const EditFile*>> scanUpdated() const;
|
||||
rpl::producer<not_null<const Value*>> valueSaveFinished() const;
|
||||
rpl::producer<not_null<const Value*>> verificationNeeded() const;
|
||||
rpl::producer<not_null<const Value*>> verificationUpdate() const;
|
||||
void verify(not_null<const Value*> value, const QString &code);
|
||||
|
||||
const Form &form() const;
|
||||
void startValueEdit(not_null<const Value*> value);
|
||||
void cancelValueEdit(not_null<const Value*> value);
|
||||
void cancelValueVerification(not_null<const Value*> value);
|
||||
void saveValueEdit(not_null<const Value*> value, ValueMap &&data);
|
||||
void deleteValueEdit(not_null<const Value*> value);
|
||||
|
||||
void cancel();
|
||||
void cancelSure();
|
||||
|
||||
[[nodiscard]] rpl::producer<EditDocumentCountry> preferredLanguage(
|
||||
const QString &countryCode);
|
||||
|
||||
rpl::lifetime &lifetime();
|
||||
|
||||
~FormController();
|
||||
|
||||
private:
|
||||
using PasswordCheckCallback = Fn<void(
|
||||
const Core::CloudPasswordResult &check)>;
|
||||
|
||||
struct FinalData {
|
||||
QVector<MTPSecureValueHash> hashes;
|
||||
QByteArray credentials;
|
||||
std::vector<not_null<const Value*>> errors;
|
||||
};
|
||||
|
||||
template <typename Condition>
|
||||
EditFile *findEditFileByCondition(Condition &&condition);
|
||||
EditFile *findEditFile(const FullMsgId &fullId);
|
||||
EditFile *findEditFile(const FileKey &key);
|
||||
std::pair<Value*, File*> findFile(const FileKey &key);
|
||||
not_null<Value*> findValue(not_null<const Value*> value);
|
||||
|
||||
void requestForm();
|
||||
void requestPassword();
|
||||
|
||||
void formDone(const MTPaccount_AuthorizationForm &result);
|
||||
void formFail(const QString &error);
|
||||
bool parseForm(const MTPaccount_AuthorizationForm &result);
|
||||
void showForm();
|
||||
Value parseValue(
|
||||
const MTPSecureValue &value,
|
||||
const std::vector<EditFile> &editData = {}) const;
|
||||
std::vector<File> parseFiles(
|
||||
const QVector<MTPSecureFile> &data,
|
||||
const std::vector<EditFile> &editData) const;
|
||||
std::optional<File> parseFile(
|
||||
const MTPSecureFile &data,
|
||||
const std::vector<EditFile> &editData) const;
|
||||
void fillDownloadedFile(
|
||||
File &destination,
|
||||
const std::vector<EditFile> &source) const;
|
||||
bool handleAppUpdateError(const QString &error);
|
||||
|
||||
void submitPassword(
|
||||
const Core::CloudPasswordResult &check,
|
||||
const QByteArray &password,
|
||||
bool submitSaved);
|
||||
void checkPasswordHash(
|
||||
mtpRequestId &guard,
|
||||
bytes::vector hash,
|
||||
PasswordCheckCallback callback);
|
||||
bool handleSrpIdInvalid(mtpRequestId &guard);
|
||||
void requestPasswordData(mtpRequestId &guard);
|
||||
void passwordChecked();
|
||||
void passwordServerError();
|
||||
void passwordDone(const MTPaccount_Password &result);
|
||||
bool applyPassword(const MTPDaccount_password &settings);
|
||||
bool applyPassword(PasswordSettings &&settings);
|
||||
bytes::vector passwordHashForAuth(bytes::const_span password) const;
|
||||
void checkSavedPasswordSettings(const SavedCredentials &credentials);
|
||||
void checkSavedPasswordSettings(
|
||||
const Core::CloudPasswordResult &check,
|
||||
const SavedCredentials &credentials);
|
||||
void validateSecureSecret(
|
||||
bytes::const_span encryptedSecret,
|
||||
bytes::const_span passwordHashForSecret,
|
||||
bytes::const_span passwordBytes,
|
||||
uint64 serverSecretId);
|
||||
void decryptValues();
|
||||
void decryptValue(Value &value) const;
|
||||
bool validateValueSecrets(Value &value) const;
|
||||
void resetValue(Value &value) const;
|
||||
void fillErrors();
|
||||
void fillNativeFromFallback();
|
||||
|
||||
void loadFile(File &file);
|
||||
void fileLoadDone(FileKey key, const QByteArray &bytes);
|
||||
void fileLoadProgress(FileKey key, int offset);
|
||||
void fileLoadFail(FileKey key);
|
||||
void generateSecret(bytes::const_span password);
|
||||
void saveSecret(
|
||||
const Core::CloudPasswordResult &check,
|
||||
const SavedCredentials &saved,
|
||||
const bytes::vector &secret);
|
||||
|
||||
void subscribeToUploader();
|
||||
void encryptFile(
|
||||
EditFile &file,
|
||||
QByteArray &&content,
|
||||
Fn<void(UploadScanData &&result)> callback);
|
||||
void prepareFile(
|
||||
EditFile &file,
|
||||
const QByteArray &content);
|
||||
void uploadEncryptedFile(
|
||||
EditFile &file,
|
||||
UploadScanData &&data);
|
||||
void scanUploadDone(const Storage::UploadSecureDone &data);
|
||||
void scanUploadProgress(const Storage::UploadSecureProgress &data);
|
||||
void scanUploadFail(const FullMsgId &fullId);
|
||||
void scanDeleteRestore(
|
||||
not_null<const Value*> value,
|
||||
FileType type,
|
||||
std::optional<int> fileIndex,
|
||||
bool deleted);
|
||||
|
||||
QString getPhoneFromValue(not_null<const Value*> value) const;
|
||||
QString getEmailFromValue(not_null<const Value*> value) const;
|
||||
QString getPlainTextFromValue(not_null<const Value*> value) const;
|
||||
void startPhoneVerification(not_null<Value*> value);
|
||||
void startEmailVerification(not_null<Value*> value);
|
||||
void valueSaveShowError(not_null<Value*> value, const MTP::Error &error);
|
||||
void valueSaveFailed(not_null<Value*> value);
|
||||
void requestPhoneCall(not_null<Value*> value);
|
||||
void verificationError(
|
||||
not_null<Value*> value,
|
||||
const QString &text);
|
||||
void valueEditFailed(not_null<Value*> value);
|
||||
void clearValueEdit(not_null<Value*> value);
|
||||
void clearValueVerification(not_null<Value*> value);
|
||||
|
||||
bool isEncryptedValue(Value::Type type) const;
|
||||
void saveEncryptedValue(not_null<Value*> value);
|
||||
void savePlainTextValue(not_null<Value*> value);
|
||||
void sendSaveRequest(
|
||||
not_null<Value*> value,
|
||||
const MTPInputSecureValue &data);
|
||||
FinalData prepareFinalData();
|
||||
|
||||
void suggestReset(bytes::vector password);
|
||||
void resetSecret(
|
||||
const Core::CloudPasswordResult &check,
|
||||
const bytes::vector &password);
|
||||
void suggestRestart();
|
||||
void cancelAbort();
|
||||
void shortPollEmailConfirmation();
|
||||
|
||||
not_null<Window::SessionController*> _controller;
|
||||
MTP::Sender _api;
|
||||
FormRequest _request;
|
||||
UserData *_bot = nullptr;
|
||||
|
||||
mtpRequestId _formRequestId = 0;
|
||||
mtpRequestId _passwordRequestId = 0;
|
||||
mtpRequestId _passwordCheckRequestId = 0;
|
||||
|
||||
PasswordSettings _password;
|
||||
crl::time _lastSrpIdInvalidTime = 0;
|
||||
bytes::vector _passwordCheckHash;
|
||||
PasswordCheckCallback _passwordCheckCallback;
|
||||
QByteArray _savedPasswordValue;
|
||||
Form _form;
|
||||
bool _cancelled = false;
|
||||
mtpRequestId _recoverRequestId = 0;
|
||||
base::flat_map<FileKey, std::unique_ptr<mtpFileLoader>> _fileLoaders;
|
||||
|
||||
struct {
|
||||
int32 hash = 0;
|
||||
std::map<QString, QString> languagesByCountryCode;
|
||||
} _passportConfig;
|
||||
|
||||
rpl::event_stream<not_null<const EditFile*>> _scanUpdated;
|
||||
rpl::event_stream<not_null<const Value*>> _valueSaveFinished;
|
||||
rpl::event_stream<not_null<const Value*>> _verificationNeeded;
|
||||
rpl::event_stream<not_null<const Value*>> _verificationUpdate;
|
||||
|
||||
bytes::vector _secret;
|
||||
uint64 _secretId = 0;
|
||||
std::vector<Fn<void()>> _secretCallbacks;
|
||||
mtpRequestId _saveSecretRequestId = 0;
|
||||
rpl::event_stream<> _secretReady;
|
||||
rpl::event_stream<QString> _passwordError;
|
||||
mtpRequestId _submitRequestId = 0;
|
||||
bool _submitSuccess = false;
|
||||
bool _suggestingRestart = false;
|
||||
QString _serviceErrorText;
|
||||
base::Timer _shortPollTimer;
|
||||
|
||||
rpl::lifetime _uploaderSubscriptions;
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
std::unique_ptr<ViewController> _view;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Passport
|
||||
91
Telegram/SourceFiles/passport/passport_form_row.cpp
Normal file
91
Telegram/SourceFiles/passport/passport_form_row.cpp
Normal file
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
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_form_row.h"
|
||||
|
||||
#include "ui/wrap/fade_wrap.h"
|
||||
#include "ui/text_options.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_passport.h"
|
||||
|
||||
namespace Passport {
|
||||
|
||||
FormRow::FormRow(
|
||||
QWidget *parent,
|
||||
const QString &title,
|
||||
const QString &description)
|
||||
: RippleButton(parent, st::passportRowRipple)
|
||||
, _title(
|
||||
st::semiboldTextStyle,
|
||||
title,
|
||||
Ui::NameTextOptions(),
|
||||
st::boxWideWidth / 2)
|
||||
, _description(
|
||||
st::defaultTextStyle,
|
||||
description,
|
||||
Ui::NameTextOptions(),
|
||||
st::boxWideWidth / 2) {
|
||||
}
|
||||
|
||||
void FormRow::setReady(bool ready) {
|
||||
_ready = ready;
|
||||
resizeToWidth(width());
|
||||
update();
|
||||
}
|
||||
|
||||
int FormRow::resizeGetHeight(int newWidth) {
|
||||
const auto availableWidth = countAvailableWidth(newWidth);
|
||||
_titleHeight = _title.countHeight(availableWidth);
|
||||
_descriptionHeight = _description.countHeight(availableWidth);
|
||||
const auto result = st::passportRowPadding.top()
|
||||
+ _titleHeight
|
||||
+ st::passportRowSkip
|
||||
+ _descriptionHeight
|
||||
+ st::passportRowPadding.bottom();
|
||||
return result;
|
||||
}
|
||||
|
||||
int FormRow::countAvailableWidth(int newWidth) const {
|
||||
return newWidth
|
||||
- st::passportRowPadding.left()
|
||||
- st::passportRowPadding.right()
|
||||
- (_ready ? st::passportRowReadyIcon : st::passportRowEmptyIcon).width();
|
||||
}
|
||||
|
||||
int FormRow::countAvailableWidth() const {
|
||||
return countAvailableWidth(width());
|
||||
}
|
||||
|
||||
void FormRow::paintEvent(QPaintEvent *e) {
|
||||
Painter p(this);
|
||||
|
||||
const auto ms = getms();
|
||||
paintRipple(p, 0, 0, ms);
|
||||
|
||||
const auto left = st::passportRowPadding.left();
|
||||
const auto availableWidth = countAvailableWidth();
|
||||
auto top = st::passportRowPadding.top();
|
||||
|
||||
p.setPen(st::passportRowTitleFg);
|
||||
_title.drawLeft(p, left, top, availableWidth, width());
|
||||
top += _titleHeight + st::passportRowSkip;
|
||||
|
||||
p.setPen(st::passportRowDescriptionFg);
|
||||
_description.drawLeft(p, left, top, availableWidth, width());
|
||||
top += _descriptionHeight + st::passportRowPadding.bottom();
|
||||
|
||||
const auto &icon = _ready
|
||||
? st::passportRowReadyIcon
|
||||
: st::passportRowEmptyIcon;
|
||||
icon.paint(
|
||||
p,
|
||||
width() - st::passportRowPadding.right() - icon.width(),
|
||||
(height() - icon.height()) / 2,
|
||||
width());
|
||||
}
|
||||
|
||||
} // namespace Passport
|
||||
19
Telegram/SourceFiles/passport/passport_form_row.h
Normal file
19
Telegram/SourceFiles/passport/passport_form_row.h
Normal file
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/widgets/buttons.h"
|
||||
|
||||
namespace Ui {
|
||||
template <typename Widget>
|
||||
class FadeWrapScaled;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Passport {
|
||||
|
||||
} // namespace Passport
|
||||
565
Telegram/SourceFiles/passport/passport_form_view_controller.cpp
Normal file
565
Telegram/SourceFiles/passport/passport_form_view_controller.cpp
Normal file
@@ -0,0 +1,565 @@
|
||||
/*
|
||||
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_form_view_controller.h"
|
||||
|
||||
#include "passport/passport_form_controller.h"
|
||||
#include "passport/passport_panel_edit_document.h"
|
||||
#include "passport/passport_panel_edit_contact.h"
|
||||
#include "passport/passport_panel_controller.h"
|
||||
#include "lang/lang_keys.h"
|
||||
|
||||
namespace Passport {
|
||||
namespace {
|
||||
|
||||
std::map<Value::Type, Scope::Type> ScopeTypesMap() {
|
||||
return {
|
||||
{ Value::Type::PersonalDetails, Scope::Type::PersonalDetails },
|
||||
{ Value::Type::Passport, Scope::Type::Identity },
|
||||
{ Value::Type::DriverLicense, Scope::Type::Identity },
|
||||
{ Value::Type::IdentityCard, Scope::Type::Identity },
|
||||
{ Value::Type::InternalPassport, Scope::Type::Identity },
|
||||
{ Value::Type::Address, Scope::Type::AddressDetails },
|
||||
{ Value::Type::UtilityBill, Scope::Type::Address },
|
||||
{ Value::Type::BankStatement, Scope::Type::Address },
|
||||
{ Value::Type::RentalAgreement, Scope::Type::Address },
|
||||
{ Value::Type::PassportRegistration, Scope::Type::Address },
|
||||
{ Value::Type::TemporaryRegistration, Scope::Type::Address },
|
||||
{ Value::Type::Phone, Scope::Type::Phone },
|
||||
{ Value::Type::Email, Scope::Type::Email },
|
||||
};
|
||||
}
|
||||
|
||||
Scope::Type ScopeTypeForValueType(Value::Type type) {
|
||||
static const auto map = ScopeTypesMap();
|
||||
const auto i = map.find(type);
|
||||
Assert(i != map.end());
|
||||
return i->second;
|
||||
}
|
||||
|
||||
std::map<Scope::Type, Value::Type> ScopeDetailsMap() {
|
||||
return {
|
||||
{ Scope::Type::PersonalDetails, Value::Type::PersonalDetails },
|
||||
{ Scope::Type::Identity, Value::Type::PersonalDetails },
|
||||
{ Scope::Type::AddressDetails, Value::Type::Address },
|
||||
{ Scope::Type::Address, Value::Type::Address },
|
||||
{ Scope::Type::Phone, Value::Type::Phone },
|
||||
{ Scope::Type::Email, Value::Type::Email },
|
||||
};
|
||||
}
|
||||
|
||||
Value::Type DetailsTypeForScopeType(Scope::Type type) {
|
||||
static const auto map = ScopeDetailsMap();
|
||||
const auto i = map.find(type);
|
||||
Assert(i != map.end());
|
||||
return i->second;
|
||||
}
|
||||
|
||||
bool InlineDetails(
|
||||
const Form::Request &request,
|
||||
Scope::Type into,
|
||||
Value::Type details) {
|
||||
const auto count = ranges::count_if(
|
||||
request,
|
||||
[&](const std::vector<Value::Type> &types) {
|
||||
Expects(!types.empty());
|
||||
|
||||
return ScopeTypeForValueType(types[0]) == into;
|
||||
});
|
||||
if (count != 1) {
|
||||
return false;
|
||||
}
|
||||
return ranges::any_of(
|
||||
request,
|
||||
[&](const std::vector<Value::Type> &types) {
|
||||
Expects(!types.empty());
|
||||
|
||||
return (types[0] == details);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
bool InlineDetails(const Form::Request &request, Value::Type details) {
|
||||
if (details == Value::Type::PersonalDetails) {
|
||||
return InlineDetails(request, Scope::Type::Identity, details);
|
||||
} else if (details == Value::Type::Address) {
|
||||
return InlineDetails(request, Scope::Type::Address, details);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
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) {
|
||||
base::flat_set<Value::Type> values;
|
||||
for (const auto &requested : form.request) {
|
||||
if (requested.empty()) {
|
||||
LOG(("API Error: Empty types list in authorization form row."));
|
||||
return false;
|
||||
}
|
||||
const auto scopeType = ScopeTypeForValueType(requested[0]);
|
||||
const auto ownsDetails = (scopeType != Scope::Type::Identity
|
||||
&& scopeType != Scope::Type::Address);
|
||||
if (ownsDetails && requested.size() != 1) {
|
||||
LOG(("API Error: Large types list in authorization form row."));
|
||||
return false;
|
||||
}
|
||||
for (const auto type : requested) {
|
||||
if (values.contains(type)) {
|
||||
LOG(("API Error: Value twice in authorization form row."));
|
||||
return false;
|
||||
}
|
||||
values.emplace(type);
|
||||
}
|
||||
}
|
||||
|
||||
// Invalid errors should be skipped while parsing the form.
|
||||
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 (!value.requiresScan(FileType::Scan)) {
|
||||
for (const auto &scan : value.files(FileType::Scan)) {
|
||||
Assert(scan.error.isEmpty());
|
||||
}
|
||||
Assert(value.fileMissingError(FileType::Scan).isEmpty());
|
||||
}
|
||||
if (!value.requiresScan(FileType::Translation)) {
|
||||
for (const auto &scan : value.files(FileType::Translation)) {
|
||||
Assert(scan.error.isEmpty());
|
||||
}
|
||||
Assert(value.fileMissingError(FileType::Translation).isEmpty());
|
||||
}
|
||||
for (const auto &[type, specialScan] : value.specialScans) {
|
||||
if (!value.requiresSpecialScan(type)) {
|
||||
Assert(specialScan.error.isEmpty());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<Scope> ComputeScopes(const Form &form) {
|
||||
auto result = std::vector<Scope>();
|
||||
const auto findValue = [&](const Value::Type type) {
|
||||
const auto i = form.values.find(type);
|
||||
Assert(i != form.values.end());
|
||||
return &i->second;
|
||||
};
|
||||
for (const auto &requested : form.request) {
|
||||
Assert(!requested.empty());
|
||||
const auto scopeType = ScopeTypeForValueType(requested[0]);
|
||||
const auto detailsType = DetailsTypeForScopeType(scopeType);
|
||||
const auto ownsDetails = (scopeType != Scope::Type::Identity
|
||||
&& scopeType != Scope::Type::Address);
|
||||
const auto inlineDetails = InlineDetails(form.request, detailsType);
|
||||
if (ownsDetails && inlineDetails) {
|
||||
continue;
|
||||
}
|
||||
result.push_back(Scope(scopeType));
|
||||
auto &scope = result.back();
|
||||
scope.details = (ownsDetails || inlineDetails)
|
||||
? findValue(detailsType)
|
||||
: nullptr;
|
||||
if (ownsDetails) {
|
||||
Assert(requested.size() == 1);
|
||||
} else {
|
||||
for (const auto type : requested) {
|
||||
scope.documents.push_back(findValue(type));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
QString JoinScopeRowReadyString(
|
||||
std::vector<std::pair<QString, QString>> &&values) {
|
||||
using Pair = std::pair<QString, QString>;
|
||||
|
||||
if (values.empty()) {
|
||||
return QString();
|
||||
}
|
||||
auto result = QString();
|
||||
auto size = ranges::accumulate(
|
||||
values,
|
||||
0,
|
||||
ranges::plus(),
|
||||
[](const Pair &v) { return v.second.size(); });
|
||||
result.reserve(size + (values.size() - 1) * 2);
|
||||
for (const auto &pair : values) {
|
||||
if (pair.second.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (!result.isEmpty()) {
|
||||
result.append(", ");
|
||||
}
|
||||
result.append(pair.second);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
ScopeRow DocumentRowByType(Value::Type type) {
|
||||
using Type = Value::Type;
|
||||
switch (type) {
|
||||
case Type::Passport:
|
||||
return {
|
||||
tr::lng_passport_identity_passport(tr::now),
|
||||
tr::lng_passport_identity_passport_upload(tr::now),
|
||||
};
|
||||
case Type::DriverLicense:
|
||||
return {
|
||||
tr::lng_passport_identity_license(tr::now),
|
||||
tr::lng_passport_identity_license_upload(tr::now),
|
||||
};
|
||||
case Type::IdentityCard:
|
||||
return {
|
||||
tr::lng_passport_identity_card(tr::now),
|
||||
tr::lng_passport_identity_card_upload(tr::now),
|
||||
};
|
||||
case Type::InternalPassport:
|
||||
return {
|
||||
tr::lng_passport_identity_internal(tr::now),
|
||||
tr::lng_passport_identity_internal_upload(tr::now),
|
||||
};
|
||||
case Type::BankStatement:
|
||||
return {
|
||||
tr::lng_passport_address_statement(tr::now),
|
||||
tr::lng_passport_address_statement_upload(tr::now),
|
||||
};
|
||||
case Type::UtilityBill:
|
||||
return {
|
||||
tr::lng_passport_address_bill(tr::now),
|
||||
tr::lng_passport_address_bill_upload(tr::now),
|
||||
};
|
||||
case Type::RentalAgreement:
|
||||
return {
|
||||
tr::lng_passport_address_agreement(tr::now),
|
||||
tr::lng_passport_address_agreement_upload(tr::now),
|
||||
};
|
||||
case Type::PassportRegistration:
|
||||
return {
|
||||
tr::lng_passport_address_registration(tr::now),
|
||||
tr::lng_passport_address_registration_upload(tr::now),
|
||||
};
|
||||
case Type::TemporaryRegistration:
|
||||
return {
|
||||
tr::lng_passport_address_temporary(tr::now),
|
||||
tr::lng_passport_address_temporary_upload(tr::now),
|
||||
};
|
||||
default: Unexpected("Value type in DocumentRowByType.");
|
||||
}
|
||||
}
|
||||
|
||||
QString DocumentName(Value::Type type) {
|
||||
return DocumentRowByType(type).title;
|
||||
}
|
||||
|
||||
ScopeRow DocumentsOneOfRow(
|
||||
const Scope &scope,
|
||||
const QString &severalTitle,
|
||||
const QString &severalDescription) {
|
||||
Expects(!scope.documents.empty());
|
||||
|
||||
const auto &documents = scope.documents;
|
||||
if (documents.size() == 1) {
|
||||
const auto type = documents.front()->type;
|
||||
return DocumentRowByType(type);
|
||||
} else if (documents.size() == 2) {
|
||||
const auto type1 = documents.front()->type;
|
||||
const auto type2 = documents.back()->type;
|
||||
return {
|
||||
tr::lng_passport_or_title(
|
||||
tr::now,
|
||||
lt_document,
|
||||
DocumentName(type1),
|
||||
lt_second_document,
|
||||
DocumentName(type2)),
|
||||
severalDescription,
|
||||
};
|
||||
}
|
||||
return {
|
||||
severalTitle,
|
||||
severalDescription,
|
||||
};
|
||||
}
|
||||
|
||||
QString ComputeScopeRowReadyString(const Scope &scope) {
|
||||
switch (scope.type) {
|
||||
case Scope::Type::PersonalDetails:
|
||||
case Scope::Type::Identity:
|
||||
case Scope::Type::AddressDetails:
|
||||
case Scope::Type::Address: {
|
||||
auto list = std::vector<std::pair<QString, QString>>();
|
||||
const auto pushListValue = [&](
|
||||
const QString &key,
|
||||
const QString &value,
|
||||
const QString &keyForAttachmentTo = QString()) {
|
||||
if (keyForAttachmentTo.isEmpty()) {
|
||||
list.push_back({ key, value.trimmed() });
|
||||
} else {
|
||||
const auto i = ranges::find(
|
||||
list,
|
||||
keyForAttachmentTo,
|
||||
[](const std::pair<QString, QString> &value) {
|
||||
return value.first;
|
||||
});
|
||||
Assert(i != end(list));
|
||||
if (const auto data = value.trimmed(); !data.isEmpty()) {
|
||||
if (i->second.isEmpty()) {
|
||||
i->second = data;
|
||||
} else {
|
||||
i->second += ' ' + data;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const auto fields = scope.details
|
||||
? &scope.details->data.parsed.fields
|
||||
: nullptr;
|
||||
const auto document = [&]() -> const Value* {
|
||||
for (const auto &document : scope.documents) {
|
||||
if (document->scansAreFilled()) {
|
||||
return document;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}();
|
||||
if (!scope.documents.empty() && !document) {
|
||||
return QString();
|
||||
}
|
||||
if ((document && scope.documents.size() > 1)
|
||||
|| (!scope.details
|
||||
&& (ScopeTypeForValueType(document->type)
|
||||
== Scope::Type::Address))) {
|
||||
pushListValue("_type", DocumentName(document->type));
|
||||
}
|
||||
const auto scheme = GetDocumentScheme(
|
||||
scope.type,
|
||||
document ? base::make_optional(document->type) : std::nullopt,
|
||||
scope.details ? scope.details->nativeNames : false,
|
||||
nullptr);
|
||||
using ValueClass = EditDocumentScheme::ValueClass;
|
||||
const auto skipAdditional = [&] {
|
||||
if (!fields) {
|
||||
return false;
|
||||
}
|
||||
for (const auto &row : scheme.rows) {
|
||||
if (row.valueClass == ValueClass::Additional) {
|
||||
const auto i = fields->find(row.key);
|
||||
const auto native = (i == end(*fields))
|
||||
? QString()
|
||||
: i->second.text;
|
||||
const auto j = fields->find(row.additionalFallbackKey);
|
||||
const auto latin = (j == end(*fields))
|
||||
? QString()
|
||||
: j->second.text;
|
||||
if (latin != native) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}();
|
||||
for (const auto &row : scheme.rows) {
|
||||
const auto format = row.format;
|
||||
if (row.valueClass != ValueClass::Scans) {
|
||||
if (!fields) {
|
||||
continue;
|
||||
} else if (row.valueClass == ValueClass::Additional
|
||||
&& skipAdditional) {
|
||||
continue;
|
||||
}
|
||||
const auto i = fields->find(row.key);
|
||||
const auto text = (i == end(*fields))
|
||||
? QString()
|
||||
: i->second.text;
|
||||
if (row.error && row.error(text).has_value()) {
|
||||
return QString();
|
||||
}
|
||||
pushListValue(
|
||||
row.key,
|
||||
format ? format(text) : text,
|
||||
row.keyForAttachmentTo);
|
||||
} else if (scope.documents.empty()) {
|
||||
continue;
|
||||
} else {
|
||||
const auto i = document->data.parsed.fields.find(row.key);
|
||||
const auto text = (i == end(document->data.parsed.fields))
|
||||
? QString()
|
||||
: i->second.text;
|
||||
if (row.error && row.error(text).has_value()) {
|
||||
return QString();
|
||||
}
|
||||
pushListValue(row.key, text, row.keyForAttachmentTo);
|
||||
}
|
||||
}
|
||||
return JoinScopeRowReadyString(std::move(list));
|
||||
} break;
|
||||
case Scope::Type::Phone:
|
||||
case Scope::Type::Email: {
|
||||
Assert(scope.details != nullptr);
|
||||
const auto format = GetContactScheme(scope.type).format;
|
||||
const auto &fields = scope.details->data.parsed.fields;
|
||||
const auto i = fields.find("value");
|
||||
return (i != end(fields))
|
||||
? (format ? format(i->second.text) : i->second.text)
|
||||
: QString();
|
||||
} break;
|
||||
}
|
||||
Unexpected("Scope type in ComputeScopeRowReadyString.");
|
||||
}
|
||||
|
||||
ScopeRow ComputeScopeRow(const Scope &scope) {
|
||||
const auto addReadyError = [&](
|
||||
ScopeRow &&row,
|
||||
QString titleFallback = QString()) {
|
||||
row.ready = ComputeScopeRowReadyString(scope);
|
||||
auto errors = QStringList();
|
||||
const auto addValueErrors = [&](not_null<const Value*> value) {
|
||||
if (!value->error.isEmpty()) {
|
||||
errors.push_back(value->error);
|
||||
}
|
||||
const auto addTypeErrors = [&](FileType type) {
|
||||
if (!value->fileMissingError(type).isEmpty()) {
|
||||
errors.push_back(value->fileMissingError(type));
|
||||
}
|
||||
for (const auto &scan : value->files(type)) {
|
||||
if (!scan.error.isEmpty()) {
|
||||
errors.push_back(scan.error);
|
||||
}
|
||||
}
|
||||
};
|
||||
addTypeErrors(FileType::Scan);
|
||||
addTypeErrors(FileType::Translation);
|
||||
for (const auto &[type, scan] : value->specialScans) {
|
||||
if (!scan.error.isEmpty()) {
|
||||
errors.push_back(scan.error);
|
||||
}
|
||||
}
|
||||
for (const auto &[key, value] : value->data.parsed.fields) {
|
||||
if (!value.error.isEmpty()) {
|
||||
errors.push_back(value.error);
|
||||
}
|
||||
}
|
||||
};
|
||||
const auto document = [&]() -> const Value* {
|
||||
for (const auto &document : scope.documents) {
|
||||
if (document->scansAreFilled()) {
|
||||
return document;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}();
|
||||
if (document) {
|
||||
addValueErrors(document);
|
||||
}
|
||||
if (scope.details) {
|
||||
addValueErrors(scope.details);
|
||||
}
|
||||
if (!errors.isEmpty()) {
|
||||
row.error = errors[0];// errors.join('\n');
|
||||
} else if (row.title == row.ready && !titleFallback.isEmpty()) {
|
||||
row.title = titleFallback;
|
||||
}
|
||||
|
||||
if (row.error.isEmpty()
|
||||
&& row.ready.isEmpty()
|
||||
&& !scope.documents.empty()) {
|
||||
if (document) {
|
||||
row.description = (scope.type == Scope::Type::Identity)
|
||||
? tr::lng_passport_personal_details_enter(tr::now)
|
||||
: tr::lng_passport_address_enter(tr::now);
|
||||
} else {
|
||||
const auto best = ranges::min(
|
||||
scope.documents,
|
||||
std::less<>(),
|
||||
[](not_null<const Value*> document) {
|
||||
return document->whatNotFilled();
|
||||
});
|
||||
const auto notFilled = best->whatNotFilled();
|
||||
if (notFilled & Value::kNoTranslationFilled) {
|
||||
row.description = tr::lng_passport_translation_needed(tr::now);
|
||||
} else if (notFilled & Value::kNoSelfieFilled) {
|
||||
row.description = tr::lng_passport_identity_selfie(tr::now);
|
||||
}
|
||||
}
|
||||
}
|
||||
return std::move(row);
|
||||
};
|
||||
switch (scope.type) {
|
||||
case Scope::Type::PersonalDetails:
|
||||
return addReadyError({
|
||||
tr::lng_passport_personal_details(tr::now),
|
||||
tr::lng_passport_personal_details_enter(tr::now),
|
||||
});
|
||||
case Scope::Type::Identity:
|
||||
return addReadyError(DocumentsOneOfRow(
|
||||
scope,
|
||||
tr::lng_passport_identity_title(tr::now),
|
||||
tr::lng_passport_identity_description(tr::now)));
|
||||
case Scope::Type::AddressDetails:
|
||||
return addReadyError({
|
||||
tr::lng_passport_address(tr::now),
|
||||
tr::lng_passport_address_enter(tr::now),
|
||||
});
|
||||
case Scope::Type::Address:
|
||||
return addReadyError(DocumentsOneOfRow(
|
||||
scope,
|
||||
tr::lng_passport_address_title(tr::now),
|
||||
tr::lng_passport_address_description(tr::now)));
|
||||
case Scope::Type::Phone:
|
||||
return addReadyError({
|
||||
tr::lng_passport_phone_title(tr::now),
|
||||
tr::lng_passport_phone_description(tr::now),
|
||||
});
|
||||
case Scope::Type::Email:
|
||||
return addReadyError({
|
||||
tr::lng_passport_email_title(tr::now),
|
||||
tr::lng_passport_email_description(tr::now),
|
||||
});
|
||||
default: Unexpected("Scope type in ComputeScopeRow.");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Passport
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "passport/passport_form_controller.h"
|
||||
#include "base/object_ptr.h"
|
||||
#include "ui/layers/box_content.h"
|
||||
|
||||
namespace Passport {
|
||||
|
||||
struct Scope {
|
||||
enum class Type {
|
||||
PersonalDetails,
|
||||
Identity,
|
||||
AddressDetails,
|
||||
Address,
|
||||
Phone,
|
||||
Email,
|
||||
};
|
||||
explicit Scope(Type type);
|
||||
|
||||
Type type;
|
||||
const Value *details = nullptr;
|
||||
std::vector<not_null<const Value*>> documents;
|
||||
};
|
||||
|
||||
struct ScopeRow {
|
||||
QString title;
|
||||
QString description;
|
||||
QString ready;
|
||||
QString error;
|
||||
};
|
||||
|
||||
bool CanHaveErrors(Value::Type type);
|
||||
bool ValidateForm(const Form &form);
|
||||
std::vector<Scope> ComputeScopes(const Form &form);
|
||||
QString ComputeScopeRowReadyString(const Scope &scope);
|
||||
ScopeRow ComputeScopeRow(const Scope &scope);
|
||||
|
||||
class ViewController {
|
||||
public:
|
||||
virtual void showAskPassword() = 0;
|
||||
virtual void showNoPassword() = 0;
|
||||
virtual void showCriticalError(const QString &error) = 0;
|
||||
virtual void showUpdateAppBox() = 0;
|
||||
virtual void editScope(int index) = 0;
|
||||
|
||||
virtual void showBox(
|
||||
object_ptr<Ui::BoxContent> box,
|
||||
Ui::LayerOptions options,
|
||||
anim::type animated) = 0;
|
||||
virtual void showToast(const QString &text) = 0;
|
||||
virtual void suggestReset(Fn<void()> callback) = 0;
|
||||
|
||||
virtual int closeGetDuration() = 0;
|
||||
|
||||
virtual ~ViewController() {
|
||||
}
|
||||
|
||||
template <typename BoxType>
|
||||
base::weak_qptr<BoxType> show(
|
||||
object_ptr<BoxType> box,
|
||||
Ui::LayerOptions options = Ui::LayerOption::KeepOther,
|
||||
anim::type animated = anim::type::normal) {
|
||||
auto result = base::weak_qptr<BoxType>(box.data());
|
||||
showBox(std::move(box), options, animated);
|
||||
return result;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
} // namespace Passport
|
||||
111
Telegram/SourceFiles/passport/passport_panel.cpp
Normal file
111
Telegram/SourceFiles/passport/passport_panel.cpp
Normal file
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
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.h"
|
||||
|
||||
#include "passport/passport_panel_controller.h"
|
||||
#include "passport/passport_panel_form.h"
|
||||
#include "passport/passport_panel_password.h"
|
||||
#include "ui/widgets/separate_panel.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/wrap/padding_wrap.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "styles/style_passport.h"
|
||||
#include "styles/style_widgets.h"
|
||||
#include "styles/style_calls.h"
|
||||
|
||||
namespace Passport {
|
||||
|
||||
Panel::Panel(not_null<PanelController*> controller)
|
||||
: _controller(controller)
|
||||
, _widget(std::make_unique<Ui::SeparatePanel>(Ui::SeparatePanelArgs{
|
||||
.onAllSpaces = true,
|
||||
})) {
|
||||
_widget->setTitle(tr::lng_passport_title());
|
||||
_widget->setInnerSize(st::passportPanelSize);
|
||||
|
||||
_widget->closeRequests(
|
||||
) | rpl::on_next([=] {
|
||||
_controller->cancelAuth();
|
||||
}, _widget->lifetime());
|
||||
|
||||
_widget->closeEvents(
|
||||
) | rpl::on_next([=] {
|
||||
_controller->cancelAuthSure();
|
||||
}, _widget->lifetime());
|
||||
}
|
||||
|
||||
rpl::producer<> Panel::backRequests() const {
|
||||
return _widget->backRequests();
|
||||
}
|
||||
|
||||
void Panel::setBackAllowed(bool allowed) {
|
||||
_widget->setBackAllowed(allowed);
|
||||
}
|
||||
|
||||
not_null<Ui::RpWidget*> Panel::widget() const {
|
||||
return _widget.get();
|
||||
}
|
||||
|
||||
int Panel::hideAndDestroyGetDuration() {
|
||||
return _widget->hideGetDuration();
|
||||
}
|
||||
|
||||
void Panel::showAskPassword() {
|
||||
_widget->showInner(
|
||||
base::make_unique_q<PanelAskPassword>(_widget.get(), _controller));
|
||||
setBackAllowed(false);
|
||||
}
|
||||
|
||||
void Panel::showNoPassword() {
|
||||
_widget->showInner(
|
||||
base::make_unique_q<PanelNoPassword>(_widget.get(), _controller));
|
||||
setBackAllowed(false);
|
||||
}
|
||||
|
||||
void Panel::showCriticalError(const QString &error) {
|
||||
auto container = base::make_unique_q<Ui::PaddingWrap<Ui::FlatLabel>>(
|
||||
_widget.get(),
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
_widget.get(),
|
||||
error,
|
||||
st::passportErrorLabel),
|
||||
style::margins(0, st::passportPanelSize.height() / 3, 0, 0));
|
||||
container->widthValue(
|
||||
) | rpl::on_next([label = container->entity()](int width) {
|
||||
label->resize(width, label->height());
|
||||
}, container->lifetime());
|
||||
|
||||
_widget->showInner(std::move(container));
|
||||
setBackAllowed(false);
|
||||
}
|
||||
|
||||
void Panel::showForm() {
|
||||
_widget->showInner(
|
||||
base::make_unique_q<PanelForm>(_widget.get(), _controller));
|
||||
setBackAllowed(false);
|
||||
}
|
||||
|
||||
void Panel::showEditValue(object_ptr<Ui::RpWidget> from) {
|
||||
_widget->showInner(base::unique_qptr<Ui::RpWidget>(from.data()));
|
||||
}
|
||||
|
||||
void Panel::showBox(
|
||||
object_ptr<Ui::BoxContent> box,
|
||||
Ui::LayerOptions options,
|
||||
anim::type animated) {
|
||||
_widget->showBox(std::move(box), options, animated);
|
||||
_widget->showAndActivate();
|
||||
}
|
||||
|
||||
void Panel::showToast(const QString &text) {
|
||||
_widget->showToast({ text });
|
||||
}
|
||||
|
||||
Panel::~Panel() = default;
|
||||
|
||||
} // namespace Passport
|
||||
52
Telegram/SourceFiles/passport/passport_panel.h
Normal file
52
Telegram/SourceFiles/passport/passport_panel.h
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/layers/layer_widget.h"
|
||||
|
||||
namespace Ui {
|
||||
class RpWidget;
|
||||
class SeparatePanel;
|
||||
class BoxContent;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Passport {
|
||||
|
||||
class PanelController;
|
||||
|
||||
class Panel {
|
||||
public:
|
||||
Panel(not_null<PanelController*> controller);
|
||||
|
||||
int hideAndDestroyGetDuration();
|
||||
|
||||
void showAskPassword();
|
||||
void showNoPassword();
|
||||
void showForm();
|
||||
void showCriticalError(const QString &error);
|
||||
void showEditValue(object_ptr<Ui::RpWidget> form);
|
||||
void showBox(
|
||||
object_ptr<Ui::BoxContent> box,
|
||||
Ui::LayerOptions options,
|
||||
anim::type animated);
|
||||
void showToast(const QString &text);
|
||||
|
||||
rpl::producer<> backRequests() const;
|
||||
void setBackAllowed(bool allowed);
|
||||
|
||||
not_null<Ui::RpWidget*> widget() const;
|
||||
|
||||
~Panel();
|
||||
|
||||
private:
|
||||
not_null<PanelController*> _controller;
|
||||
std::unique_ptr<Ui::SeparatePanel> _widget;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Passport
|
||||
1480
Telegram/SourceFiles/passport/passport_panel_controller.cpp
Normal file
1480
Telegram/SourceFiles/passport/passport_panel_controller.cpp
Normal file
File diff suppressed because it is too large
Load Diff
191
Telegram/SourceFiles/passport/passport_panel_controller.h
Normal file
191
Telegram/SourceFiles/passport/passport_panel_controller.h
Normal file
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "passport/passport_form_view_controller.h"
|
||||
#include "passport/passport_form_controller.h"
|
||||
#include "ui/layers/layer_widget.h"
|
||||
|
||||
namespace Ui {
|
||||
class BoxContent;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Passport {
|
||||
|
||||
class FormController;
|
||||
class Panel;
|
||||
|
||||
struct EditDocumentCountry;
|
||||
struct EditDocumentScheme;
|
||||
struct EditContactScheme;
|
||||
|
||||
enum class ReadScanError;
|
||||
|
||||
using preferredLangCallback
|
||||
= Fn<rpl::producer<EditDocumentCountry>(const QString &)>;
|
||||
EditDocumentScheme GetDocumentScheme(
|
||||
Scope::Type type,
|
||||
std::optional<Value::Type> scansType,
|
||||
bool nativeNames,
|
||||
preferredLangCallback &&preferredLanguage);
|
||||
EditContactScheme GetContactScheme(Scope::Type type);
|
||||
|
||||
const std::map<QString, QString> &LatinToNativeMap();
|
||||
const std::map<QString, QString> &NativeToLatinMap();
|
||||
QString AdjustKeyName(not_null<const Value*> value, const QString &key);
|
||||
bool SkipFieldCheck(not_null<const Value*> value, const QString &key);
|
||||
|
||||
struct ScanInfo {
|
||||
explicit ScanInfo(FileType type);
|
||||
ScanInfo(
|
||||
FileType type,
|
||||
const FileKey &key,
|
||||
const QString &status,
|
||||
const QImage &thumb,
|
||||
bool deleted,
|
||||
const QString &error);
|
||||
|
||||
FileType type;
|
||||
FileKey key;
|
||||
QString status;
|
||||
QImage thumb;
|
||||
bool deleted = false;
|
||||
QString error;
|
||||
};
|
||||
|
||||
struct ScopeError {
|
||||
enum class General {
|
||||
WholeValue,
|
||||
ScanMissing,
|
||||
TranslationMissing,
|
||||
};
|
||||
|
||||
// FileKey - file_hash error (bad scan / selfie / translation)
|
||||
// General - general value error (or scan / translation missing)
|
||||
// QString - data_hash with such key error (bad value)
|
||||
std::variant<FileKey, General, QString> key;
|
||||
QString text;
|
||||
};
|
||||
|
||||
class PanelController : public ViewController {
|
||||
public:
|
||||
PanelController(not_null<FormController*> form);
|
||||
|
||||
not_null<UserData*> bot() const;
|
||||
QString privacyPolicyUrl() const;
|
||||
void submitForm();
|
||||
void submitPassword(const QByteArray &password);
|
||||
void recoverPassword();
|
||||
rpl::producer<QString> passwordError() const;
|
||||
QString passwordHint() const;
|
||||
QString unconfirmedEmailPattern() const;
|
||||
|
||||
void setupPassword();
|
||||
void cancelPasswordSubmit();
|
||||
void validateRecoveryEmail();
|
||||
|
||||
bool canAddScan(FileType type) const;
|
||||
void uploadScan(FileType type, QByteArray &&content);
|
||||
void deleteScan(FileType type, std::optional<int> fileIndex);
|
||||
void restoreScan(FileType type, std::optional<int> fileIndex);
|
||||
rpl::producer<ScanInfo> scanUpdated() const;
|
||||
rpl::producer<ScopeError> saveErrors() const;
|
||||
void readScanError(ReadScanError error);
|
||||
|
||||
std::optional<rpl::producer<QString>> deleteValueLabel() const;
|
||||
void deleteValue();
|
||||
|
||||
QString defaultEmail() const;
|
||||
QString defaultPhoneNumber() const;
|
||||
|
||||
void showAskPassword() override;
|
||||
void showNoPassword() override;
|
||||
void showCriticalError(const QString &error) override;
|
||||
void showUpdateAppBox() override;
|
||||
|
||||
void fillRows(
|
||||
Fn<void(
|
||||
QString title,
|
||||
QString description,
|
||||
bool ready,
|
||||
bool error)> callback);
|
||||
rpl::producer<> refillRows() const;
|
||||
|
||||
void editScope(int index) override;
|
||||
void saveScope(ValueMap &&data, ValueMap &&filesData);
|
||||
bool editScopeChanged(
|
||||
const ValueMap &data,
|
||||
const ValueMap &filesData) const;
|
||||
void cancelEditScope();
|
||||
|
||||
void showBox(
|
||||
object_ptr<Ui::BoxContent> box,
|
||||
Ui::LayerOptions options,
|
||||
anim::type animated) override;
|
||||
void showToast(const QString &text) override;
|
||||
void suggestReset(Fn<void()> callback) override;
|
||||
|
||||
int closeGetDuration() override;
|
||||
|
||||
void cancelAuth();
|
||||
void cancelAuthSure();
|
||||
|
||||
rpl::lifetime &lifetime();
|
||||
|
||||
~PanelController();
|
||||
|
||||
private:
|
||||
void ensurePanelCreated();
|
||||
|
||||
void editScope(int index, std::optional<int> documentIndex);
|
||||
void editWithUpload(int index, int documentIndex);
|
||||
bool editRequiresScanUpload(
|
||||
int index,
|
||||
std::optional<int> documentIndex) const;
|
||||
void startScopeEdit(int index, std::optional<int> documentIndex);
|
||||
std::optional<int> findBestDocumentIndex(const Scope &scope) const;
|
||||
void requestScopeFilesType(int index);
|
||||
void cancelValueEdit();
|
||||
void processValueSaveFinished(not_null<const Value*> value);
|
||||
void processVerificationNeeded(not_null<const Value*> value);
|
||||
|
||||
bool savingScope() const;
|
||||
bool uploadingScopeScan() const;
|
||||
bool hasValueDocument() const;
|
||||
bool hasValueFields() const;
|
||||
std::vector<ScopeError> collectSaveErrors(
|
||||
not_null<const Value*> value) const;
|
||||
QString getDefaultContactValue(Scope::Type type) const;
|
||||
void deleteValueSure(bool withDetails);
|
||||
|
||||
void resetPassport(Fn<void()> callback);
|
||||
void cancelReset();
|
||||
|
||||
not_null<FormController*> _form;
|
||||
std::vector<Scope> _scopes;
|
||||
rpl::event_stream<> _submitFailed;
|
||||
std::vector<not_null<const Value*>> _submitErrors;
|
||||
rpl::event_stream<ScopeError> _saveErrors;
|
||||
|
||||
std::unique_ptr<Panel> _panel;
|
||||
Fn<bool()> _panelHasUnsavedChanges;
|
||||
base::weak_qptr<Ui::BoxContent> _confirmForgetChangesBox;
|
||||
std::vector<Ui::BoxPointer> _editScopeBoxes;
|
||||
Scope *_editScope = nullptr;
|
||||
const Value *_editValue = nullptr;
|
||||
const Value *_editDocument = nullptr;
|
||||
Ui::BoxPointer _scopeDocumentTypeBox;
|
||||
std::map<not_null<const Value*>, Ui::BoxPointer> _verificationBoxes;
|
||||
|
||||
Ui::BoxPointer _resetBox;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Passport
|
||||
453
Telegram/SourceFiles/passport/passport_panel_edit_contact.cpp
Normal file
453
Telegram/SourceFiles/passport/passport_panel_edit_contact.cpp
Normal file
@@ -0,0 +1,453 @@
|
||||
/*
|
||||
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_contact.h"
|
||||
|
||||
#include "core/file_utilities.h"
|
||||
#include "passport/passport_panel_controller.h"
|
||||
#include "passport/ui/passport_details_row.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/shadow.h"
|
||||
#include "ui/widgets/box_content_divider.h"
|
||||
#include "ui/widgets/sent_code_field.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
#include "ui/wrap/fade_wrap.h"
|
||||
#include "ui/text/format_values.h" // Ui::FormatPhone
|
||||
#include "ui/widgets/fields/special_fields.h"
|
||||
#include "boxes/abstract_box.h"
|
||||
#include "data/data_user.h"
|
||||
#include "countries/countries_instance.h" // Countries::ExtractPhoneCode.
|
||||
#include "main/main_session.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_passport.h"
|
||||
#include "styles/style_layers.h"
|
||||
|
||||
namespace Passport {
|
||||
namespace {
|
||||
|
||||
class VerifyBox : public Ui::BoxContent {
|
||||
public:
|
||||
VerifyBox(
|
||||
QWidget*,
|
||||
rpl::producer<QString> title,
|
||||
const QString &text,
|
||||
int codeLength,
|
||||
const QString &openUrl,
|
||||
Fn<void(QString code)> submit,
|
||||
Fn<void()> resend,
|
||||
rpl::producer<QString> call,
|
||||
rpl::producer<QString> error,
|
||||
rpl::producer<QString> resent);
|
||||
|
||||
void setInnerFocus() override;
|
||||
|
||||
protected:
|
||||
void prepare() override;
|
||||
|
||||
private:
|
||||
void setupControls(
|
||||
const QString &text,
|
||||
int codeLength,
|
||||
const QString &openUrl,
|
||||
Fn<void(QString code)> submit,
|
||||
Fn<void()> resend,
|
||||
rpl::producer<QString> call,
|
||||
rpl::producer<QString> error,
|
||||
rpl::producer<QString> resent);
|
||||
|
||||
rpl::producer<QString> _title;
|
||||
Fn<void()> _submit;
|
||||
QPointer<Ui::SentCodeField> _code;
|
||||
QPointer<Ui::VerticalLayout> _content;
|
||||
|
||||
};
|
||||
|
||||
VerifyBox::VerifyBox(
|
||||
QWidget*,
|
||||
rpl::producer<QString> title,
|
||||
const QString &text,
|
||||
int codeLength,
|
||||
const QString &openUrl,
|
||||
Fn<void(QString code)> submit,
|
||||
Fn<void()> resend,
|
||||
rpl::producer<QString> call,
|
||||
rpl::producer<QString> error,
|
||||
rpl::producer<QString> resent)
|
||||
: _title(std::move(title)) {
|
||||
setupControls(
|
||||
text,
|
||||
codeLength,
|
||||
openUrl,
|
||||
submit,
|
||||
resend,
|
||||
std::move(call),
|
||||
std::move(error),
|
||||
std::move(resent));
|
||||
}
|
||||
|
||||
void VerifyBox::setupControls(
|
||||
const QString &text,
|
||||
int codeLength,
|
||||
const QString &openUrl,
|
||||
Fn<void(QString code)> submit,
|
||||
Fn<void()> resend,
|
||||
rpl::producer<QString> call,
|
||||
rpl::producer<QString> error,
|
||||
rpl::producer<QString> resent) {
|
||||
_content = Ui::CreateChild<Ui::VerticalLayout>(this);
|
||||
|
||||
const auto small = style::margins(
|
||||
st::boxPadding.left(),
|
||||
0,
|
||||
st::boxPadding.right(),
|
||||
st::boxPadding.bottom());
|
||||
_content->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
_content,
|
||||
text,
|
||||
st::boxLabel),
|
||||
small);
|
||||
_code = _content->add(
|
||||
object_ptr<Ui::SentCodeField>(
|
||||
_content,
|
||||
st::defaultInputField,
|
||||
tr::lng_change_phone_code_title()),
|
||||
small);
|
||||
|
||||
const auto problem = _content->add(
|
||||
object_ptr<Ui::FadeWrap<Ui::FlatLabel>>(
|
||||
_content,
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
_content,
|
||||
QString(),
|
||||
st::passportVerifyErrorLabel)),
|
||||
small);
|
||||
_content->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
_content,
|
||||
std::move(call),
|
||||
st::boxDividerLabel),
|
||||
small);
|
||||
if (!openUrl.isEmpty()) {
|
||||
const auto button = _content->add(
|
||||
object_ptr<Ui::RoundButton>(
|
||||
_content,
|
||||
tr::lng_intro_fragment_button(),
|
||||
st::fragmentBoxButton),
|
||||
small);
|
||||
_content->widthValue(
|
||||
) | rpl::on_next([=](int w) {
|
||||
button->setFullWidth(w - small.left() - small.right());
|
||||
}, button->lifetime());
|
||||
button->setClickedCallback([=] { ::File::OpenUrl(openUrl); });
|
||||
button->setTextTransform(
|
||||
Ui::RoundButton::TextTransform::NoTransform);
|
||||
}
|
||||
if (resend) {
|
||||
auto link = TextWithEntities{ tr::lng_cloud_password_resend(tr::now) };
|
||||
link.entities.push_back({
|
||||
EntityType::CustomUrl,
|
||||
0,
|
||||
int(link.text.size()),
|
||||
QString("internal:resend") });
|
||||
const auto label = _content->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
_content,
|
||||
rpl::single(
|
||||
link
|
||||
) | rpl::then(rpl::duplicate(
|
||||
resent
|
||||
) | rpl::map(TextWithEntities::Simple)),
|
||||
st::boxDividerLabel),
|
||||
small);
|
||||
std::move(
|
||||
resent
|
||||
) | rpl::on_next([=] {
|
||||
_content->resizeToWidth(st::boxWidth);
|
||||
}, _content->lifetime());
|
||||
label->overrideLinkClickHandler(resend);
|
||||
}
|
||||
std::move(
|
||||
error
|
||||
) | rpl::on_next([=](const QString &error) {
|
||||
if (error.isEmpty()) {
|
||||
problem->hide(anim::type::normal);
|
||||
} else {
|
||||
problem->entity()->setText(error);
|
||||
_content->resizeToWidth(st::boxWidth);
|
||||
problem->show(anim::type::normal);
|
||||
_code->showError();
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
_submit = [=] {
|
||||
submit(_code->getDigitsOnly());
|
||||
};
|
||||
if (codeLength > 0) {
|
||||
_code->setAutoSubmit(codeLength, _submit);
|
||||
} else {
|
||||
_code->submits() | rpl::on_next(_submit, _code->lifetime());
|
||||
}
|
||||
_code->changes(
|
||||
) | rpl::on_next([=] {
|
||||
problem->hide(anim::type::normal);
|
||||
}, _code->lifetime());
|
||||
}
|
||||
|
||||
void VerifyBox::setInnerFocus() {
|
||||
_code->setFocusFast();
|
||||
}
|
||||
|
||||
void VerifyBox::prepare() {
|
||||
setTitle(std::move(_title));
|
||||
|
||||
addButton(tr::lng_change_phone_new_submit(), _submit);
|
||||
addButton(tr::lng_cancel(), [=] { closeBox(); });
|
||||
|
||||
_content->resizeToWidth(st::boxWidth);
|
||||
_content->heightValue(
|
||||
) | rpl::on_next([=](int height) {
|
||||
setDimensions(st::boxWidth, height);
|
||||
}, _content->lifetime());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
EditContactScheme::EditContactScheme(ValueType type) : type(type) {
|
||||
}
|
||||
|
||||
PanelEditContact::PanelEditContact(
|
||||
QWidget*,
|
||||
not_null<PanelController*> controller,
|
||||
Scheme scheme,
|
||||
const QString &data,
|
||||
const QString &existing)
|
||||
: _controller(controller)
|
||||
, _scheme(std::move(scheme))
|
||||
, _content(this)
|
||||
, _bottomShadow(this)
|
||||
, _done(
|
||||
this,
|
||||
tr::lng_passport_save_value(),
|
||||
st::passportPanelSaveValue) {
|
||||
setupControls(data, existing);
|
||||
}
|
||||
|
||||
void PanelEditContact::setupControls(
|
||||
const QString &data,
|
||||
const QString &existing) {
|
||||
widthValue(
|
||||
) | rpl::on_next([=](int width) {
|
||||
_content->resizeToWidth(width);
|
||||
}, _content->lifetime());
|
||||
|
||||
_content->add(object_ptr<Ui::BoxContentDivider>(
|
||||
_content,
|
||||
st::passportFormDividerHeight));
|
||||
if (!existing.isEmpty()) {
|
||||
_content->add(
|
||||
object_ptr<Ui::SettingsButton>(
|
||||
_content,
|
||||
tr::lng_passport_use_existing(
|
||||
lt_existing,
|
||||
rpl::single(_scheme.format
|
||||
? _scheme.format(existing)
|
||||
: existing)),
|
||||
st::passportUploadButton),
|
||||
st::passportUploadButtonPadding
|
||||
)->addClickHandler([=] {
|
||||
save(existing);
|
||||
});
|
||||
_content->add(
|
||||
object_ptr<Ui::DividerLabel>(
|
||||
_content,
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
_content,
|
||||
_scheme.aboutExisting,
|
||||
st::boxDividerLabel),
|
||||
st::passportFormLabelPadding));
|
||||
_content->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
_content,
|
||||
_scheme.newHeader,
|
||||
st::passportFormHeader),
|
||||
st::passportDetailsHeaderPadding);
|
||||
}
|
||||
const auto &fieldStyle = existing.isEmpty()
|
||||
? st::passportContactField
|
||||
: st::passportDetailsField;
|
||||
const auto fieldPadding = existing.isEmpty()
|
||||
? st::passportContactFieldPadding
|
||||
: st::passportContactNewFieldPadding;
|
||||
auto fieldPlaceholder = existing.isEmpty()
|
||||
? rpl::duplicate(_scheme.newPlaceholder)
|
||||
: nullptr;
|
||||
auto wrap = object_ptr<Ui::RpWidget>(_content);
|
||||
if (_scheme.type == Scheme::ValueType::Phone) {
|
||||
_field = Ui::CreateChild<Ui::PhoneInput>(
|
||||
wrap.data(),
|
||||
fieldStyle,
|
||||
std::move(fieldPlaceholder),
|
||||
Countries::ExtractPhoneCode(
|
||||
_controller->bot()->session().user()->phone()),
|
||||
data,
|
||||
[](const QString &s) { return Countries::Groups(s); });
|
||||
} else {
|
||||
_field = Ui::CreateChild<Ui::MaskedInputField>(
|
||||
wrap.data(),
|
||||
fieldStyle,
|
||||
std::move(fieldPlaceholder),
|
||||
data);
|
||||
}
|
||||
|
||||
_field->move(0, 0);
|
||||
_field->heightValue(
|
||||
) | rpl::on_next([=, pointer = wrap.data()](int height) {
|
||||
pointer->resize(pointer->width(), height);
|
||||
}, _field->lifetime());
|
||||
wrap->widthValue(
|
||||
) | rpl::on_next([=](int width) {
|
||||
_field->resize(width, _field->height());
|
||||
}, _field->lifetime());
|
||||
|
||||
_content->add(std::move(wrap), fieldPadding);
|
||||
const auto errorWrap = _content->add(
|
||||
object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
|
||||
_content,
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
_content,
|
||||
QString(),
|
||||
st::passportVerifyErrorLabel),
|
||||
st::passportContactErrorPadding),
|
||||
st::passportContactErrorMargin);
|
||||
errorWrap->hide(anim::type::instant);
|
||||
|
||||
_content->add(
|
||||
object_ptr<Ui::DividerLabel>(
|
||||
_content,
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
_content,
|
||||
_scheme.aboutNew,
|
||||
st::boxDividerLabel),
|
||||
st::passportFormLabelPadding));
|
||||
|
||||
if (auto text = _controller->deleteValueLabel()) {
|
||||
_content->add(
|
||||
object_ptr<Ui::SettingsButton>(
|
||||
_content,
|
||||
std::move(*text) | rpl::map(tr::upper),
|
||||
st::passportDeleteButton),
|
||||
st::passportUploadButtonPadding
|
||||
)->addClickHandler([=] {
|
||||
_controller->deleteValue();
|
||||
});
|
||||
}
|
||||
|
||||
_controller->saveErrors(
|
||||
) | rpl::on_next([=](const ScopeError &error) {
|
||||
if (error.key == QString("value")) {
|
||||
_field->showError();
|
||||
errorWrap->entity()->setText(error.text);
|
||||
_content->resizeToWidth(width());
|
||||
errorWrap->show(anim::type::normal);
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
const auto submit = [=] {
|
||||
crl::on_main(this, [=] {
|
||||
save();
|
||||
});
|
||||
};
|
||||
connect(_field, &Ui::MaskedInputField::submitted, submit);
|
||||
connect(_field, &Ui::MaskedInputField::changed, [=] {
|
||||
errorWrap->hide(anim::type::normal);
|
||||
});
|
||||
_done->addClickHandler(submit);
|
||||
}
|
||||
|
||||
void PanelEditContact::focusInEvent(QFocusEvent *e) {
|
||||
crl::on_main(this, [=] {
|
||||
_field->setFocusFast();
|
||||
});
|
||||
}
|
||||
|
||||
void PanelEditContact::resizeEvent(QResizeEvent *e) {
|
||||
updateControlsGeometry();
|
||||
}
|
||||
|
||||
void PanelEditContact::updateControlsGeometry() {
|
||||
const auto submitTop = height() - _done->height();
|
||||
_bottomShadow->resizeToWidth(width());
|
||||
_bottomShadow->moveToLeft(0, submitTop - st::lineWidth);
|
||||
_done->resizeToWidth(width());
|
||||
_done->moveToLeft(0, submitTop);
|
||||
}
|
||||
|
||||
void PanelEditContact::save() {
|
||||
const auto result = _field->getLastText();
|
||||
const auto processed = _scheme.postprocess
|
||||
? _scheme.postprocess(result)
|
||||
: result;
|
||||
if (_scheme.validate && !_scheme.validate(processed)) {
|
||||
_field->showError();
|
||||
return;
|
||||
}
|
||||
save(processed);
|
||||
}
|
||||
|
||||
void PanelEditContact::save(const QString &value) {
|
||||
auto data = ValueMap();
|
||||
data.fields["value"].text = value;
|
||||
_controller->saveScope(std::move(data), {});
|
||||
}
|
||||
|
||||
object_ptr<Ui::BoxContent> VerifyPhoneBox(
|
||||
const QString &phone,
|
||||
int codeLength,
|
||||
const QString &openUrl,
|
||||
Fn<void(QString code)> submit,
|
||||
rpl::producer<QString> call,
|
||||
rpl::producer<QString> error) {
|
||||
return Box<VerifyBox>(
|
||||
tr::lng_passport_phone_title(),
|
||||
tr::lng_passport_confirm_phone(
|
||||
tr::now,
|
||||
lt_phone,
|
||||
Ui::FormatPhone(phone)),
|
||||
codeLength,
|
||||
openUrl,
|
||||
submit,
|
||||
nullptr,
|
||||
std::move(call),
|
||||
std::move(error),
|
||||
nullptr);
|
||||
}
|
||||
|
||||
object_ptr<Ui::BoxContent> VerifyEmailBox(
|
||||
const QString &email,
|
||||
int codeLength,
|
||||
Fn<void(QString code)> submit,
|
||||
Fn<void()> resend,
|
||||
rpl::producer<QString> error,
|
||||
rpl::producer<QString> resent) {
|
||||
return Box<VerifyBox>(
|
||||
tr::lng_passport_email_title(),
|
||||
tr::lng_passport_confirm_email(tr::now, lt_email, email),
|
||||
codeLength,
|
||||
QString(),
|
||||
submit,
|
||||
resend,
|
||||
rpl::single(QString()),
|
||||
std::move(error),
|
||||
std::move(resent));
|
||||
}
|
||||
|
||||
} // namespace Passport
|
||||
93
Telegram/SourceFiles/passport/passport_panel_edit_contact.h
Normal file
93
Telegram/SourceFiles/passport/passport_panel_edit_contact.h
Normal file
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
#include "base/object_ptr.h"
|
||||
|
||||
namespace Ui {
|
||||
class MaskedInputField;
|
||||
class PlainShadow;
|
||||
class RoundButton;
|
||||
class VerticalLayout;
|
||||
class BoxContent;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Passport {
|
||||
|
||||
class PanelController;
|
||||
|
||||
struct EditContactScheme {
|
||||
enum class ValueType {
|
||||
Phone,
|
||||
Text,
|
||||
};
|
||||
explicit EditContactScheme(ValueType type);
|
||||
|
||||
ValueType type;
|
||||
|
||||
QString aboutExisting;
|
||||
QString newHeader;
|
||||
rpl::producer<QString> newPlaceholder;
|
||||
QString aboutNew;
|
||||
Fn<bool(const QString &value)> validate;
|
||||
Fn<QString(const QString &value)> format;
|
||||
Fn<QString(const QString &value)> postprocess;
|
||||
|
||||
};
|
||||
|
||||
class PanelEditContact : public Ui::RpWidget {
|
||||
public:
|
||||
using Scheme = EditContactScheme;
|
||||
|
||||
PanelEditContact(
|
||||
QWidget *parent,
|
||||
not_null<PanelController*> controller,
|
||||
Scheme scheme,
|
||||
const QString &data,
|
||||
const QString &existing);
|
||||
|
||||
protected:
|
||||
void focusInEvent(QFocusEvent *e) override;
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
|
||||
private:
|
||||
void setupControls(
|
||||
const QString &data,
|
||||
const QString &existing);
|
||||
void updateControlsGeometry();
|
||||
|
||||
void save();
|
||||
void save(const QString &value);
|
||||
|
||||
not_null<PanelController*> _controller;
|
||||
Scheme _scheme;
|
||||
|
||||
object_ptr<Ui::VerticalLayout> _content;
|
||||
QPointer<Ui::MaskedInputField> _field;
|
||||
object_ptr<Ui::PlainShadow> _bottomShadow;
|
||||
object_ptr<Ui::RoundButton> _done;
|
||||
|
||||
};
|
||||
|
||||
object_ptr<Ui::BoxContent> VerifyPhoneBox(
|
||||
const QString &phone,
|
||||
int codeLength,
|
||||
const QString &openUrl,
|
||||
Fn<void(QString code)> submit,
|
||||
rpl::producer<QString> call,
|
||||
rpl::producer<QString> error);
|
||||
object_ptr<Ui::BoxContent> VerifyEmailBox(
|
||||
const QString &email,
|
||||
int codeLength,
|
||||
Fn<void(QString code)> submit,
|
||||
Fn<void()> resend,
|
||||
rpl::producer<QString> error,
|
||||
rpl::producer<QString> resent);
|
||||
|
||||
} // namespace Passport
|
||||
719
Telegram/SourceFiles/passport/passport_panel_edit_document.cpp
Normal file
719
Telegram/SourceFiles/passport/passport_panel_edit_document.cpp
Normal file
@@ -0,0 +1,719 @@
|
||||
/*
|
||||
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_document.h"
|
||||
|
||||
#include "passport/passport_panel_controller.h"
|
||||
#include "passport/passport_panel_edit_scans.h"
|
||||
#include "passport/ui/passport_details_row.h"
|
||||
#include "ui/effects/scroll_content_shadow.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "ui/wrap/fade_wrap.h"
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
#include "countries/countries_instance.h"
|
||||
#include "data/data_user.h" // ->bot()->session()
|
||||
#include "main/main_session.h" // ->session().user()
|
||||
#include "boxes/abstract_box.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "styles/style_widgets.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_passport.h"
|
||||
|
||||
namespace Passport {
|
||||
namespace {
|
||||
|
||||
class RequestTypeBox : public Ui::BoxContent {
|
||||
public:
|
||||
RequestTypeBox(
|
||||
QWidget*,
|
||||
rpl::producer<QString> title,
|
||||
const QString &about,
|
||||
std::vector<QString> labels,
|
||||
Fn<void(int index)> submit);
|
||||
|
||||
protected:
|
||||
void prepare() override;
|
||||
|
||||
private:
|
||||
void setupControls(
|
||||
const QString &about,
|
||||
std::vector<QString> labels,
|
||||
Fn<void(int index)> submit);
|
||||
|
||||
rpl::producer<QString> _title;
|
||||
Fn<void()> _submit;
|
||||
int _height = 0;
|
||||
|
||||
};
|
||||
|
||||
class DeleteDocumentBox : public Ui::BoxContent {
|
||||
public:
|
||||
DeleteDocumentBox(
|
||||
QWidget*,
|
||||
const QString &text,
|
||||
const QString &detailsCheckbox,
|
||||
Fn<void(bool withDetails)> submit);
|
||||
|
||||
protected:
|
||||
void prepare() override;
|
||||
|
||||
private:
|
||||
void setupControls(
|
||||
const QString &text,
|
||||
const QString &detailsCheckbox,
|
||||
Fn<void(bool withDetails)> submit);
|
||||
|
||||
Fn<void()> _submit;
|
||||
int _height = 0;
|
||||
|
||||
};
|
||||
|
||||
RequestTypeBox::RequestTypeBox(
|
||||
QWidget*,
|
||||
rpl::producer<QString> title,
|
||||
const QString &about,
|
||||
std::vector<QString> labels,
|
||||
Fn<void(int index)> submit)
|
||||
: _title(std::move(title)) {
|
||||
setupControls(about, std::move(labels), submit);
|
||||
}
|
||||
|
||||
void RequestTypeBox::prepare() {
|
||||
setTitle(std::move(_title));
|
||||
addButton(tr::lng_passport_upload_document(), _submit);
|
||||
addButton(tr::lng_cancel(), [=] { closeBox(); });
|
||||
setDimensions(st::boxWidth, _height);
|
||||
}
|
||||
|
||||
void RequestTypeBox::setupControls(
|
||||
const QString &about,
|
||||
std::vector<QString> labels,
|
||||
Fn<void(int index)> submit) {
|
||||
const auto header = Ui::CreateChild<Ui::FlatLabel>(
|
||||
this,
|
||||
tr::lng_passport_document_type(tr::now),
|
||||
st::boxDividerLabel);
|
||||
|
||||
const auto group = std::make_shared<Ui::RadiobuttonGroup>(0);
|
||||
auto buttons = std::vector<QPointer<Ui::Radiobutton>>();
|
||||
auto index = 0;
|
||||
for (const auto &label : labels) {
|
||||
buttons.emplace_back(Ui::CreateChild<Ui::Radiobutton>(
|
||||
this,
|
||||
group,
|
||||
index++,
|
||||
label,
|
||||
st::defaultBoxCheckbox));
|
||||
}
|
||||
|
||||
const auto description = Ui::CreateChild<Ui::FlatLabel>(
|
||||
this,
|
||||
about,
|
||||
st::boxDividerLabel);
|
||||
|
||||
auto y = 0;
|
||||
const auto innerWidth = st::boxWidth
|
||||
- st::boxPadding.left()
|
||||
- st::boxPadding.right();
|
||||
header->resizeToWidth(innerWidth);
|
||||
header->moveToLeft(st::boxPadding.left(), y);
|
||||
y += header->height() + st::passportRequestTypeSkip;
|
||||
for (const auto &button : buttons) {
|
||||
button->resizeToNaturalWidth(innerWidth);
|
||||
button->moveToLeft(st::boxPadding.left(), y);
|
||||
y += button->heightNoMargins() + st::passportRequestTypeSkip;
|
||||
}
|
||||
description->resizeToWidth(innerWidth);
|
||||
description->moveToLeft(st::boxPadding.left(), y);
|
||||
y += description->height() + st::passportRequestTypeSkip;
|
||||
_height = y;
|
||||
|
||||
_submit = [=] {
|
||||
const auto value = group->hasValue() ? group->current() : -1;
|
||||
if (value >= 0) {
|
||||
submit(value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
DeleteDocumentBox::DeleteDocumentBox(
|
||||
QWidget*,
|
||||
const QString &text,
|
||||
const QString &detailsCheckbox,
|
||||
Fn<void(bool withDetails)> submit) {
|
||||
setupControls(text, detailsCheckbox, submit);
|
||||
}
|
||||
|
||||
void DeleteDocumentBox::prepare() {
|
||||
addButton(tr::lng_box_delete(), _submit);
|
||||
addButton(tr::lng_cancel(), [=] { closeBox(); });
|
||||
|
||||
setDimensions(st::boxWidth, _height);
|
||||
}
|
||||
|
||||
void DeleteDocumentBox::setupControls(
|
||||
const QString &text,
|
||||
const QString &detailsCheckbox,
|
||||
Fn<void(bool withDetails)> submit) {
|
||||
const auto label = Ui::CreateChild<Ui::FlatLabel>(
|
||||
this,
|
||||
text,
|
||||
st::boxLabel);
|
||||
const auto details = !detailsCheckbox.isEmpty()
|
||||
? Ui::CreateChild<Ui::Checkbox>(
|
||||
this,
|
||||
detailsCheckbox,
|
||||
false,
|
||||
st::defaultBoxCheckbox)
|
||||
: nullptr;
|
||||
|
||||
_height = st::boxPadding.top();
|
||||
const auto availableWidth = st::boxWidth
|
||||
- st::boxPadding.left()
|
||||
- st::boxPadding.right();
|
||||
label->resizeToWidth(availableWidth);
|
||||
label->moveToLeft(st::boxPadding.left(), _height);
|
||||
_height += label->height();
|
||||
|
||||
if (details) {
|
||||
_height += st::boxPadding.bottom();
|
||||
details->moveToLeft(st::boxPadding.left(), _height);
|
||||
_height += details->heightNoMargins();
|
||||
}
|
||||
_height += st::boxPadding.bottom();
|
||||
|
||||
_submit = [=] {
|
||||
submit(details ? details->checked() : false);
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct PanelEditDocument::Result {
|
||||
ValueMap data;
|
||||
ValueMap filesData;
|
||||
};
|
||||
|
||||
PanelEditDocument::PanelEditDocument(
|
||||
QWidget*,
|
||||
not_null<PanelController*> controller,
|
||||
Scheme scheme,
|
||||
const QString &error,
|
||||
const ValueMap &data,
|
||||
const QString &scansError,
|
||||
const ValueMap &scansData,
|
||||
ScanListData &&scans,
|
||||
std::optional<ScanListData> &&translations,
|
||||
std::map<FileType, ScanInfo> &&specialFiles)
|
||||
: _controller(controller)
|
||||
, _scheme(std::move(scheme))
|
||||
, _scroll(this, st::passportPanelScroll)
|
||||
, _done(
|
||||
this,
|
||||
tr::lng_passport_save_value(),
|
||||
st::passportPanelSaveValue) {
|
||||
setupControls(
|
||||
&error,
|
||||
&data,
|
||||
&scansError,
|
||||
&scansData,
|
||||
std::move(scans),
|
||||
std::move(translations),
|
||||
std::move(specialFiles));
|
||||
}
|
||||
|
||||
PanelEditDocument::PanelEditDocument(
|
||||
QWidget*,
|
||||
not_null<PanelController*> controller,
|
||||
Scheme scheme,
|
||||
const QString &scansError,
|
||||
const ValueMap &scansData,
|
||||
ScanListData &&scans,
|
||||
std::optional<ScanListData> &&translations,
|
||||
std::map<FileType, ScanInfo> &&specialFiles)
|
||||
: _controller(controller)
|
||||
, _scheme(std::move(scheme))
|
||||
, _scroll(this, st::passportPanelScroll)
|
||||
, _done(
|
||||
this,
|
||||
tr::lng_passport_save_value(),
|
||||
st::passportPanelSaveValue) {
|
||||
setupControls(
|
||||
nullptr,
|
||||
nullptr,
|
||||
&scansError,
|
||||
&scansData,
|
||||
std::move(scans),
|
||||
std::move(translations),
|
||||
std::move(specialFiles));
|
||||
}
|
||||
|
||||
PanelEditDocument::PanelEditDocument(
|
||||
QWidget*,
|
||||
not_null<PanelController*> controller,
|
||||
Scheme scheme,
|
||||
const QString &error,
|
||||
const ValueMap &data)
|
||||
: _controller(controller)
|
||||
, _scheme(std::move(scheme))
|
||||
, _scroll(this, st::passportPanelScroll)
|
||||
, _done(
|
||||
this,
|
||||
tr::lng_passport_save_value(),
|
||||
st::passportPanelSaveValue) {
|
||||
setupControls(&error, &data, nullptr, nullptr, {}, {}, {});
|
||||
}
|
||||
|
||||
void PanelEditDocument::setupControls(
|
||||
const QString *error,
|
||||
const ValueMap *data,
|
||||
const QString *scansError,
|
||||
const ValueMap *scansData,
|
||||
ScanListData &&scans,
|
||||
std::optional<ScanListData> &&translations,
|
||||
std::map<FileType, ScanInfo> &&specialFiles) {
|
||||
const auto inner = setupContent(
|
||||
error,
|
||||
data,
|
||||
scansError,
|
||||
scansData,
|
||||
std::move(scans),
|
||||
std::move(translations),
|
||||
std::move(specialFiles));
|
||||
|
||||
Ui::SetupShadowsToScrollContent(this, _scroll, inner->heightValue());
|
||||
|
||||
_done->addClickHandler([=] {
|
||||
crl::on_main(this, [=] {
|
||||
save();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
not_null<Ui::RpWidget*> PanelEditDocument::setupContent(
|
||||
const QString *error,
|
||||
const ValueMap *data,
|
||||
const QString *scansError,
|
||||
const ValueMap *scansData,
|
||||
ScanListData &&scans,
|
||||
std::optional<ScanListData> &&translations,
|
||||
std::map<FileType, ScanInfo> &&specialFiles) {
|
||||
const auto inner = _scroll->setOwnedWidget(
|
||||
object_ptr<Ui::VerticalLayout>(this));
|
||||
_scroll->widthValue(
|
||||
) | rpl::on_next([=](int width) {
|
||||
inner->resizeToWidth(width);
|
||||
}, inner->lifetime());
|
||||
|
||||
if (!specialFiles.empty()) {
|
||||
_editScans = inner->add(
|
||||
object_ptr<EditScans>(
|
||||
inner,
|
||||
_controller,
|
||||
_scheme.scansHeader,
|
||||
*scansError,
|
||||
std::move(specialFiles),
|
||||
std::move(translations)));
|
||||
} else if (scansData) {
|
||||
_editScans = inner->add(
|
||||
object_ptr<EditScans>(
|
||||
inner,
|
||||
_controller,
|
||||
_scheme.scansHeader,
|
||||
*scansError,
|
||||
std::move(scans),
|
||||
std::move(translations)));
|
||||
}
|
||||
|
||||
const auto enumerateRows = [&](auto &&callback) {
|
||||
for (auto i = 0, count = int(_scheme.rows.size()); i != count; ++i) {
|
||||
const auto &row = _scheme.rows[i];
|
||||
|
||||
Assert(row.valueClass != Scheme::ValueClass::Additional
|
||||
|| !_scheme.additionalDependencyKey.isEmpty());
|
||||
auto fields = (row.valueClass == Scheme::ValueClass::Scans)
|
||||
? scansData
|
||||
: data;
|
||||
if (!fields) {
|
||||
continue;
|
||||
}
|
||||
callback(i, row, *fields);
|
||||
}
|
||||
};
|
||||
auto maxLabelWidth = 0;
|
||||
enumerateRows([&](
|
||||
int i,
|
||||
const EditDocumentScheme::Row &row,
|
||||
const ValueMap &fields) {
|
||||
accumulate_max(
|
||||
maxLabelWidth,
|
||||
Ui::PanelDetailsRow::LabelWidth(row.label));
|
||||
});
|
||||
if (maxLabelWidth > 0) {
|
||||
if (error && !error->isEmpty()) {
|
||||
_commonError = inner->add(
|
||||
object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
|
||||
inner,
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
inner,
|
||||
*error,
|
||||
st::passportVerifyErrorLabel),
|
||||
st::passportValueErrorPadding));
|
||||
_commonError->toggle(true, anim::type::instant);
|
||||
}
|
||||
inner->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
inner,
|
||||
data ? _scheme.detailsHeader : _scheme.fieldsHeader,
|
||||
st::passportFormHeader),
|
||||
st::passportDetailsHeaderPadding);
|
||||
enumerateRows([&](
|
||||
int i,
|
||||
const Scheme::Row &row,
|
||||
const ValueMap &fields) {
|
||||
if (row.valueClass != Scheme::ValueClass::Additional) {
|
||||
createDetailsRow(inner, i, row, fields, maxLabelWidth);
|
||||
}
|
||||
});
|
||||
if (data && !_scheme.additionalDependencyKey.isEmpty()) {
|
||||
const auto row = findRow(_scheme.additionalDependencyKey);
|
||||
const auto wrap = inner->add(
|
||||
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
|
||||
inner,
|
||||
object_ptr<Ui::VerticalLayout>(inner)));
|
||||
const auto added = wrap->entity();
|
||||
|
||||
auto showIfError = false;
|
||||
enumerateRows([&](
|
||||
int i,
|
||||
const Scheme::Row &row,
|
||||
const ValueMap &fields) {
|
||||
if (row.valueClass != Scheme::ValueClass::Additional) {
|
||||
return;
|
||||
}
|
||||
const auto it = fields.fields.find(row.key);
|
||||
if (it == end(fields.fields)) {
|
||||
return;
|
||||
} else if (!it->second.error.isEmpty()) {
|
||||
showIfError = true;
|
||||
} else if (it->second.text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const auto fallbackIt = fields.fields.find(
|
||||
row.additionalFallbackKey);
|
||||
if (fallbackIt != end(fields.fields)
|
||||
&& fallbackIt->second.text != it->second.text) {
|
||||
showIfError = true;
|
||||
}
|
||||
});
|
||||
const auto shown = [=](const Scheme::CountryInfo &info) {
|
||||
using Result = Scheme::AdditionalVisibility;
|
||||
const auto value = _scheme.additionalShown(info);
|
||||
return (value == Result::Shown)
|
||||
|| (value == Result::OnlyIfError && showIfError);
|
||||
};
|
||||
|
||||
auto langValue = row->value(
|
||||
) | rpl::map(
|
||||
_scheme.preferredLanguage
|
||||
) | rpl::flatten_latest();
|
||||
|
||||
auto title = rpl::duplicate(langValue) | rpl::filter(
|
||||
shown
|
||||
) | rpl::map([=](const Scheme::CountryInfo &info) {
|
||||
return _scheme.additionalHeader(info);
|
||||
});
|
||||
const auto headerLabel = added->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
added,
|
||||
rpl::duplicate(title),
|
||||
st::passportFormHeader),
|
||||
st::passportNativeNameHeaderPadding);
|
||||
std::move(
|
||||
title
|
||||
) | rpl::on_next([=] {
|
||||
const auto &padding = st::passportNativeNameHeaderPadding;
|
||||
const auto available = added->width()
|
||||
- padding.left()
|
||||
- padding.right();
|
||||
headerLabel->resizeToNaturalWidth(available);
|
||||
headerLabel->moveToLeft(
|
||||
padding.left(),
|
||||
padding.top(),
|
||||
available);
|
||||
}, headerLabel->lifetime());
|
||||
|
||||
enumerateRows([&](
|
||||
int i,
|
||||
const Scheme::Row &row,
|
||||
const ValueMap &fields) {
|
||||
if (row.valueClass == Scheme::ValueClass::Additional) {
|
||||
createDetailsRow(added, i, row, fields, maxLabelWidth);
|
||||
}
|
||||
});
|
||||
|
||||
auto description = rpl::duplicate(langValue) | rpl::filter(
|
||||
shown
|
||||
) | rpl::map([=](const Scheme::CountryInfo &info) {
|
||||
return _scheme.additionalDescription(info);
|
||||
});
|
||||
added->add(
|
||||
object_ptr<Ui::DividerLabel>(
|
||||
added,
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
added,
|
||||
std::move(description),
|
||||
st::boxDividerLabel),
|
||||
st::passportFormLabelPadding),
|
||||
st::passportNativeNameAboutMargin);
|
||||
|
||||
wrap->toggleOn(rpl::duplicate(langValue) | rpl::map(shown));
|
||||
wrap->finishAnimating();
|
||||
|
||||
std::move(langValue) | rpl::map(
|
||||
shown
|
||||
) | rpl::on_next([=](bool visible) {
|
||||
_additionalShown = visible;
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
inner->add(object_ptr<Ui::FixedHeightWidget>(
|
||||
inner,
|
||||
st::passportDetailsSkip));
|
||||
}
|
||||
if (auto text = _controller->deleteValueLabel()) {
|
||||
inner->add(
|
||||
object_ptr<Ui::SettingsButton>(
|
||||
inner,
|
||||
std::move(*text) | rpl::map(tr::upper),
|
||||
st::passportDeleteButton),
|
||||
st::passportUploadButtonPadding
|
||||
)->addClickHandler([=] {
|
||||
_controller->deleteValue();
|
||||
});
|
||||
}
|
||||
|
||||
return inner;
|
||||
}
|
||||
|
||||
void PanelEditDocument::createDetailsRow(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
int i,
|
||||
const Scheme::Row &row,
|
||||
const ValueMap &fields,
|
||||
int maxLabelWidth) {
|
||||
const auto valueOrEmpty = [&](
|
||||
const ValueMap &values,
|
||||
const QString &key) {
|
||||
const auto &fields = values.fields;
|
||||
if (const auto i = fields.find(key); i != fields.end()) {
|
||||
return i->second;
|
||||
}
|
||||
return ValueField();
|
||||
};
|
||||
|
||||
const auto current = valueOrEmpty(fields, row.key);
|
||||
const auto showBox = [controller = _controller](
|
||||
object_ptr<Ui::BoxContent> box) {
|
||||
controller->show(std::move(box));
|
||||
};
|
||||
const auto isoByPhone = Countries::Instance().countryISO2ByPhone(
|
||||
_controller->bot()->session().user()->phone());
|
||||
|
||||
const auto &[it, ok] = _details.emplace(
|
||||
i,
|
||||
container->add(Ui::PanelDetailsRow::Create(
|
||||
container,
|
||||
showBox,
|
||||
isoByPhone,
|
||||
row.inputType,
|
||||
row.label,
|
||||
maxLabelWidth,
|
||||
current.text,
|
||||
current.error,
|
||||
row.lengthLimit)));
|
||||
const bool details = (row.valueClass != Scheme::ValueClass::Scans);
|
||||
it->second->value(
|
||||
) | rpl::skip(1) | rpl::on_next([=] {
|
||||
if (details) {
|
||||
_fieldsChanged = true;
|
||||
updateCommonError();
|
||||
} else {
|
||||
Assert(_editScans != nullptr);
|
||||
_editScans->scanFieldsChanged(true);
|
||||
}
|
||||
}, it->second->lifetime());
|
||||
}
|
||||
|
||||
not_null<Ui::PanelDetailsRow*> PanelEditDocument::findRow(
|
||||
const QString &key) const {
|
||||
for (auto i = 0, count = int(_scheme.rows.size()); i != count; ++i) {
|
||||
const auto &row = _scheme.rows[i];
|
||||
if (row.key == key) {
|
||||
const auto it = _details.find(i);
|
||||
Assert(it != end(_details));
|
||||
return it->second.data();
|
||||
}
|
||||
}
|
||||
Unexpected("Row not found in PanelEditDocument::findRow.");
|
||||
}
|
||||
|
||||
void PanelEditDocument::updateCommonError() {
|
||||
if (_commonError) {
|
||||
_commonError->toggle(!_fieldsChanged, anim::type::normal);
|
||||
}
|
||||
}
|
||||
|
||||
void PanelEditDocument::focusInEvent(QFocusEvent *e) {
|
||||
crl::on_main(this, [=] {
|
||||
for (const auto &[index, row] : _details) {
|
||||
if (row->setFocusFast()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void PanelEditDocument::resizeEvent(QResizeEvent *e) {
|
||||
updateControlsGeometry();
|
||||
}
|
||||
|
||||
bool PanelEditDocument::hasUnsavedChanges() const {
|
||||
const auto result = collect();
|
||||
return _controller->editScopeChanged(result.data, result.filesData);
|
||||
}
|
||||
|
||||
void PanelEditDocument::updateControlsGeometry() {
|
||||
const auto submitTop = height() - _done->height();
|
||||
_scroll->setGeometry(0, 0, width(), submitTop);
|
||||
_done->resizeToWidth(width());
|
||||
_done->moveToLeft(0, submitTop);
|
||||
|
||||
_scroll->updateBars();
|
||||
}
|
||||
|
||||
PanelEditDocument::Result PanelEditDocument::collect() const {
|
||||
auto result = Result();
|
||||
for (const auto &[i, field] : _details) {
|
||||
const auto &row = _scheme.rows[i];
|
||||
auto &fields = (row.valueClass == Scheme::ValueClass::Scans)
|
||||
? result.filesData
|
||||
: result.data;
|
||||
if (row.valueClass == Scheme::ValueClass::Additional
|
||||
&& !_additionalShown) {
|
||||
continue;
|
||||
}
|
||||
fields.fields[row.key].text = field->valueCurrent();
|
||||
}
|
||||
if (!_additionalShown) {
|
||||
fillAdditionalFromFallbacks(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void PanelEditDocument::fillAdditionalFromFallbacks(Result &result) const {
|
||||
for (const auto &row : _scheme.rows) {
|
||||
if (row.valueClass != Scheme::ValueClass::Additional) {
|
||||
continue;
|
||||
}
|
||||
Assert(!row.additionalFallbackKey.isEmpty());
|
||||
auto &fields = result.data;
|
||||
const auto j = fields.fields.find(row.additionalFallbackKey);
|
||||
Assert(j != end(fields.fields));
|
||||
fields.fields[row.key] = j->second;
|
||||
}
|
||||
}
|
||||
|
||||
bool PanelEditDocument::validate() {
|
||||
auto error = _editScans
|
||||
? _editScans->validateGetErrorTop()
|
||||
: std::nullopt;
|
||||
if (error) {
|
||||
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);
|
||||
} 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<Ui::PanelDetailsRow>();
|
||||
for (const auto &[i, field] : ranges::views::reverse(_details)) {
|
||||
const auto &row = _scheme.rows[i];
|
||||
if (row.valueClass == Scheme::ValueClass::Additional
|
||||
&& !_additionalShown) {
|
||||
continue;
|
||||
}
|
||||
if (field->errorShown()) {
|
||||
field->showError();
|
||||
first = field;
|
||||
} else if (row.error) {
|
||||
if (const auto error = row.error(field->valueCurrent())) {
|
||||
field->showError(error);
|
||||
first = field;
|
||||
}
|
||||
}
|
||||
}
|
||||
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));
|
||||
const auto scrolldelta = firsttop.y() - scrolltop.y();
|
||||
_scroll->scrollToY(_scroll->scrollTop() + scrolldelta);
|
||||
return false;
|
||||
}
|
||||
|
||||
void PanelEditDocument::save() {
|
||||
if (!validate()) {
|
||||
return;
|
||||
}
|
||||
auto result = collect();
|
||||
_controller->saveScope(
|
||||
std::move(result.data),
|
||||
std::move(result.filesData));
|
||||
}
|
||||
|
||||
object_ptr<Ui::BoxContent> RequestIdentityType(
|
||||
Fn<void(int index)> submit,
|
||||
std::vector<QString> labels) {
|
||||
return Box<RequestTypeBox>(
|
||||
tr::lng_passport_identity_title(),
|
||||
tr::lng_passport_identity_about(tr::now),
|
||||
std::move(labels),
|
||||
submit);
|
||||
}
|
||||
|
||||
object_ptr<Ui::BoxContent> RequestAddressType(
|
||||
Fn<void(int index)> submit,
|
||||
std::vector<QString> labels) {
|
||||
return Box<RequestTypeBox>(
|
||||
tr::lng_passport_address_title(),
|
||||
tr::lng_passport_address_about(tr::now),
|
||||
std::move(labels),
|
||||
submit);
|
||||
}
|
||||
|
||||
object_ptr<Ui::BoxContent> ConfirmDeleteDocument(
|
||||
Fn<void(bool withDetails)> submit,
|
||||
const QString &text,
|
||||
const QString &detailsCheckbox) {
|
||||
return Box<DeleteDocumentBox>(text, detailsCheckbox, submit);
|
||||
}
|
||||
|
||||
} // namespace Passport
|
||||
183
Telegram/SourceFiles/passport/passport_panel_edit_document.h
Normal file
183
Telegram/SourceFiles/passport/passport_panel_edit_document.h
Normal file
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
#include "base/object_ptr.h"
|
||||
|
||||
namespace Ui {
|
||||
class InputField;
|
||||
class ScrollArea;
|
||||
class FlatLabel;
|
||||
class RoundButton;
|
||||
class VerticalLayout;
|
||||
class SettingsButton;
|
||||
class BoxContent;
|
||||
template <typename Widget>
|
||||
class SlideWrap;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Passport::Ui {
|
||||
using namespace ::Ui;
|
||||
enum class PanelDetailsType;
|
||||
class PanelDetailsRow;
|
||||
} // namespace Passport::Ui
|
||||
|
||||
namespace Passport {
|
||||
|
||||
class PanelController;
|
||||
struct ValueMap;
|
||||
struct ScanInfo;
|
||||
class EditScans;
|
||||
enum class FileType;
|
||||
struct ScanListData;
|
||||
|
||||
struct EditDocumentCountry {
|
||||
QString countryCode;
|
||||
QString languageCode;
|
||||
};
|
||||
|
||||
struct EditDocumentScheme {
|
||||
enum class ValueClass {
|
||||
Fields,
|
||||
Additional,
|
||||
Scans,
|
||||
};
|
||||
enum class AdditionalVisibility {
|
||||
Hidden,
|
||||
OnlyIfError,
|
||||
Shown,
|
||||
};
|
||||
using CountryInfo = EditDocumentCountry;
|
||||
struct Row {
|
||||
using Validator = Fn<std::optional<QString>(const QString &value)>;
|
||||
using Formatter = Fn<QString(const QString &value)>;
|
||||
ValueClass valueClass = ValueClass::Fields;
|
||||
Ui::PanelDetailsType inputType = Ui::PanelDetailsType();
|
||||
QString key;
|
||||
QString label;
|
||||
Validator error;
|
||||
Formatter format;
|
||||
int lengthLimit = 0;
|
||||
QString keyForAttachmentTo; // Attach [last|middle]_name to first_*.
|
||||
QString additionalFallbackKey; // *_name_native from *_name.
|
||||
};
|
||||
std::vector<Row> rows;
|
||||
QString fieldsHeader;
|
||||
QString detailsHeader;
|
||||
QString scansHeader;
|
||||
|
||||
QString additionalDependencyKey;
|
||||
Fn<AdditionalVisibility(const CountryInfo &dependency)> additionalShown;
|
||||
Fn<QString(const CountryInfo &dependency)> additionalHeader;
|
||||
Fn<QString(const CountryInfo &dependency)> additionalDescription;
|
||||
Fn<rpl::producer<CountryInfo>(const QString &)> preferredLanguage;
|
||||
|
||||
};
|
||||
|
||||
class PanelEditDocument : public Ui::RpWidget {
|
||||
public:
|
||||
using Scheme = EditDocumentScheme;
|
||||
|
||||
PanelEditDocument(
|
||||
QWidget *parent,
|
||||
not_null<PanelController*> controller,
|
||||
Scheme scheme,
|
||||
const QString &error,
|
||||
const ValueMap &data,
|
||||
const QString &scansError,
|
||||
const ValueMap &scansData,
|
||||
ScanListData &&scans,
|
||||
std::optional<ScanListData> &&translations,
|
||||
std::map<FileType, ScanInfo> &&specialFiles);
|
||||
PanelEditDocument(
|
||||
QWidget *parent,
|
||||
not_null<PanelController*> controller,
|
||||
Scheme scheme,
|
||||
const QString &scansError,
|
||||
const ValueMap &scansData,
|
||||
ScanListData &&scans,
|
||||
std::optional<ScanListData> &&translations,
|
||||
std::map<FileType, ScanInfo> &&specialFiles);
|
||||
PanelEditDocument(
|
||||
QWidget *parent,
|
||||
not_null<PanelController*> controller,
|
||||
Scheme scheme,
|
||||
const QString &error,
|
||||
const ValueMap &data);
|
||||
|
||||
bool hasUnsavedChanges() const;
|
||||
|
||||
protected:
|
||||
void focusInEvent(QFocusEvent *e) override;
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
|
||||
private:
|
||||
struct Result;
|
||||
void setupControls(
|
||||
const QString *error,
|
||||
const ValueMap *data,
|
||||
const QString *scansError,
|
||||
const ValueMap *scansData,
|
||||
ScanListData &&scans,
|
||||
std::optional<ScanListData> &&translations,
|
||||
std::map<FileType, ScanInfo> &&specialFiles);
|
||||
not_null<Ui::RpWidget*> setupContent(
|
||||
const QString *error,
|
||||
const ValueMap *data,
|
||||
const QString *scansError,
|
||||
const ValueMap *scansData,
|
||||
ScanListData &&scans,
|
||||
std::optional<ScanListData> &&translations,
|
||||
std::map<FileType, ScanInfo> &&specialFiles);
|
||||
void updateControlsGeometry();
|
||||
void updateCommonError();
|
||||
|
||||
Result collect() const;
|
||||
void fillAdditionalFromFallbacks(Result &result) const;
|
||||
bool validate();
|
||||
void save();
|
||||
|
||||
void createDetailsRow(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
int i,
|
||||
const Scheme::Row &row,
|
||||
const ValueMap &fields,
|
||||
int maxLabelWidth);
|
||||
not_null<Ui::PanelDetailsRow*> findRow(const QString &key) const;
|
||||
|
||||
not_null<PanelController*> _controller;
|
||||
Scheme _scheme;
|
||||
|
||||
object_ptr<Ui::ScrollArea> _scroll;
|
||||
|
||||
QPointer<EditScans> _editScans;
|
||||
QPointer<Ui::SlideWrap<Ui::FlatLabel>> _commonError;
|
||||
std::map<int, QPointer<Ui::PanelDetailsRow>> _details;
|
||||
bool _fieldsChanged = false;
|
||||
bool _additionalShown = false;
|
||||
|
||||
QPointer<Ui::SettingsButton> _delete;
|
||||
|
||||
object_ptr<Ui::RoundButton> _done;
|
||||
|
||||
};
|
||||
|
||||
object_ptr<Ui::BoxContent> RequestIdentityType(
|
||||
Fn<void(int index)> submit,
|
||||
std::vector<QString> labels);
|
||||
object_ptr<Ui::BoxContent> RequestAddressType(
|
||||
Fn<void(int index)> submit,
|
||||
std::vector<QString> labels);
|
||||
|
||||
object_ptr<Ui::BoxContent> ConfirmDeleteDocument(
|
||||
Fn<void(bool withDetails)> submit,
|
||||
const QString &text,
|
||||
const QString &detailsCheckbox = QString());
|
||||
|
||||
} // namespace Passport
|
||||
980
Telegram/SourceFiles/passport/passport_panel_edit_scans.cpp
Normal file
980
Telegram/SourceFiles/passport/passport_panel_edit_scans.cpp
Normal file
@@ -0,0 +1,980 @@
|
||||
/*
|
||||
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_options.h"
|
||||
#include "ui/image/image_prepare.h"
|
||||
#include "ui/painter.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 <QtCore/QBuffer>
|
||||
|
||||
namespace Passport {
|
||||
namespace {
|
||||
|
||||
constexpr auto kMaxDimensions = 2048;
|
||||
constexpr auto kMaxSize = 10 * 1024 * 1024;
|
||||
constexpr auto kJpegQuality = 89;
|
||||
|
||||
static_assert(kMaxSize <= Storage::kUseBigFilesFrom);
|
||||
|
||||
std::variant<ReadScanError, QByteArray> 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<Ui::FadeWrapScaled<Ui::IconButton>> _delete;
|
||||
object_ptr<Ui::FadeWrapScaled<Ui::RoundButton>> _restore;
|
||||
|
||||
};
|
||||
|
||||
struct EditScans::SpecialScan {
|
||||
SpecialScan(ScanInfo &&file);
|
||||
|
||||
ScanInfo file;
|
||||
QPointer<Ui::SlideWrap<Ui::FlatLabel>> header;
|
||||
QPointer<Ui::VerticalLayout> wrap;
|
||||
base::unique_qptr<Ui::SlideWrap<ScanButton>> row;
|
||||
QPointer<Ui::SettingsButton> upload;
|
||||
bool errorShown = false;
|
||||
Ui::Animations::Simple errorAnimation;
|
||||
rpl::variable<bool> rowCreated;
|
||||
};
|
||||
|
||||
void UpdateFileRow(
|
||||
not_null<ScanButton*> button,
|
||||
const ScanInfo &info) {
|
||||
button->setStatus(info.status);
|
||||
button->setImage(info.thumb);
|
||||
button->setDeleted(info.deleted);
|
||||
button->setError(!info.error.isEmpty());
|
||||
}
|
||||
|
||||
base::unique_qptr<Ui::SlideWrap<ScanButton>> CreateScan(
|
||||
not_null<Ui::VerticalLayout*> parent,
|
||||
const ScanInfo &info,
|
||||
const QString &name) {
|
||||
auto result = base::unique_qptr<Ui::SlideWrap<ScanButton>>(
|
||||
parent->add(object_ptr<Ui::SlideWrap<ScanButton>>(
|
||||
parent,
|
||||
object_ptr<ScanButton>(
|
||||
parent,
|
||||
st::passportScanRow,
|
||||
name,
|
||||
info.status,
|
||||
info.deleted,
|
||||
!info.error.isEmpty()))));
|
||||
result->entity()->setImage(info.thumb);
|
||||
return result;
|
||||
}
|
||||
|
||||
EditScans::List::List(
|
||||
not_null<PanelController*> controller,
|
||||
ScanListData &&data)
|
||||
: controller(controller)
|
||||
, files(std::move(data.files))
|
||||
, initialCount(int(files.size()))
|
||||
, errorMissing(data.errorMissing) {
|
||||
}
|
||||
|
||||
EditScans::List::List(
|
||||
not_null<PanelController*> controller)
|
||||
: List(controller, std::nullopt)
|
||||
{
|
||||
}
|
||||
|
||||
EditScans::List::List(
|
||||
not_null<PanelController*> controller,
|
||||
std::optional<ScanListData> &&data)
|
||||
: controller(controller)
|
||||
, files(data ? std::move(data->files) : std::vector<ScanInfo>())
|
||||
, 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<ScanButton> *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<QString> EditScans::List::uploadButtonText() const {
|
||||
return (files.empty()
|
||||
? tr::lng_passport_upload_scans
|
||||
: tr::lng_passport_upload_more)(tr::upper);
|
||||
}
|
||||
|
||||
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::on_next([=] {
|
||||
controller->deleteScan(type, index);
|
||||
}, scan->lifetime());
|
||||
|
||||
scan->restoreClicks(
|
||||
) | rpl::on_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<Ui::IconButton>(this, _st.remove))
|
||||
, _restore(
|
||||
this,
|
||||
object_ptr<Ui::RoundButton>(
|
||||
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<PanelController*> controller,
|
||||
const QString &header,
|
||||
const QString &error,
|
||||
ScanListData &&scans,
|
||||
std::optional<ScanListData> &&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<PanelController*> controller,
|
||||
const QString &header,
|
||||
const QString &error,
|
||||
std::map<FileType, ScanInfo> &&specialFiles,
|
||||
std::optional<ScanListData> &&translations)
|
||||
: RpWidget(parent)
|
||||
, _controller(controller)
|
||||
, _error(error)
|
||||
, _content(this)
|
||||
, _scansList(_controller)
|
||||
, _translationsList(_controller, std::move(translations)) {
|
||||
setupSpecialScans(header, std::move(specialFiles));
|
||||
}
|
||||
|
||||
std::optional<int> EditScans::validateGetErrorTop() {
|
||||
auto result = std::optional<int>();
|
||||
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<Ui::SlideWrap<Ui::FlatLabel>>(
|
||||
inner,
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
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<Ui::VerticalLayout*> 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<Ui::SlideWrap<Ui::BoxContentDivider>>(
|
||||
container,
|
||||
object_ptr<Ui::BoxContentDivider>(
|
||||
container,
|
||||
st::passportFormDividerHeight)));
|
||||
list.divider->toggle(list.files.empty(), anim::type::instant);
|
||||
}
|
||||
list.header = container->add(
|
||||
object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
|
||||
container,
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
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<Ui::SlideWrap<Ui::FlatLabel>>(
|
||||
container,
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
container,
|
||||
list.errorMissing,
|
||||
st::passportVerifyErrorLabel),
|
||||
st::passportUploadErrorPadding));
|
||||
list.uploadMoreError->toggle(true, anim::type::instant);
|
||||
}
|
||||
list.wrap = container->add(object_ptr<Ui::VerticalLayout>(container));
|
||||
for (const auto &scan : list.files) {
|
||||
list.pushScan(scan);
|
||||
list.rows.back()->show(anim::type::instant);
|
||||
}
|
||||
|
||||
list.upload = container->add(
|
||||
object_ptr<Ui::SettingsButton>(
|
||||
container,
|
||||
list.uploadTexts.events_starting_with(
|
||||
list.uploadButtonText()
|
||||
) | rpl::flatten_latest(),
|
||||
st::passportUploadButton),
|
||||
st::passportUploadButtonPadding);
|
||||
list.upload->addClickHandler([=] {
|
||||
chooseScan(type);
|
||||
});
|
||||
|
||||
container->add(object_ptr<Ui::BoxContentDivider>(
|
||||
container,
|
||||
st::passportFormDividerHeight));
|
||||
}
|
||||
|
||||
void EditScans::setupSpecialScans(
|
||||
const QString &header,
|
||||
std::map<FileType, ScanInfo> &&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<Ui::SlideWrap<Ui::FlatLabel>>(
|
||||
inner,
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
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<Ui::SlideWrap<Ui::FlatLabel>>(
|
||||
inner,
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
inner,
|
||||
header,
|
||||
st::passportFormHeader),
|
||||
st::passportUploadHeaderPadding));
|
||||
scan.header->toggle(scan.file.key.id != 0, anim::type::instant);
|
||||
}
|
||||
scan.wrap = inner->add(object_ptr<Ui::VerticalLayout>(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)(tr::upper);
|
||||
}) | rpl::flatten_latest();
|
||||
scan.upload = inner->add(
|
||||
object_ptr<Ui::SettingsButton>(
|
||||
inner,
|
||||
std::move(label),
|
||||
st::passportUploadButton),
|
||||
st::passportUploadButtonPadding);
|
||||
scan.upload->addClickHandler([=, type = type] {
|
||||
chooseScan(type);
|
||||
});
|
||||
|
||||
inner->add(object_ptr<Ui::DividerLabel>(
|
||||
inner,
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
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::on_next([=](ScanInfo &&info) {
|
||||
updateScan(std::move(info));
|
||||
}, lifetime());
|
||||
|
||||
widthValue(
|
||||
) | rpl::on_next([=](int width) {
|
||||
_content->resizeToWidth(width);
|
||||
}, _content->lifetime());
|
||||
|
||||
_content->heightValue(
|
||||
) | rpl::on_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::on_next([=] {
|
||||
_controller->deleteScan(type, std::nullopt);
|
||||
}, row->lifetime());
|
||||
|
||||
row->restoreClicks(
|
||||
) | rpl::on_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<QWidget> parent,
|
||||
FileType type,
|
||||
Fn<void(QByteArray&&)> doneCallback,
|
||||
Fn<void(ReadScanError)> 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<ReadScanError>(&result)) {
|
||||
onMainError(*error);
|
||||
} else {
|
||||
auto content = std::get_if<QByteArray>(&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
|
||||
153
Telegram/SourceFiles/passport/passport_panel_edit_scans.h
Normal file
153
Telegram/SourceFiles/passport/passport_panel_edit_scans.h
Normal file
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "base/object_ptr.h"
|
||||
|
||||
namespace Ui {
|
||||
class BoxContentDivider;
|
||||
class VerticalLayout;
|
||||
class SettingsButton;
|
||||
class FlatLabel;
|
||||
template <typename Widget>
|
||||
class SlideWrap;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Passport {
|
||||
|
||||
enum class FileType;
|
||||
class PanelController;
|
||||
class ScanButton;
|
||||
struct ScanInfo;
|
||||
|
||||
enum class ReadScanError {
|
||||
FileTooLarge,
|
||||
CantReadImage,
|
||||
BadImageSize,
|
||||
Unknown,
|
||||
};
|
||||
|
||||
struct ScanListData {
|
||||
std::vector<ScanInfo> files;
|
||||
QString errorMissing;
|
||||
};
|
||||
|
||||
class EditScans : public Ui::RpWidget {
|
||||
public:
|
||||
EditScans(
|
||||
QWidget *parent,
|
||||
not_null<PanelController*> controller,
|
||||
const QString &header,
|
||||
const QString &error,
|
||||
ScanListData &&scans,
|
||||
std::optional<ScanListData> &&translations);
|
||||
EditScans(
|
||||
QWidget *parent,
|
||||
not_null<PanelController*> controller,
|
||||
const QString &header,
|
||||
const QString &error,
|
||||
std::map<FileType, ScanInfo> &&specialFiles,
|
||||
std::optional<ScanListData> &&translations);
|
||||
|
||||
std::optional<int> validateGetErrorTop();
|
||||
|
||||
void scanFieldsChanged(bool changed);
|
||||
|
||||
static void ChooseScan(
|
||||
QPointer<QWidget> parent,
|
||||
FileType type,
|
||||
Fn<void(QByteArray&&)> doneCallback,
|
||||
Fn<void(ReadScanError)> errorCallback);
|
||||
|
||||
~EditScans();
|
||||
|
||||
private:
|
||||
struct SpecialScan;
|
||||
struct List {
|
||||
List(not_null<PanelController*> controller, ScanListData &&data);
|
||||
List(not_null<PanelController*> controller);
|
||||
List(
|
||||
not_null<PanelController*> controller,
|
||||
std::optional<ScanListData> &&data);
|
||||
|
||||
bool uploadedSomeMore() const;
|
||||
bool uploadMoreRequired() const;
|
||||
Ui::SlideWrap<ScanButton> *nonDeletedErrorRow() const;
|
||||
rpl::producer<QString> uploadButtonText() const;
|
||||
void toggleError(bool shown);
|
||||
void hideError();
|
||||
void errorAnimationCallback();
|
||||
void updateScan(ScanInfo &&info, int width);
|
||||
void pushScan(const ScanInfo &info);
|
||||
|
||||
not_null<PanelController*> controller;
|
||||
std::vector<ScanInfo> files;
|
||||
std::optional<int> initialCount;
|
||||
QString errorMissing;
|
||||
QPointer<Ui::SlideWrap<Ui::BoxContentDivider>> divider;
|
||||
QPointer<Ui::SlideWrap<Ui::FlatLabel>> header;
|
||||
QPointer<Ui::SlideWrap<Ui::FlatLabel>> uploadMoreError;
|
||||
QPointer<Ui::VerticalLayout> wrap;
|
||||
std::vector<base::unique_qptr<Ui::SlideWrap<ScanButton>>> rows;
|
||||
QPointer<Ui::SettingsButton> upload;
|
||||
rpl::event_stream<rpl::producer<QString>> uploadTexts;
|
||||
bool errorShown = false;
|
||||
Ui::Animations::Simple errorAnimation;
|
||||
};
|
||||
|
||||
List &list(FileType type);
|
||||
const List &list(FileType type) const;
|
||||
|
||||
void setupScans(const QString &header);
|
||||
void setupList(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
FileType type,
|
||||
const QString &header);
|
||||
void setupSpecialScans(
|
||||
const QString &header,
|
||||
std::map<FileType, ScanInfo> &&files);
|
||||
void init();
|
||||
|
||||
void chooseScan(FileType type);
|
||||
void updateScan(ScanInfo &&info);
|
||||
void updateSpecialScan(ScanInfo &&info);
|
||||
void createSpecialScanRow(
|
||||
SpecialScan &scan,
|
||||
const ScanInfo &info,
|
||||
bool requiresBothSides);
|
||||
base::unique_qptr<Ui::SlideWrap<ScanButton>> createScan(
|
||||
not_null<Ui::VerticalLayout*> parent,
|
||||
const ScanInfo &info,
|
||||
const QString &name);
|
||||
SpecialScan &findSpecialScan(FileType type);
|
||||
|
||||
void updateErrorLabels();
|
||||
bool somethingChanged() const;
|
||||
|
||||
void toggleSpecialScanError(FileType type, bool shown);
|
||||
void hideSpecialScanError(FileType type);
|
||||
void specialScanErrorAnimationCallback(FileType type);
|
||||
void specialScanChanged(FileType type, bool changed);
|
||||
|
||||
not_null<PanelController*> _controller;
|
||||
QString _error;
|
||||
object_ptr<Ui::VerticalLayout> _content;
|
||||
QPointer<Ui::SlideWrap<Ui::FlatLabel>> _commonError;
|
||||
bool _scanFieldsChanged = false;
|
||||
bool _specialScanChanged = false;
|
||||
|
||||
List _scansList;
|
||||
std::map<FileType, SpecialScan> _specialScans;
|
||||
List _translationsList;
|
||||
|
||||
|
||||
};
|
||||
|
||||
} // namespace Passport
|
||||
181
Telegram/SourceFiles/passport/passport_panel_form.cpp
Normal file
181
Telegram/SourceFiles/passport/passport_panel_form.cpp
Normal file
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
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_form.h"
|
||||
|
||||
#include "passport/passport_panel_controller.h"
|
||||
#include "passport/ui/passport_form_row.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "boxes/abstract_box.h"
|
||||
#include "core/click_handler_types.h"
|
||||
#include "data/data_user.h"
|
||||
#include "ui/controls/userpic_button.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/effects/scroll_content_shadow.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/box_content_divider.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "ui/wrap/padding_wrap.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/text/text_options.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "styles/style_passport.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_boxes.h"
|
||||
|
||||
namespace Passport {
|
||||
|
||||
PanelForm::PanelForm(
|
||||
QWidget *parent,
|
||||
not_null<PanelController*> controller)
|
||||
: RpWidget(parent)
|
||||
, _controller(controller)
|
||||
, _scroll(this, st::passportPanelScroll)
|
||||
, _submit(
|
||||
this,
|
||||
tr::lng_passport_authorize(),
|
||||
st::passportPanelAuthorize) {
|
||||
setupControls();
|
||||
}
|
||||
|
||||
void PanelForm::setupControls() {
|
||||
const auto inner = setupContent();
|
||||
|
||||
_submit->addClickHandler([=] {
|
||||
_controller->submitForm();
|
||||
});
|
||||
|
||||
SetupShadowsToScrollContent(this, _scroll, inner->heightValue());
|
||||
}
|
||||
|
||||
not_null<Ui::RpWidget*> PanelForm::setupContent() {
|
||||
const auto bot = _controller->bot();
|
||||
|
||||
const auto inner = _scroll->setOwnedWidget(
|
||||
object_ptr<Ui::VerticalLayout>(this));
|
||||
_scroll->widthValue(
|
||||
) | rpl::on_next([=](int width) {
|
||||
inner->resizeToWidth(width);
|
||||
}, inner->lifetime());
|
||||
|
||||
_userpic = inner->add(
|
||||
object_ptr<Ui::UserpicButton>(
|
||||
inner,
|
||||
bot,
|
||||
st::passportFormUserpic),
|
||||
st::passportFormUserpicPadding,
|
||||
style::al_top);
|
||||
|
||||
_about1 = inner->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
inner,
|
||||
tr::lng_passport_request1(tr::now, lt_bot, bot->name()),
|
||||
st::passportPasswordLabelBold),
|
||||
st::passportFormAbout1Padding,
|
||||
style::al_top);
|
||||
|
||||
_about2 = inner->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
inner,
|
||||
tr::lng_passport_request2(tr::now),
|
||||
st::passportPasswordLabel),
|
||||
st::passportFormAbout2Padding,
|
||||
style::al_top);
|
||||
|
||||
inner->add(object_ptr<Ui::BoxContentDivider>(
|
||||
inner,
|
||||
st::passportFormDividerHeight));
|
||||
inner->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
inner,
|
||||
tr::lng_passport_header(tr::now),
|
||||
st::passportFormHeader),
|
||||
st::passportFormHeaderPadding);
|
||||
|
||||
auto index = 0;
|
||||
_controller->fillRows([&](
|
||||
QString title,
|
||||
QString description,
|
||||
bool ready,
|
||||
bool error) {
|
||||
_rows.push_back(inner->add(object_ptr<Row>(this)));
|
||||
_rows.back()->addClickHandler([=] {
|
||||
_controller->editScope(index);
|
||||
});
|
||||
_rows.back()->updateContent(
|
||||
title,
|
||||
description,
|
||||
ready,
|
||||
error,
|
||||
anim::type::instant);
|
||||
++index;
|
||||
});
|
||||
_controller->refillRows(
|
||||
) | rpl::on_next([=] {
|
||||
auto index = 0;
|
||||
_controller->fillRows([&](
|
||||
QString title,
|
||||
QString description,
|
||||
bool ready,
|
||||
bool error) {
|
||||
Expects(index < _rows.size());
|
||||
|
||||
_rows[index++]->updateContent(
|
||||
title,
|
||||
description,
|
||||
ready,
|
||||
error,
|
||||
anim::type::normal);
|
||||
});
|
||||
}, lifetime());
|
||||
const auto policyUrl = _controller->privacyPolicyUrl();
|
||||
auto policyLink = tr::lng_passport_policy(
|
||||
lt_bot,
|
||||
rpl::single(bot->name())
|
||||
) | rpl::map(
|
||||
tr::url(policyUrl)
|
||||
) | rpl::map([=](TextWithEntities &&text) {
|
||||
return Ui::Text::Wrapped(std::move(text), EntityType::Bold);
|
||||
});
|
||||
auto text = policyUrl.isEmpty()
|
||||
? tr::lng_passport_allow(
|
||||
lt_bot,
|
||||
rpl::single(tr::marked('@' + bot->username())),
|
||||
tr::marked)
|
||||
: tr::lng_passport_accept_allow(
|
||||
lt_policy,
|
||||
std::move(policyLink),
|
||||
lt_bot,
|
||||
rpl::single(tr::marked('@' + bot->username())),
|
||||
tr::marked);
|
||||
const auto policy = inner->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
inner,
|
||||
std::move(text),
|
||||
st::passportFormPolicy),
|
||||
st::passportFormPolicyPadding);
|
||||
policy->setLinksTrusted();
|
||||
|
||||
return inner;
|
||||
}
|
||||
|
||||
void PanelForm::resizeEvent(QResizeEvent *e) {
|
||||
updateControlsGeometry();
|
||||
}
|
||||
|
||||
void PanelForm::updateControlsGeometry() {
|
||||
const auto submitTop = height() - _submit->height();
|
||||
_scroll->setGeometry(0, 0, width(), submitTop);
|
||||
_submit->setFullWidth(width());
|
||||
_submit->moveToLeft(0, submitTop);
|
||||
|
||||
_scroll->updateBars();
|
||||
}
|
||||
|
||||
} // namespace Passport
|
||||
58
Telegram/SourceFiles/passport/passport_panel_form.h
Normal file
58
Telegram/SourceFiles/passport/passport_panel_form.h
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
#include "base/object_ptr.h"
|
||||
|
||||
namespace Ui {
|
||||
class BoxContentDivider;
|
||||
class ScrollArea;
|
||||
class RoundButton;
|
||||
class FlatLabel;
|
||||
class UserpicButton;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Passport::Ui {
|
||||
using namespace ::Ui;
|
||||
class FormRow;
|
||||
} // namespace Passport::Ui
|
||||
|
||||
namespace Passport {
|
||||
|
||||
class PanelController;
|
||||
|
||||
class PanelForm : public Ui::RpWidget {
|
||||
public:
|
||||
PanelForm(
|
||||
QWidget *parent,
|
||||
not_null<PanelController*> controller);
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
|
||||
private:
|
||||
using Row = Ui::FormRow;
|
||||
|
||||
void setupControls();
|
||||
not_null<Ui::RpWidget*> setupContent();
|
||||
void updateControlsGeometry();
|
||||
|
||||
not_null<PanelController*> _controller;
|
||||
|
||||
object_ptr<Ui::ScrollArea> _scroll;
|
||||
object_ptr<Ui::RoundButton> _submit;
|
||||
|
||||
QPointer<Ui::UserpicButton> _userpic;
|
||||
QPointer<Ui::FlatLabel> _about1;
|
||||
QPointer<Ui::FlatLabel> _about2;
|
||||
std::vector<QPointer<Row>> _rows;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Passport
|
||||
269
Telegram/SourceFiles/passport/passport_panel_password.cpp
Normal file
269
Telegram/SourceFiles/passport/passport_panel_password.cpp
Normal file
@@ -0,0 +1,269 @@
|
||||
/*
|
||||
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_password.h"
|
||||
|
||||
#include "passport/passport_panel_controller.h"
|
||||
#include "ui/controls/userpic_button.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/fields/password_input.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "ui/wrap/padding_wrap.h"
|
||||
#include "boxes/passcode_box.h"
|
||||
#include "data/data_user.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "info/profile/info_profile_icon.h"
|
||||
#include "styles/style_passport.h"
|
||||
#include "styles/style_layers.h"
|
||||
|
||||
namespace Passport {
|
||||
|
||||
PanelAskPassword::PanelAskPassword(
|
||||
QWidget *parent,
|
||||
not_null<PanelController*> controller)
|
||||
: RpWidget(parent)
|
||||
, _controller(controller)
|
||||
, _userpic(
|
||||
this,
|
||||
_controller->bot(),
|
||||
st::passportPasswordUserpic)
|
||||
, _about1(
|
||||
this,
|
||||
tr::lng_passport_request1(
|
||||
tr::now,
|
||||
lt_bot,
|
||||
_controller->bot()->name()),
|
||||
st::passportPasswordLabelBold)
|
||||
, _about2(
|
||||
this,
|
||||
tr::lng_passport_request2(tr::now),
|
||||
st::passportPasswordLabel)
|
||||
, _password(
|
||||
this,
|
||||
st::defaultInputField,
|
||||
tr::lng_passport_password_placeholder())
|
||||
, _submit(this, tr::lng_passport_next(), st::passportPasswordSubmit)
|
||||
, _forgot(this, tr::lng_signin_recover(tr::now), st::defaultLinkButton) {
|
||||
connect(_password, &Ui::PasswordInput::submitted, this, [=] {
|
||||
submit();
|
||||
});
|
||||
connect(_password, &Ui::PasswordInput::changed, this, [=] {
|
||||
hideError();
|
||||
});
|
||||
if (const auto hint = _controller->passwordHint(); !hint.isEmpty()) {
|
||||
_hint.create(
|
||||
this,
|
||||
hint,
|
||||
st::passportPasswordHintLabel);
|
||||
}
|
||||
_controller->passwordError(
|
||||
) | rpl::on_next([=](const QString &error) {
|
||||
showError(error);
|
||||
}, lifetime());
|
||||
|
||||
_forgot->addClickHandler([=] {
|
||||
recover();
|
||||
});
|
||||
|
||||
_password->setFocusFast();
|
||||
_userpic->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
|
||||
_submit->addClickHandler([=] {
|
||||
submit();
|
||||
});
|
||||
}
|
||||
|
||||
void PanelAskPassword::showError(const QString &error) {
|
||||
_password->showError();
|
||||
_error.create(
|
||||
this,
|
||||
error,
|
||||
st::passportErrorLabel);
|
||||
_error->show();
|
||||
updateControlsGeometry();
|
||||
}
|
||||
|
||||
void PanelAskPassword::hideError() {
|
||||
_error.destroy();
|
||||
}
|
||||
|
||||
void PanelAskPassword::submit() {
|
||||
_controller->submitPassword(_password->getLastText().toUtf8());
|
||||
}
|
||||
|
||||
void PanelAskPassword::recover() {
|
||||
_controller->recoverPassword();
|
||||
}
|
||||
|
||||
void PanelAskPassword::resizeEvent(QResizeEvent *e) {
|
||||
updateControlsGeometry();
|
||||
}
|
||||
|
||||
void PanelAskPassword::focusInEvent(QFocusEvent *e) {
|
||||
crl::on_main(this, [=] {
|
||||
_password->setFocusFast();
|
||||
});
|
||||
}
|
||||
|
||||
void PanelAskPassword::updateControlsGeometry() {
|
||||
const auto padding = st::passportPasswordPadding;
|
||||
const auto availableWidth = width()
|
||||
- st::boxPadding.left()
|
||||
- st::boxPadding.right();
|
||||
|
||||
auto top = st::passportPasswordFieldBottom;
|
||||
top -= _password->height();
|
||||
_password->resize(
|
||||
st::passportPasswordSubmit.width,
|
||||
_password->height());
|
||||
_password->moveToLeft((width() - _password->width()) / 2, top);
|
||||
|
||||
top -= st::passportPasswordFieldSkip + _about2->height();
|
||||
_about2->resizeToWidth(availableWidth);
|
||||
_about2->moveToLeft(padding.left(), top);
|
||||
|
||||
top -= _about1->height();
|
||||
_about1->resizeToWidth(availableWidth);
|
||||
_about1->moveToLeft(padding.left(), top);
|
||||
|
||||
top -= st::passportPasswordUserpicSkip + _userpic->height();
|
||||
_userpic->moveToLeft((width() - _userpic->width()) / 2, top);
|
||||
|
||||
top = st::passportPasswordFieldBottom;
|
||||
if (_hint) {
|
||||
top += st::passportPasswordHintSkip;
|
||||
_hint->resizeToWidth(availableWidth);
|
||||
_hint->moveToLeft(padding.left(), top);
|
||||
top += _hint->height();
|
||||
}
|
||||
if (_error) {
|
||||
top += st::passportPasswordHintSkip;
|
||||
_error->resizeToWidth(availableWidth);
|
||||
_error->moveToLeft(padding.left(), top);
|
||||
top += _error->height();
|
||||
}
|
||||
|
||||
top = height() - st::passportPasswordSubmitBottom - _submit->height();
|
||||
_submit->moveToLeft((width() - _submit->width()) / 2, top);
|
||||
|
||||
top = height() - st::passportPasswordForgotBottom - _forgot->height();
|
||||
_forgot->moveToLeft((width() - _forgot->width()) / 2, top);
|
||||
}
|
||||
|
||||
PanelNoPassword::PanelNoPassword(
|
||||
QWidget *parent,
|
||||
not_null<PanelController*> controller)
|
||||
: RpWidget(parent)
|
||||
, _controller(controller)
|
||||
, _inner(Ui::CreateChild<Ui::VerticalLayout>(this)) {
|
||||
setupContent();
|
||||
}
|
||||
|
||||
void PanelNoPassword::setupContent() {
|
||||
widthValue(
|
||||
) | rpl::on_next([=](int newWidth) {
|
||||
_inner->resizeToWidth(newWidth);
|
||||
}, _inner->lifetime());
|
||||
|
||||
_inner->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
_inner,
|
||||
tr::lng_passport_request1(
|
||||
tr::now,
|
||||
lt_bot,
|
||||
_controller->bot()->name()),
|
||||
st::passportPasswordLabelBold),
|
||||
st::passportPasswordAbout1Padding,
|
||||
style::al_top);
|
||||
|
||||
_inner->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
_inner,
|
||||
tr::lng_passport_request2(tr::now),
|
||||
st::passportPasswordLabel),
|
||||
st::passportPasswordAbout2Padding,
|
||||
style::al_top);
|
||||
|
||||
const auto iconWrap = _inner->add(
|
||||
object_ptr<Ui::FixedHeightWidget>(
|
||||
_inner,
|
||||
st::passportPasswordIconHeight),
|
||||
style::al_top);
|
||||
iconWrap->setNaturalWidth(st::passportPasswordIcon.width());
|
||||
Ui::CreateChild<Info::Profile::FloatingIcon>(
|
||||
iconWrap,
|
||||
st::passportPasswordIcon,
|
||||
QPoint(0, 0));
|
||||
|
||||
_inner->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
_inner,
|
||||
tr::lng_passport_create_password(tr::now),
|
||||
st::passportPasswordSetupLabel),
|
||||
st::passportFormAbout2Padding,
|
||||
style::al_top);
|
||||
|
||||
refreshBottom();
|
||||
}
|
||||
|
||||
void PanelNoPassword::refreshBottom() {
|
||||
const auto pattern = _controller->unconfirmedEmailPattern();
|
||||
_about.reset(_inner->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
_inner,
|
||||
(pattern.isEmpty()
|
||||
? tr::lng_passport_about_password(tr::now)
|
||||
: tr::lng_passport_code_sent(tr::now, lt_email, pattern)),
|
||||
st::passportPasswordSetupLabel),
|
||||
st::passportFormAbout2Padding,
|
||||
style::al_top));
|
||||
if (pattern.isEmpty()) {
|
||||
const auto button = _inner->add(
|
||||
object_ptr<Ui::RoundButton>(
|
||||
_inner,
|
||||
tr::lng_passport_password_create(),
|
||||
st::defaultBoxButton),
|
||||
style::al_top);
|
||||
button->addClickHandler([=] {
|
||||
_controller->setupPassword();
|
||||
});
|
||||
} else {
|
||||
const auto container = _inner->add(
|
||||
object_ptr<Ui::FixedHeightWidget>(
|
||||
_inner,
|
||||
st::defaultBoxButton.height));
|
||||
const auto cancel = Ui::CreateChild<Ui::RoundButton>(
|
||||
container,
|
||||
tr::lng_cancel(),
|
||||
st::defaultBoxButton);
|
||||
cancel->setTextTransform(
|
||||
Ui::RoundButton::TextTransform::NoTransform);
|
||||
cancel->addClickHandler([=] {
|
||||
_controller->cancelPasswordSubmit();
|
||||
});
|
||||
const auto validate = Ui::CreateChild<Ui::RoundButton>(
|
||||
container,
|
||||
tr::lng_passport_email_validate(),
|
||||
st::defaultBoxButton);
|
||||
validate->setTextTransform(
|
||||
Ui::RoundButton::TextTransform::NoTransform);
|
||||
validate->addClickHandler([=] {
|
||||
_controller->validateRecoveryEmail();
|
||||
});
|
||||
container->widthValue(
|
||||
) | rpl::on_next([=](int width) {
|
||||
const auto both = cancel->width()
|
||||
+ validate->width()
|
||||
+ st::boxLittleSkip;
|
||||
cancel->moveToLeft((width - both) / 2, 0, width);
|
||||
validate->moveToRight((width - both) / 2, 0, width);
|
||||
}, container->lifetime());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Passport
|
||||
74
Telegram/SourceFiles/passport/passport_panel_password.h
Normal file
74
Telegram/SourceFiles/passport/passport_panel_password.h
Normal file
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
#include "base/object_ptr.h"
|
||||
|
||||
namespace Ui {
|
||||
class PasswordInput;
|
||||
class FlatLabel;
|
||||
class LinkButton;
|
||||
class RoundButton;
|
||||
class UserpicButton;
|
||||
class VerticalLayout;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Passport {
|
||||
|
||||
class PanelController;
|
||||
|
||||
class PanelAskPassword : public Ui::RpWidget {
|
||||
public:
|
||||
PanelAskPassword(
|
||||
QWidget *parent,
|
||||
not_null<PanelController*> controller);
|
||||
|
||||
void submit();
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
void focusInEvent(QFocusEvent *e) override;
|
||||
|
||||
private:
|
||||
void updateControlsGeometry();
|
||||
void showError(const QString &error);
|
||||
void hideError();
|
||||
void recover();
|
||||
|
||||
not_null<PanelController*> _controller;
|
||||
|
||||
object_ptr<Ui::UserpicButton> _userpic;
|
||||
object_ptr<Ui::FlatLabel> _about1;
|
||||
object_ptr<Ui::FlatLabel> _about2;
|
||||
object_ptr<Ui::PasswordInput> _password;
|
||||
object_ptr<Ui::FlatLabel> _hint = { nullptr };
|
||||
object_ptr<Ui::FlatLabel> _error = { nullptr };
|
||||
object_ptr<Ui::RoundButton> _submit;
|
||||
object_ptr<Ui::LinkButton> _forgot;
|
||||
|
||||
};
|
||||
|
||||
class PanelNoPassword : public Ui::RpWidget {
|
||||
public:
|
||||
PanelNoPassword(
|
||||
QWidget *parent,
|
||||
not_null<PanelController*> controller);
|
||||
|
||||
private:
|
||||
void setupContent();
|
||||
void refreshBottom();
|
||||
|
||||
not_null<PanelController*> _controller;
|
||||
|
||||
not_null<Ui::VerticalLayout*> _inner;
|
||||
base::unique_qptr<Ui::RpWidget> _about;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Passport
|
||||
1145
Telegram/SourceFiles/passport/ui/passport_details_row.cpp
Normal file
1145
Telegram/SourceFiles/passport/ui/passport_details_row.cpp
Normal file
File diff suppressed because it is too large
Load Diff
85
Telegram/SourceFiles/passport/ui/passport_details_row.h
Normal file
85
Telegram/SourceFiles/passport/ui/passport_details_row.h
Normal file
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/wrap/padding_wrap.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
|
||||
namespace Ui {
|
||||
class BoxContent;
|
||||
class InputField;
|
||||
class FlatLabel;
|
||||
template <typename Widget>
|
||||
class SlideWrap;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Passport::Ui {
|
||||
|
||||
using namespace ::Ui;
|
||||
|
||||
enum class PanelDetailsType {
|
||||
Text,
|
||||
Postcode,
|
||||
Country,
|
||||
Date,
|
||||
Gender,
|
||||
};
|
||||
|
||||
class PanelDetailsRow : public RpWidget {
|
||||
public:
|
||||
using Type = PanelDetailsType;
|
||||
|
||||
PanelDetailsRow(
|
||||
QWidget *parent,
|
||||
const QString &label,
|
||||
int maxLabelWidth);
|
||||
|
||||
static object_ptr<PanelDetailsRow> Create(
|
||||
QWidget *parent,
|
||||
Fn<void(object_ptr<BoxContent>)> showBox,
|
||||
const QString &defaultCountry,
|
||||
Type type,
|
||||
const QString &label,
|
||||
int maxLabelWidth,
|
||||
const QString &value,
|
||||
const QString &error,
|
||||
int limit = 0);
|
||||
static int LabelWidth(const QString &label);
|
||||
|
||||
virtual bool setFocusFast();
|
||||
virtual rpl::producer<QString> value() const = 0;
|
||||
virtual QString valueCurrent() const = 0;
|
||||
void showError(std::optional<QString> error = std::nullopt);
|
||||
bool errorShown() const;
|
||||
void hideError();
|
||||
void finishAnimating();
|
||||
|
||||
protected:
|
||||
int resizeGetHeight(int newWidth) override;
|
||||
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
private:
|
||||
virtual int resizeInner(int left, int top, int width) = 0;
|
||||
virtual void showInnerError() = 0;
|
||||
virtual void finishInnerAnimating() = 0;
|
||||
|
||||
void startErrorAnimation(bool shown);
|
||||
|
||||
QString _label;
|
||||
int _maxLabelWidth = 0;
|
||||
object_ptr<SlideWrap<FlatLabel>> _error = { nullptr };
|
||||
bool _errorShown = false;
|
||||
bool _errorHideSubscription = false;
|
||||
Animations::Simple _errorAnimation;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Passport::Ui
|
||||
126
Telegram/SourceFiles/passport/ui/passport_form_row.cpp
Normal file
126
Telegram/SourceFiles/passport/ui/passport_form_row.cpp
Normal file
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "passport/ui/passport_form_row.h"
|
||||
|
||||
#include "ui/text/text_options.h"
|
||||
#include "ui/painter.h"
|
||||
#include "styles/style_passport.h"
|
||||
#include "styles/style_layers.h"
|
||||
|
||||
namespace Passport::Ui {
|
||||
|
||||
FormRow::FormRow(QWidget *parent)
|
||||
: RippleButton(parent, st::passportRowRipple)
|
||||
, _title(st::boxWideWidth / 2)
|
||||
, _description(st::boxWideWidth / 2) {
|
||||
}
|
||||
|
||||
void FormRow::updateContent(
|
||||
const QString &title,
|
||||
const QString &description,
|
||||
bool ready,
|
||||
bool error,
|
||||
anim::type animated) {
|
||||
_title.setText(
|
||||
st::semiboldTextStyle,
|
||||
title,
|
||||
NameTextOptions());
|
||||
_description.setText(
|
||||
st::defaultTextStyle,
|
||||
description,
|
||||
TextParseOptions {
|
||||
TextParseMultiline,
|
||||
0,
|
||||
0,
|
||||
Qt::LayoutDirectionAuto
|
||||
});
|
||||
_ready = ready && !error;
|
||||
if (_error != error) {
|
||||
_error = error;
|
||||
if (animated == anim::type::instant) {
|
||||
_errorAnimation.stop();
|
||||
} else {
|
||||
_errorAnimation.start(
|
||||
[=] { update(); },
|
||||
_error ? 0. : 1.,
|
||||
_error ? 1. : 0.,
|
||||
st::fadeWrapDuration);
|
||||
}
|
||||
}
|
||||
resizeToWidth(width());
|
||||
update();
|
||||
}
|
||||
|
||||
int FormRow::resizeGetHeight(int newWidth) {
|
||||
const auto availableWidth = countAvailableWidth(newWidth);
|
||||
_titleHeight = _title.countHeight(availableWidth);
|
||||
_descriptionHeight = _description.countHeight(availableWidth);
|
||||
const auto result = st::passportRowPadding.top()
|
||||
+ _titleHeight
|
||||
+ st::passportRowSkip
|
||||
+ _descriptionHeight
|
||||
+ st::passportRowPadding.bottom();
|
||||
return result;
|
||||
}
|
||||
|
||||
int FormRow::countAvailableWidth(int newWidth) const {
|
||||
return newWidth
|
||||
- st::passportRowPadding.left()
|
||||
- st::passportRowPadding.right()
|
||||
- (_ready
|
||||
? st::passportRowReadyIcon
|
||||
: st::passportRowEmptyIcon).width()
|
||||
- st::passportRowIconSkip;
|
||||
}
|
||||
|
||||
int FormRow::countAvailableWidth() const {
|
||||
return countAvailableWidth(width());
|
||||
}
|
||||
|
||||
void FormRow::paintEvent(QPaintEvent *e) {
|
||||
Painter p(this);
|
||||
|
||||
paintRipple(p, 0, 0);
|
||||
|
||||
const auto left = st::passportRowPadding.left();
|
||||
const auto availableWidth = countAvailableWidth();
|
||||
auto top = st::passportRowPadding.top();
|
||||
|
||||
const auto error = _errorAnimation.value(_error ? 1. : 0.);
|
||||
|
||||
p.setPen(st::passportRowTitleFg);
|
||||
_title.drawLeft(p, left, top, availableWidth, width());
|
||||
top += _titleHeight + st::passportRowSkip;
|
||||
|
||||
p.setPen(anim::pen(
|
||||
st::passportRowDescriptionFg,
|
||||
st::boxTextFgError,
|
||||
error));
|
||||
_description.drawLeft(p, left, top, availableWidth, width());
|
||||
top += _descriptionHeight + st::passportRowPadding.bottom();
|
||||
|
||||
const auto &icon = _ready
|
||||
? st::passportRowReadyIcon
|
||||
: st::passportRowEmptyIcon;
|
||||
if (error > 0. && !_ready) {
|
||||
icon.paint(
|
||||
p,
|
||||
width() - st::passportRowPadding.right() - icon.width(),
|
||||
(height() - icon.height()) / 2,
|
||||
width(),
|
||||
anim::color(st::menuIconFgOver, st::boxTextFgError, error));
|
||||
} else {
|
||||
icon.paint(
|
||||
p,
|
||||
width() - st::passportRowPadding.right() - icon.width(),
|
||||
(height() - icon.height()) / 2,
|
||||
width());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Passport::Ui
|
||||
48
Telegram/SourceFiles/passport/ui/passport_form_row.h
Normal file
48
Telegram/SourceFiles/passport/ui/passport_form_row.h
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/text/text.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
|
||||
namespace Passport::Ui {
|
||||
|
||||
using namespace ::Ui;
|
||||
|
||||
class FormRow : public RippleButton {
|
||||
public:
|
||||
explicit FormRow(QWidget *parent);
|
||||
|
||||
void updateContent(
|
||||
const QString &title,
|
||||
const QString &description,
|
||||
bool ready,
|
||||
bool error,
|
||||
anim::type animated);
|
||||
|
||||
protected:
|
||||
int resizeGetHeight(int newWidth) override;
|
||||
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
private:
|
||||
int countAvailableWidth() const;
|
||||
int countAvailableWidth(int newWidth) const;
|
||||
|
||||
Text::String _title;
|
||||
Text::String _description;
|
||||
int _titleHeight = 0;
|
||||
int _descriptionHeight = 0;
|
||||
bool _ready = false;
|
||||
bool _error = false;
|
||||
Animations::Simple _errorAnimation;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Passport::Ui
|
||||
Reference in New Issue
Block a user