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

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

View 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);

View 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

View 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

View 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 &&params,
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 &&params) {
return EncryptOrDecrypt(decrypted, std::move(params), AES_ENCRYPT);
}
bytes::vector Decrypt(
bytes::const_span encrypted,
AesParams &&params) {
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

View 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

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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

View 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

View File

@@ -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

View 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

View 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

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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