tdesktop/Telegram/SourceFiles/intro/intro_qr.cpp

446 lines
12 KiB
C++

/*
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 "intro/intro_qr.h"
#include "boxes/abstract_box.h"
#include "intro/intro_phone.h"
#include "intro/intro_widget.h"
#include "intro/intro_password_check.h"
#include "lang/lang_keys.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/wrap/fade_wrap.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/effects/radial_animation.h"
#include "ui/text/text_utilities.h"
#include "ui/image/image_prepare.h"
#include "ui/painter.h"
#include "main/main_account.h"
#include "ui/boxes/confirm_box.h"
#include "core/application.h"
#include "core/core_cloud_password.h"
#include "core/update_checker.h"
#include "base/unixtime.h"
#include "qr/qr_generate.h"
#include "styles/style_intro.h"
namespace Intro {
namespace details {
namespace {
[[nodiscard]] QImage TelegramQrExact(const Qr::Data &data, int pixel) {
return Qr::Generate(data, pixel, Qt::black);
}
[[nodiscard]] QImage TelegramQr(const Qr::Data &data, int pixel, int max = 0) {
Expects(data.size > 0);
if (max > 0 && data.size * pixel > max) {
pixel = std::max(max / data.size, 1);
}
const auto qr = TelegramQrExact(data, pixel * style::DevicePixelRatio());
auto result = QImage(qr.size(), QImage::Format_ARGB32_Premultiplied);
result.fill(Qt::white);
{
auto p = QPainter(&result);
p.drawImage(QRect(QPoint(), qr.size()), qr);
}
return result;
}
[[nodiscard]] QColor QrActiveColor() {
return QColor(0x40, 0xA7, 0xE3); // Default windowBgActive.
}
[[nodiscard]] not_null<Ui::RpWidget*> PrepareQrWidget(
not_null<QWidget*> parent,
rpl::producer<QByteArray> codes) {
struct State {
explicit State(Fn<void()> callback)
: waiting(callback, st::defaultInfiniteRadialAnimation) {
}
QImage previous;
QImage qr;
QImage center;
Ui::Animations::Simple shown;
Ui::InfiniteRadialAnimation waiting;
};
auto qrs = std::move(
codes
) | rpl::map([](const QByteArray &code) {
return Qr::Encode(code, Qr::Redundancy::Quartile);
});
auto palettes = rpl::single(rpl::empty) | rpl::then(
style::PaletteChanged()
);
auto result = Ui::CreateChild<Ui::RpWidget>(parent.get());
const auto state = result->lifetime().make_state<State>(
[=] { result->update(); });
state->waiting.start();
const auto size = st::introQrMaxSize + 2 * st::introQrBackgroundSkip;
result->resize(size, size);
rpl::combine(
std::move(qrs),
rpl::duplicate(palettes)
) | rpl::map([](const Qr::Data &code, const auto &) {
return TelegramQr(code, st::introQrPixel, st::introQrMaxSize);
}) | rpl::start_with_next([=](QImage &&image) {
state->previous = std::move(state->qr);
state->qr = std::move(image);
state->waiting.stop();
state->shown.stop();
state->shown.start(
[=] { result->update(); },
0.,
1.,
st::fadeWrapDuration);
}, result->lifetime());
std::move(
palettes
) | rpl::map([] {
return TelegramLogoImage();
}) | rpl::start_with_next([=](QImage &&image) {
state->center = std::move(image);
}, result->lifetime());
result->paintRequest(
) | rpl::start_with_next([=](QRect clip) {
auto p = QPainter(result);
const auto has = !state->qr.isNull();
const auto shown = has ? state->shown.value(1.) : 0.;
const auto usualSize = 41;
const auto pixel = std::clamp(
st::introQrMaxSize / usualSize,
1,
st::introQrPixel);
const auto size = has
? (state->qr.size() / style::DevicePixelRatio())
: QSize(usualSize * pixel, usualSize * pixel);
const auto qr = QRect(
(result->width() - size.width()) / 2,
(result->height() - size.height()) / 2,
size.width(),
size.height());
const auto radius = st::introQrBackgroundRadius;
const auto skip = st::introQrBackgroundSkip;
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(Qt::white);
p.drawRoundedRect(
qr.marginsAdded({ skip, skip, skip, skip }),
radius,
radius);
if (!state->qr.isNull()) {
if (shown == 1.) {
state->previous = QImage();
} else if (!state->previous.isNull()) {
p.drawImage(qr, state->previous);
}
p.setOpacity(shown);
p.drawImage(qr, state->qr);
p.setOpacity(1.);
}
const auto rect = QRect(
(result->width() - st::introQrCenterSize) / 2,
(result->height() - st::introQrCenterSize) / 2,
st::introQrCenterSize,
st::introQrCenterSize);
p.drawImage(rect, state->center);
if (!anim::Disabled() && state->waiting.animating()) {
auto hq = PainterHighQualityEnabler(p);
const auto line = st::radialLine;
const auto radial = state->waiting.computeState();
auto pen = QPen(QrActiveColor());
pen.setWidth(line);
pen.setCapStyle(Qt::RoundCap);
p.setOpacity(radial.shown * (1. - shown));
p.setPen(pen);
p.drawArc(
rect.marginsAdded({ line, line, line, line }),
radial.arcFrom,
radial.arcLength);
p.setOpacity(1.);
}
}, result->lifetime());
return result;
}
} // namespace
QrWidget::QrWidget(
QWidget *parent,
not_null<Main::Account*> account,
not_null<Data*> data)
: Step(parent, account, data)
, _refreshTimer([=] { refreshCode(); }) {
setTitleText(rpl::single(QString()));
setDescriptionText(rpl::single(QString()));
setErrorCentered(true);
cancelNearestDcRequest();
account->mtpUpdates(
) | rpl::start_with_next([=](const MTPUpdates &updates) {
checkForTokenUpdate(updates);
}, lifetime());
setupControls();
account->mtp().mainDcIdValue(
) | rpl::start_with_next([=] {
api().request(base::take(_requestId)).cancel();
refreshCode();
}, lifetime());
}
int QrWidget::errorTop() const {
return contentTop() + st::introQrErrorTop;
}
void QrWidget::checkForTokenUpdate(const MTPUpdates &updates) {
updates.match([&](const MTPDupdateShort &data) {
checkForTokenUpdate(data.vupdate());
}, [&](const MTPDupdates &data) {
for (const auto &update : data.vupdates().v) {
checkForTokenUpdate(update);
}
}, [&](const MTPDupdatesCombined &data) {
for (const auto &update : data.vupdates().v) {
checkForTokenUpdate(update);
}
}, [](const auto &) {});
}
void QrWidget::checkForTokenUpdate(const MTPUpdate &update) {
update.match([&](const MTPDupdateLoginToken &data) {
if (_requestId) {
_forceRefresh = true;
} else {
_refreshTimer.cancel();
refreshCode();
}
}, [](const auto &) {});
}
void QrWidget::submit() {
goReplace<PhoneWidget>(Animate::Forward);
}
rpl::producer<QString> QrWidget::nextButtonText() const {
return rpl::single(QString());
}
void QrWidget::setupControls() {
const auto code = PrepareQrWidget(this, _qrCodes.events());
rpl::combine(
sizeValue(),
code->widthValue()
) | rpl::start_with_next([=](QSize size, int codeWidth) {
code->moveToLeft(
(size.width() - codeWidth) / 2,
contentTop() + st::introQrTop);
}, code->lifetime());
const auto title = Ui::CreateChild<Ui::FlatLabel>(
this,
tr::lng_intro_qr_title(),
st::introQrTitle);
rpl::combine(
sizeValue(),
title->widthValue()
) | rpl::start_with_next([=](QSize size, int titleWidth) {
title->resizeToWidth(st::introQrTitleWidth);
const auto oneLine = st::introQrTitle.style.font->height;
const auto topDelta = (title->height() - oneLine);
title->moveToLeft(
(size.width() - title->width()) / 2,
contentTop() + st::introQrTitleTop - topDelta);
}, title->lifetime());
const auto steps = Ui::CreateChild<Ui::VerticalLayout>(this);
const auto texts = {
tr::lng_intro_qr_step1,
tr::lng_intro_qr_step2,
tr::lng_intro_qr_step3,
};
auto index = 0;
for (const auto &text : texts) {
const auto label = steps->add(
object_ptr<Ui::FlatLabel>(
steps,
text(Ui::Text::RichLangValue),
st::introQrStep),
st::introQrStepMargins);
const auto number = Ui::CreateChild<Ui::FlatLabel>(
steps,
rpl::single(Ui::Text::Semibold(QString::number(++index) + ".")),
st::defaultFlatLabel);
rpl::combine(
number->widthValue(),
label->positionValue()
) | rpl::start_with_next([=](int width, QPoint position) {
number->moveToLeft(
position.x() - width - st::normalFont->spacew,
position.y());
}, number->lifetime());
}
steps->resizeToWidth(st::introQrLabelsWidth);
rpl::combine(
sizeValue(),
steps->widthValue()
) | rpl::start_with_next([=](QSize size, int stepsWidth) {
steps->moveToLeft(
(size.width() - stepsWidth) / 2,
contentTop() + st::introQrStepsTop);
}, steps->lifetime());
const auto skip = Ui::CreateChild<Ui::LinkButton>(
this,
tr::lng_intro_qr_skip(tr::now));
rpl::combine(
sizeValue(),
skip->widthValue()
) | rpl::start_with_next([=](QSize size, int skipWidth) {
skip->moveToLeft(
(size.width() - skipWidth) / 2,
contentTop() + st::introQrSkipTop);
}, skip->lifetime());
skip->setClickedCallback([=] { submit(); });
}
void QrWidget::refreshCode() {
if (_requestId) {
return;
}
_requestId = api().request(MTPauth_ExportLoginToken(
MTP_int(ApiId),
MTP_string(ApiHash),
MTP_vector<MTPlong>(0)
)).done([=](const MTPauth_LoginToken &result) {
handleTokenResult(result);
}).fail([=](const MTP::Error &error) {
showTokenError(error);
}).send();
}
void QrWidget::handleTokenResult(const MTPauth_LoginToken &result) {
result.match([&](const MTPDauth_loginToken &data) {
_requestId = 0;
showToken(data.vtoken().v);
if (base::take(_forceRefresh)) {
refreshCode();
} else {
const auto left = data.vexpires().v - base::unixtime::now();
_refreshTimer.callOnce(std::max(left, 1) * crl::time(1000));
}
}, [&](const MTPDauth_loginTokenMigrateTo &data) {
importTo(data.vdc_id().v, data.vtoken().v);
}, [&](const MTPDauth_loginTokenSuccess &data) {
done(data.vauthorization());
});
}
void QrWidget::showTokenError(const MTP::Error &error) {
_requestId = 0;
if (error.type() == u"SESSION_PASSWORD_NEEDED"_q) {
sendCheckPasswordRequest();
} else if (base::take(_forceRefresh)) {
refreshCode();
} else {
showError(rpl::single(error.type()));
}
}
void QrWidget::showToken(const QByteArray &token) {
const auto encoded = token.toBase64(QByteArray::Base64UrlEncoding);
_qrCodes.fire_copy("tg://login?token=" + encoded);
}
void QrWidget::importTo(MTP::DcId dcId, const QByteArray &token) {
Expects(_requestId != 0);
api().instance().setMainDcId(dcId);
_requestId = api().request(MTPauth_ImportLoginToken(
MTP_bytes(token)
)).done([=](const MTPauth_LoginToken &result) {
handleTokenResult(result);
}).fail([=](const MTP::Error &error) {
showTokenError(error);
}).toDC(dcId).send();
}
void QrWidget::done(const MTPauth_Authorization &authorization) {
finish(authorization);
}
void QrWidget::sendCheckPasswordRequest() {
_requestId = api().request(MTPaccount_GetPassword(
)).done([=](const MTPaccount_Password &result) {
result.match([&](const MTPDaccount_password &data) {
getData()->pwdState = Core::ParseCloudPasswordState(data);
if (!data.vcurrent_algo() || !data.vsrp_id() || !data.vsrp_B()) {
LOG(("API Error: No current password received on login."));
goReplace<QrWidget>(Animate::Forward);
return;
} else if (!getData()->pwdState.hasPassword) {
const auto callback = [=](Fn<void()> &&close) {
Core::UpdateApplication();
close();
};
Ui::show(Ui::MakeConfirmBox({
.text = tr::lng_passport_app_out_of_date(),
.confirmed = callback,
.confirmText = tr::lng_menu_update(),
}));
return;
}
goReplace<PasswordCheckWidget>(Animate::Forward);
});
}).fail([=](const MTP::Error &error) {
showTokenError(error);
}).send();
}
void QrWidget::activate() {
Step::activate();
showChildren();
}
void QrWidget::finished() {
Step::finished();
_refreshTimer.cancel();
apiClear();
cancelled();
}
void QrWidget::cancelled() {
api().request(base::take(_requestId)).cancel();
}
QImage TelegramLogoImage() {
const auto size = QSize(st::introQrCenterSize, st::introQrCenterSize);
auto result = QImage(
size * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
result.fill(Qt::transparent);
result.setDevicePixelRatio(style::DevicePixelRatio());
{
auto p = QPainter(&result);
auto hq = PainterHighQualityEnabler(p);
p.setBrush(QrActiveColor());
p.setPen(Qt::NoPen);
p.drawEllipse(QRect(QPoint(), size));
st::introQrPlane.paintInCenter(p, QRect(QPoint(), size));
}
return result;
}
} // namespace details
} // namespace Intro