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,195 @@
/*
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";
countryRipple: defaultRippleAnimation;
introCoverHeight: 208px;
introCoverMaxWidth: 880px;
introCoverIconsMinSkip: 120px;
introCoverLeft: icon {{ "intro_left", introCoverIconsFg }};
introCoverRight: icon {{ "intro_right", introCoverIconsFg }};
introCoverIcon: icon {
{ "intro_plane_trace", introCoverPlaneTrace },
{ "intro_plane_inner", introCoverPlaneInner },
{ "intro_plane_outer", introCoverPlaneOuter },
{ "intro_plane_top", introCoverPlaneTop },
};
introCoverIconLeft: 50px;
introCoverIconTop: 46px;
introSettingsSkip: 10px;
introPhotoTop: 10px;
introCoverTitle: FlatLabel(defaultFlatLabel) {
textFg: introTitleFg;
align: align(center);
style: TextStyle(defaultTextStyle) {
font: font(22px semibold);
}
}
introCoverTitleTop: 136px;
introCoverDescription: FlatLabel(defaultFlatLabel) {
textFg: introDescriptionFg;
align: align(center);
style: TextStyle(defaultTextStyle) {
font: font(15px);
lineHeight: 24px;
}
}
introCoverDescriptionTop: 174px;
introTitle: FlatLabel(defaultFlatLabel) {
textFg: introTitleFg;
style: TextStyle(defaultTextStyle) {
font: font(17px semibold);
}
}
introTitleTop: 1px;
introDescription: FlatLabel(defaultFlatLabel) {
minWidth: 300px;
textFg: introDescriptionFg;
style: TextStyle(defaultTextStyle) {
lineHeight: 20px;
}
}
introDescriptionTop: 34px;
introLink: defaultLinkButton;
introPlaneWidth: 48px;
introPlaneHeight: 38px;
introHeight: 406px;
introStepTopMin: 76px;
introStepWidth: 380px;
introNextTop: 266px;
introNextSlide: 200px;
introStepHeight: 384px;
introContentTopAdd: 30px;
introStepHeightFull: 590px;
introSlideDuration: 200;
introCoverDuration: 200;
introNextButton: RoundButton(defaultActiveButton) {
width: 300px;
height: 42px;
radius: 6px;
textTop: 11px;
style: TextStyle(semiboldTextStyle) {
font: font(boxFontSize semibold);
}
}
introFragmentIcon: icon{{ "fragment", activeButtonFg }};
introFragmentIconOver: icon{{ "fragment", activeButtonFgOver }};
introFragmentButton: RoundButton(introNextButton) {
icon: introFragmentIcon;
iconOver: introFragmentIconOver;
iconPosition: point(-10px, 9px);
}
introStepFieldTop: 96px;
introPhoneTop: 6px;
introLinkTop: 24px;
introCountry: InputField(defaultInputField) {
textMargins: margins(3px, 27px, 3px, 6px);
style: TextStyle(defaultTextStyle) {
font: font(16px);
}
width: 300px;
heightMin: 61px;
}
introCountryCode: InputField(introCountry) {
width: 64px;
textAlign: align(top);
}
introPhone: InputField(introCountry) {
textMargins: margins(12px, 27px, 12px, 6px);
width: 225px;
}
introQrLoginLinkTop: 368px;
introCode: introCountry;
introName: introCountry;
introPassword: introCountry;
introPasswordTop: 74px;
introPasswordHintTop: 151px;
introCodeDigitFont: font(20px);
introCodeDigitHeight: 50px;
introCodeDigitBorderWidth: 4px;
introCodeDigitSkip: 10px;
introPasswordHint: FlatLabel(introDescription) {
textFg: windowFg;
}
introResetButton: RoundButton(defaultLightButton) {
textFg: attentionButtonFg;
textFgOver: attentionButtonFgOver;
textBgOver: attentionButtonBgOver;
ripple: RippleAnimation(defaultRippleAnimation) {
color: attentionButtonBgRipple;
}
}
introResetBottom: 20px;
introTermsLabel: FlatLabel(defaultFlatLabel) {
align: align(top);
}
introTermsBottom: 20px;
introCountryIcon: icon {{ "intro_country_dropdown", menuIconFg }};
introCountryIconPosition: point(8px, 37px);
introErrorTop: 235px;
introErrorBelowLinkTop: 220px;
introError: FlatLabel(introDescription) {
}
introErrorCentered: FlatLabel(introError) {
align: align(center);
}
introBackButton: IconButton(defaultIconButton) {
width: 56px;
height: 56px;
icon: backButtonIcon;
iconOver: backButtonIconOver;
rippleAreaPosition: point(8px, 8px);
rippleAreaSize: 40px;
ripple: defaultRippleAnimationBgOver;
}
introQrTop: -18px;
introQrPixel: 50px; // large enough
introQrMaxSize: 180px;
introQrBackgroundSkip: 12px;
introQrBackgroundRadius: 8px;
introQrLabelsWidth: 292px;
introQrTitleWidth: 320px;
introQrTitle: FlatLabel(defaultFlatLabel) {
textFg: introTitleFg;
align: align(top);
minWidth: introQrTitleWidth;
style: TextStyle(defaultTextStyle) {
font: font(20px semibold);
}
}
introQrErrorTop: 336px;
introQrTitleTop: 196px;
introQrStep: FlatLabel(defaultFlatLabel) {
minWidth: 200px;
}
introQrStepsTop: 232px;
introQrStepMargins: margins(20px, 8px, 0px, 0px);
introQrSkipTop: 360px;
introQrCenterSize: 44px;
introQrPlane: icon {{ "intro_qr_plane", activeButtonFg }};

View File

@@ -0,0 +1,500 @@
/*
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_code.h"
#include "lang/lang_keys.h"
#include "intro/intro_code_input.h"
#include "intro/intro_signup.h"
#include "intro/intro_password_check.h"
#include "boxes/abstract_box.h"
#include "core/file_utilities.h"
#include "core/update_checker.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/fields/masked_input_field.h"
#include "ui/text/format_values.h" // Ui::FormatPhone
#include "ui/text/text_utilities.h"
#include "ui/boxes/confirm_box.h"
#include "main/main_account.h"
#include "mtproto/mtp_instance.h"
#include "styles/style_intro.h"
namespace Intro {
namespace details {
CodeWidget::CodeWidget(
QWidget *parent,
not_null<Main::Account*> account,
not_null<Data*> data)
: Step(parent, account, data)
, _noTelegramCode(this, tr::lng_code_no_telegram(tr::now), st::introLink)
, _code(this)
, _callTimer([=] { sendCall(); })
, _callStatus(getData()->callStatus)
, _callTimeout(getData()->callTimeout)
, _callLabel(this, st::introDescription)
, _checkRequestTimer([=] { checkRequest(); }) {
Lang::Updated(
) | rpl::on_next([=] {
refreshLang();
}, lifetime());
_noTelegramCode->addClickHandler([=] { noTelegramCode(); });
_code->setDigitsCountMax(getData()->codeLength);
updateDescText();
setTitleText(_isFragment.value(
) | rpl::map([=](bool isFragment) {
return !isFragment
? rpl::single(Ui::FormatPhone(getData()->phone))
: tr::lng_intro_fragment_title();
}) | rpl::flatten_latest());
account->setHandleLoginCode([=](const QString &code) {
_code->setCode(code);
_code->requestCode();
});
_code->codeCollected(
) | rpl::on_next([=](const QString &code) {
hideError();
submitCode(code);
}, lifetime());
}
void CodeWidget::refreshLang() {
if (_noTelegramCode) {
_noTelegramCode->setText(tr::lng_code_no_telegram(tr::now));
}
updateDescText();
updateControlsGeometry();
}
int CodeWidget::errorTop() const {
return contentTop() + st::introErrorBelowLinkTop;
}
void CodeWidget::updateDescText() {
const auto byTelegram = getData()->codeByTelegram;
const auto isFragment = !getData()->codeByFragmentUrl.isEmpty();
_isFragment = isFragment;
const auto emailPattern = !getData()->emailPatternSetup.isEmpty()
? getData()->emailPatternSetup
: getData()->emailPatternLogin;
setDescriptionText(!emailPattern.isEmpty()
? tr::lng_intro_email_confirm_subtitle(
lt_email,
rpl::single(Ui::Text::WrapEmailPattern(emailPattern)),
tr::marked)
: isFragment
? tr::lng_intro_fragment_about(
lt_phone_number,
rpl::single(
TextWithEntities::Simple(Ui::FormatPhone(getData()->phone))),
tr::rich)
: (byTelegram ? tr::lng_code_from_telegram : tr::lng_code_desc)(
tr::rich));
if (getData()->codeByTelegram) {
_noTelegramCode->show();
_callTimer.cancel();
} else {
_noTelegramCode->hide();
_callStatus = getData()->callStatus;
_callTimeout = getData()->callTimeout;
if (_callStatus == CallStatus::Waiting && !_callTimer.isActive()) {
_callTimer.callEach(1000);
}
}
updateCallText();
}
void CodeWidget::updateCallText() {
auto text = ([this]() -> QString {
if (getData()->codeByTelegram) {
return QString();
}
switch (_callStatus) {
case CallStatus::Waiting: {
if (_callTimeout >= 3600) {
return tr::lng_code_call(
tr::now,
lt_minutes,
(u"%1:%2"_q
).arg(_callTimeout / 3600
).arg((_callTimeout / 60) % 60, 2, 10, QChar('0')),
lt_seconds,
u"%1"_q.arg(_callTimeout % 60, 2, 10, QChar('0')));
} else {
return tr::lng_code_call(
tr::now,
lt_minutes,
QString::number(_callTimeout / 60),
lt_seconds,
u"%1"_q.arg(_callTimeout % 60, 2, 10, QChar('0')));
}
} break;
case CallStatus::Calling:
return tr::lng_code_calling(tr::now);
case CallStatus::Called:
return tr::lng_code_called(tr::now);
}
return QString();
})();
_callLabel->setText(text);
_callLabel->setVisible(!text.isEmpty() && !animating());
}
void CodeWidget::resizeEvent(QResizeEvent *e) {
Step::resizeEvent(e);
updateControlsGeometry();
}
void CodeWidget::updateControlsGeometry() {
_code->moveToLeft(
contentLeft() - st::shakeShift - st::lineWidth,
contentTop() + st::introStepFieldTop + st::introPhoneTop * 3);
auto linkTop = _code->y() + _code->height() + st::introLinkTop;
_noTelegramCode->moveToLeft(contentLeft() + st::buttonRadius, linkTop);
_callLabel->moveToLeft(contentLeft() + st::buttonRadius, linkTop);
}
void CodeWidget::showCodeError(rpl::producer<QString> text) {
_code->showError();
showError(std::move(text));
}
void CodeWidget::setInnerFocus() {
_code->setFocus();
}
void CodeWidget::activate() {
Step::activate();
_code->show();
if (getData()->codeByTelegram) {
_noTelegramCode->show();
} else {
_callLabel->show();
}
setInnerFocus();
}
void CodeWidget::finished() {
Step::finished();
account().setHandleLoginCode(nullptr);
_checkRequestTimer.cancel();
_callTimer.cancel();
apiClear();
cancelled();
_sentCode.clear();
_code->clear();
}
void CodeWidget::cancelled() {
api().request(base::take(_sentRequest)).cancel();
api().request(base::take(_callRequestId)).cancel();
api().request(MTPauth_CancelCode(
MTP_string(getData()->phone),
MTP_bytes(getData()->phoneHash)
)).send();
}
void CodeWidget::stopCheck() {
_checkRequestTimer.cancel();
}
void CodeWidget::checkRequest() {
auto status = api().instance().state(_sentRequest);
if (status < 0) {
auto leftms = -status;
if (leftms >= 1000) {
if (_sentRequest) {
api().request(base::take(_sentRequest)).cancel();
_sentCode.clear();
}
}
}
if (!_sentRequest && status == MTP::RequestSent) {
stopCheck();
}
}
void CodeWidget::codeSubmitDone(const MTPauth_Authorization &result) {
stopCheck();
_code->setEnabled(true);
_sentRequest = 0;
finish(result);
}
void CodeWidget::emailVerifyDone(const MTPaccount_EmailVerified &result) {
stopCheck();
_sentRequest = 0;
result.match([&](const MTPDaccount_emailVerified &data) {
_code->setEnabled(true);
showCodeError(rpl::single(
u"Unexpected type of response: emailVerifiedLogin"_q));
}, [&](const MTPDaccount_emailVerifiedLogin &data) {
getData()->emailPatternSetup.clear();
data.vsent_code().match([&](const MTPDauth_sentCode &sentData) {
fillSentCodeData(sentData);
getData()->phoneHash = qba(sentData.vphone_code_hash());
const auto next = sentData.vnext_type();
if (next && next->type() == mtpc_auth_codeTypeCall) {
getData()->callStatus = CallStatus::Waiting;
getData()->callTimeout = sentData.vtimeout().value_or(60);
} else {
getData()->callStatus = CallStatus::Disabled;
getData()->callTimeout = 0;
}
goReplace<CodeWidget>(Animate::Forward);
}, [&](const MTPDauth_sentCodeSuccess &sentData) {
finish(sentData.vauthorization());
}, [](const MTPDauth_sentCodePaymentRequired &) {
LOG(("API Error: Unexpected auth.sentCodePaymentRequired "
"(CodeWidget::emailVerifyDone)."));
});
});
}
void CodeWidget::codeSubmitFail(const MTP::Error &error) {
if (MTP::IsFloodError(error)) {
stopCheck();
_code->setEnabled(true);
_code->setFocus();
_sentRequest = 0;
showCodeError(tr::lng_flood_error());
return;
}
stopCheck();
_code->setEnabled(true);
_code->setFocus();
_sentRequest = 0;
auto &err = error.type();
if (err == u"PHONE_NUMBER_INVALID"_q
|| err == u"PHONE_CODE_EXPIRED"_q
|| err == u"PHONE_NUMBER_BANNED"_q) { // show error
goBack();
} else if (err == u"PHONE_CODE_EMPTY"_q || err == u"PHONE_CODE_INVALID"_q) {
showCodeError(tr::lng_bad_code());
} else if (err == u"SESSION_PASSWORD_NEEDED"_q) {
_checkRequestTimer.callEach(1000);
_sentRequest = api().request(MTPaccount_GetPassword(
)).done([=](const MTPaccount_Password &result) {
gotPassword(result);
}).fail([=](const MTP::Error &error) {
codeSubmitFail(error);
}).handleFloodErrors().send();
} else if (Logs::DebugEnabled()) { // internal server error
showCodeError(rpl::single(err + ": " + error.description()));
} else {
showCodeError(rpl::single(Lang::Hard::ServerError()));
}
}
void CodeWidget::sendCall() {
if (_callStatus == CallStatus::Waiting) {
if (--_callTimeout <= 0) {
_callStatus = CallStatus::Calling;
_callTimer.cancel();
_callRequestId = api().request(MTPauth_ResendCode(
MTP_flags(0),
MTP_string(getData()->phone),
MTP_bytes(getData()->phoneHash),
MTPstring() // reason
)).done([=](const MTPauth_SentCode &result) {
callDone(result);
}).send();
} else {
getData()->callStatus = _callStatus;
getData()->callTimeout = _callTimeout;
}
updateCallText();
}
}
void CodeWidget::callDone(const MTPauth_SentCode &result) {
result.match([&](const MTPDauth_sentCode &data) {
fillSentCodeData(data);
_code->setDigitsCountMax(getData()->codeLength);
if (_callStatus == CallStatus::Calling) {
_callStatus = CallStatus::Called;
getData()->callStatus = _callStatus;
getData()->callTimeout = _callTimeout;
updateCallText();
}
}, [&](const MTPDauth_sentCodeSuccess &data) {
finish(data.vauthorization());
}, [](const MTPDauth_sentCodePaymentRequired &) {
LOG(("API Error: Unexpected auth.sentCodePaymentRequired "
"(CodeWidget::callDone)."));
});
}
void CodeWidget::gotPassword(const MTPaccount_Password &result) {
Expects(result.type() == mtpc_account_password);
stopCheck();
_sentRequest = 0;
const auto &d = result.c_account_password();
getData()->pwdState = Core::ParseCloudPasswordState(d);
if (!d.vcurrent_algo() || !d.vsrp_id() || !d.vsrp_B()) {
LOG(("API Error: No current password received on login."));
_code->setFocus();
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);
}
void CodeWidget::submit() {
if (getData()->codeByFragmentUrl.isEmpty()) {
_code->requestCode();
} else {
File::OpenUrl(getData()->codeByFragmentUrl);
}
}
void CodeWidget::submitCode(const QString &text) {
if (_sentRequest
|| _sentCode == text
|| text.size() != getData()->codeLength) {
return;
}
hideError();
_checkRequestTimer.callEach(1000);
_sentCode = text;
_code->setEnabled(false);
getData()->pwdState = Core::CloudPasswordState();
if (isEmailVerification()) {
_sentRequest = api().request(MTPaccount_VerifyEmail(
MTP_emailVerifyPurposeLoginSetup(
MTP_string(getData()->phone),
MTP_bytes(getData()->phoneHash)),
MTP_emailVerificationCode(MTP_string(_sentCode))
)).done([=](const MTPaccount_EmailVerified &result) {
emailVerifyDone(result);
}).fail([=](const MTP::Error &error) {
codeSubmitFail(error);
}).handleFloodErrors().send();
} else {
const auto isEmailLogin = !getData()->emailPatternLogin.isEmpty();
_sentRequest = api().request(MTPauth_SignIn(
MTP_flags(isEmailLogin
? MTPauth_SignIn::Flag::f_email_verification
: MTPauth_SignIn::Flag::f_phone_code),
MTP_string(getData()->phone),
MTP_bytes(getData()->phoneHash),
MTP_string(_sentCode),
MTP_emailVerificationCode(
MTP_string(isEmailLogin ? _sentCode : QString()))
)).done([=](const MTPauth_Authorization &result) {
codeSubmitDone(result);
}).fail([=](const MTP::Error &error) {
codeSubmitFail(error);
}).handleFloodErrors().send();
}
}
bool CodeWidget::isEmailVerification() const {
return !getData()->emailPatternSetup.isEmpty();
}
rpl::producer<QString> CodeWidget::nextButtonText() const {
return _isFragment.value(
) | rpl::map([=](bool isFragment) {
return isFragment
? tr::lng_intro_fragment_button()
: Step::nextButtonText();
}) | rpl::flatten_latest();
}
rpl::producer<const style::RoundButton*> CodeWidget::nextButtonStyle() const {
return _isFragment.value(
) | rpl::map([](bool isFragment) {
return isFragment ? &st::introFragmentButton : nullptr;
});
}
void CodeWidget::noTelegramCode() {
if (_noTelegramCodeRequestId) {
return;
}
_noTelegramCodeRequestId = api().request(MTPauth_ResendCode(
MTP_flags(0),
MTP_string(getData()->phone),
MTP_bytes(getData()->phoneHash),
MTPstring() // reason
)).done([=](const MTPauth_SentCode &result) {
noTelegramCodeDone(result);
}).fail([=](const MTP::Error &error) {
noTelegramCodeFail(error);
}).handleFloodErrors().send();
}
void CodeWidget::noTelegramCodeDone(const MTPauth_SentCode &result) {
_noTelegramCodeRequestId = 0;
result.match([&](const MTPDauth_sentCode &data) {
const auto &d = result.c_auth_sentCode();
fillSentCodeData(data);
_code->setDigitsCountMax(getData()->codeLength);
const auto next = data.vnext_type();
if (next && next->type() == mtpc_auth_codeTypeCall) {
getData()->callStatus = CallStatus::Waiting;
getData()->callTimeout = d.vtimeout().value_or(60);
} else {
getData()->callStatus = CallStatus::Disabled;
getData()->callTimeout = 0;
}
getData()->codeByTelegram = false;
updateDescText();
}, [&](const MTPDauth_sentCodeSuccess &data) {
finish(data.vauthorization());
}, [](const MTPDauth_sentCodePaymentRequired &) {
LOG(("API Error: Unexpected auth.sentCodePaymentRequired "
"(CodeWidget::noTelegramCodeDone)."));
});
}
void CodeWidget::noTelegramCodeFail(const MTP::Error &error) {
if (MTP::IsFloodError(error)) {
_noTelegramCodeRequestId = 0;
showCodeError(tr::lng_flood_error());
return;
} else if (error.type() == u"SEND_CODE_UNAVAILABLE"_q) {
_noTelegramCodeRequestId = 0;
return;
}
_noTelegramCodeRequestId = 0;
if (Logs::DebugEnabled()) { // internal server error
showCodeError(rpl::single(error.type() + ": " + error.description()));
} else {
showCodeError(rpl::single(Lang::Hard::ServerError()));
}
}
} // namespace details
} // namespace Intro

View File

@@ -0,0 +1,98 @@
/*
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 "intro/intro_step.h"
#include "intro/intro_widget.h"
#include "ui/widgets/fields/masked_input_field.h"
#include "base/timer.h"
namespace Ui {
class RoundButton;
class LinkButton;
class FlatLabel;
class CodeInput;
} // namespace Ui
namespace Intro {
namespace details {
enum class CallStatus;
class CodeWidget final : public Step {
public:
CodeWidget(
QWidget *parent,
not_null<Main::Account*> account,
not_null<Data*> data);
bool hasBack() const override {
return true;
}
void setInnerFocus() override;
void activate() override;
void finished() override;
void cancelled() override;
void submit() override;
rpl::producer<QString> nextButtonText() const override;
rpl::producer<const style::RoundButton*> nextButtonStyle() const override;
void updateDescText();
protected:
void resizeEvent(QResizeEvent *e) override;
private:
void noTelegramCode();
void sendCall();
void checkRequest();
int errorTop() const override;
[[nodiscard]] bool isEmailVerification() const;
void updateCallText();
void refreshLang();
void updateControlsGeometry();
void codeSubmitDone(const MTPauth_Authorization &result);
void codeSubmitFail(const MTP::Error &error);
void emailVerifyDone(const MTPaccount_EmailVerified &result);
void showCodeError(rpl::producer<QString> text);
void callDone(const MTPauth_SentCode &result);
void gotPassword(const MTPaccount_Password &result);
void noTelegramCodeDone(const MTPauth_SentCode &result);
void noTelegramCodeFail(const MTP::Error &result);
void submitCode(const QString &text);
void stopCheck();
object_ptr<Ui::LinkButton> _noTelegramCode;
mtpRequestId _noTelegramCodeRequestId = 0;
object_ptr<Ui::CodeInput> _code;
QString _sentCode;
mtpRequestId _sentRequest = 0;
rpl::variable<bool> _isFragment = false;
base::Timer _callTimer;
CallStatus _callStatus = CallStatus();
int _callTimeout;
mtpRequestId _callRequestId = 0;
object_ptr<Ui::FlatLabel> _callLabel;
base::Timer _checkRequestTimer;
};
} // namespace details
} // namespace Intro

View File

@@ -0,0 +1,353 @@
/*
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_code_input.h"
#include "lang/lang_keys.h"
#include "ui/abstract_button.h"
#include "ui/effects/shake_animation.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/widgets/popup_menu.h"
#include "styles/style_basic.h"
#include "styles/style_intro.h"
#include "styles/style_layers.h" // boxRadius
#include <QtCore/QRegularExpression>
#include <QtGui/QClipboard>
#include <QtGui/QGuiApplication>
namespace Ui {
namespace {
constexpr auto kDigitNone = int(-1);
[[nodiscard]] int Circular(int left, int right) {
return ((left % right) + right) % right;
}
class Shaker final {
public:
explicit Shaker(not_null<Ui::RpWidget*> widget);
void shake();
private:
const not_null<Ui::RpWidget*> _widget;
Ui::Animations::Simple _animation;
};
Shaker::Shaker(not_null<Ui::RpWidget*> widget)
: _widget(widget) {
}
void Shaker::shake() {
if (_animation.animating()) {
return;
}
_animation.start(DefaultShakeCallback([=, x = _widget->x()](int shift) {
_widget->moveToLeft(x + shift, _widget->y());
}), 0., 1., st::shakeDuration);
}
} // namespace
class CodeDigit final : public Ui::AbstractButton {
public:
explicit CodeDigit(not_null<Ui::RpWidget*> widget);
void setDigit(int digit);
[[nodiscard]] int digit() const;
void setBorderColor(const QBrush &brush);
void shake();
protected:
void paintEvent(QPaintEvent *e) override;
private:
Shaker _shaker;
Ui::Animations::Simple _animation;
int _dataDigit = kDigitNone;
int _viewDigit = kDigitNone;
QPen _borderPen;
};
CodeDigit::CodeDigit(not_null<Ui::RpWidget*> widget)
: Ui::AbstractButton(widget)
, _shaker(this) {
setBorderColor(st::windowBgRipple);
}
void CodeDigit::setDigit(int digit) {
if ((_dataDigit == digit) && _animation.animating()) {
return;
}
_dataDigit = digit;
if (_viewDigit != digit) {
_animation.stop();
if (digit == kDigitNone) {
_animation.start([=](float64 value) {
update();
if (!value) {
_viewDigit = digit;
}
}, 1., 0., st::universalDuration);
} else {
_viewDigit = digit;
_animation.start([=] { update(); }, 0, 1., st::universalDuration);
}
}
}
int CodeDigit::digit() const {
return _dataDigit;
}
void CodeDigit::setBorderColor(const QBrush &brush) {
_borderPen = QPen(brush, st::introCodeDigitBorderWidth);
update();
}
void CodeDigit::shake() {
_shaker.shake();
}
void CodeDigit::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
auto clipPath = QPainterPath();
clipPath.addRoundedRect(rect(), st::boxRadius, st::boxRadius);
p.setClipPath(clipPath);
p.fillRect(rect(), st::windowBgOver);
{
auto hq = PainterHighQualityEnabler(p);
p.strokePath(clipPath, _borderPen);
}
if (_viewDigit == kDigitNone) {
return;
}
const auto hiding = (_dataDigit == kDigitNone);
const auto progress = _animation.value(1.);
if (hiding) {
p.setOpacity(progress * progress);
const auto center = rect().center();
p.setTransform(QTransform()
.translate(center.x(), center.y())
.scale(progress, progress)
.translate(-center.x(), -center.y()));
} else {
p.setOpacity(progress);
constexpr auto kSlideDistanceRatio = 0.2;
const auto distance = rect().height() * kSlideDistanceRatio;
p.translate(0, (distance * (1. - progress)));
}
p.setFont(st::introCodeDigitFont);
p.setPen(st::windowFg);
p.drawText(rect(), QString::number(_viewDigit), style::al_center);
}
CodeInput::CodeInput(QWidget *parent)
: Ui::RpWidget(parent) {
setFocusPolicy(Qt::StrongFocus);
}
QString CodeInput::accessibilityName() {
return tr::lng_code_ph(tr::now);
}
QString CodeInput::accessibilityValue() const {
return collectDigits();
}
void CodeInput::setDigitsCountMax(int digitsCount) {
_digitsCountMax = digitsCount;
_digits.clear();
_currentIndex = 0;
constexpr auto kWidthRatio = 0.8;
const auto digitWidth = st::introCodeDigitHeight * kWidthRatio;
const auto padding = Margins(st::introCodeDigitSkip);
resize(
padding.left()
+ digitWidth * digitsCount
+ st::introCodeDigitSkip * (digitsCount - 1)
+ padding.right(),
st::introCodeDigitHeight);
setNaturalWidth(width());
for (auto i = 0; i < digitsCount; i++) {
const auto widget = Ui::CreateChild<CodeDigit>(this);
widget->setPointerCursor(false);
widget->setClickedCallback([=] { unfocusAll(_currentIndex = i); });
widget->resize(digitWidth, st::introCodeDigitHeight);
widget->moveToLeft(
padding.left() + (digitWidth + st::introCodeDigitSkip) * i,
0);
_digits.emplace_back(widget);
}
}
void CodeInput::setCode(QString code) {
using namespace TextUtilities;
code = code.remove(RegExpDigitsExclude()).mid(0, _digitsCountMax);
for (int i = 0; i < _digits.size(); i++) {
if (i >= code.size()) {
return;
}
_digits[i]->setDigit(code.at(i).digitValue());
}
}
void CodeInput::requestCode() {
const auto result = collectDigits();
if (result.size() == _digitsCountMax) {
_codeCollected.fire_copy(result);
} else {
findEmptyAndPerform([&](int i) { _digits[i]->shake(); });
}
}
rpl::producer<QString> CodeInput::codeCollected() const {
return _codeCollected.events();
}
void CodeInput::clear() {
for (const auto &digit : _digits) {
digit->setDigit(kDigitNone);
}
unfocusAll(_currentIndex = 0);
}
void CodeInput::showError() {
clear();
for (const auto &digit : _digits) {
digit->shake();
digit->setBorderColor(st::activeLineFgError);
}
}
void CodeInput::focusInEvent(QFocusEvent *e) {
unfocusAll(_currentIndex);
}
void CodeInput::focusOutEvent(QFocusEvent *e) {
unfocusAll(kDigitNone);
}
void CodeInput::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
p.fillRect(rect(), st::windowBg);
}
void CodeInput::keyPressEvent(QKeyEvent *e) {
const auto key = e->key();
if (key == Qt::Key_Down || key == Qt::Key_Right || key == Qt::Key_Space) {
_currentIndex = Circular(_currentIndex + 1, _digits.size());
unfocusAll(_currentIndex);
} else if (key == Qt::Key_Up || key == Qt::Key_Left) {
_currentIndex = Circular(_currentIndex - 1, _digits.size());
unfocusAll(_currentIndex);
} else if (key >= Qt::Key_0 && key <= Qt::Key_9) {
const auto index = int(key - Qt::Key_0);
_digits[_currentIndex]->setDigit(index);
_currentIndex = Circular(_currentIndex + 1, _digits.size());
if (!_currentIndex) {
const auto result = collectDigits();
if (result.size() == _digitsCountMax) {
_codeCollected.fire_copy(result);
_currentIndex = _digits.size() - 1;
} else {
findEmptyAndPerform([&](int i) { _currentIndex = i; });
}
}
unfocusAll(_currentIndex);
} else if (key == Qt::Key_Delete) {
_digits[_currentIndex]->setDigit(kDigitNone);
} else if (key == Qt::Key_Backspace) {
const auto wasDigit = _digits[_currentIndex]->digit();
_digits[_currentIndex]->setDigit(kDigitNone);
_currentIndex = std::clamp(_currentIndex - 1, 0, int(_digits.size()));
if (wasDigit == kDigitNone) {
_digits[_currentIndex]->setDigit(kDigitNone);
}
unfocusAll(_currentIndex);
} else if (key == Qt::Key_Enter || key == Qt::Key_Return) {
requestCode();
} else if (e == QKeySequence::Paste) {
insertCodeAndSubmit(QGuiApplication::clipboard()->text());
} else if (key >= Qt::Key_A && key <= Qt::Key_Z) {
_digits[_currentIndex]->shake();
} else if (key == Qt::Key_Home || key == Qt::Key_PageUp) {
unfocusAll(_currentIndex = 0);
} else if (key == Qt::Key_End || key == Qt::Key_PageDown) {
unfocusAll(_currentIndex = (_digits.size() - 1));
}
}
void CodeInput::contextMenuEvent(QContextMenuEvent *e) {
if (_menu) {
return;
}
_menu = base::make_unique_q<Ui::PopupMenu>(this, st::defaultPopupMenu);
_menu->addAction(tr::lng_mac_menu_paste(tr::now), [=] {
insertCodeAndSubmit(QGuiApplication::clipboard()->text());
})->setEnabled(!QGuiApplication::clipboard()->text().isEmpty());
_menu->popup(QCursor::pos());
}
void CodeInput::insertCodeAndSubmit(const QString &code) {
if (code.isEmpty()) {
return;
}
setCode(code);
_currentIndex = _digits.size() - 1;
findEmptyAndPerform([&](int i) { _currentIndex = i; });
unfocusAll(_currentIndex);
if ((_currentIndex == _digits.size() - 1)
&& _digits[_currentIndex]->digit() != kDigitNone) {
requestCode();
}
accessibilityValueChanged();
}
QString CodeInput::collectDigits() const {
auto result = QString();
for (const auto &digit : _digits) {
if (digit->digit() != kDigitNone) {
result += QString::number(digit->digit());
}
}
return result;
}
void CodeInput::unfocusAll(int except) {
for (auto i = 0; i < _digits.size(); i++) {
const auto focused = (i == except);
_digits[i]->setBorderColor(focused
? st::windowActiveTextFg
: st::windowBgRipple);
}
}
void CodeInput::findEmptyAndPerform(const Fn<void(int)> &callback) {
for (auto i = 0; i < _digits.size(); i++) {
if (_digits[i]->digit() == kDigitNone) {
callback(i);
break;
}
}
}
} // namespace Ui

View File

@@ -0,0 +1,61 @@
/*
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"
namespace Ui {
class CodeDigit;
class PopupMenu;
class CodeInput final : public Ui::RpWidget {
public:
CodeInput(QWidget *parent);
QAccessible::Role accessibilityRole() override {
return QAccessible::EditableText;
}
QString accessibilityName() override;
QString accessibilityValue() const override;
void setDigitsCountMax(int digitsCount);
void setCode(QString code);
void requestCode();
[[nodiscard]] rpl::producer<QString> codeCollected() const;
void clear();
void showError();
protected:
void focusInEvent(QFocusEvent *e) override;
void focusOutEvent(QFocusEvent *e) override;
void paintEvent(QPaintEvent *e) override;
void keyPressEvent(QKeyEvent *e) override;
void contextMenuEvent(QContextMenuEvent *e) override;
private:
[[nodiscard]] QString collectDigits() const;
void insertCodeAndSubmit(const QString &code);
void unfocusAll(int except);
void findEmptyAndPerform(const Fn<void(int)> &callback);
int _digitsCountMax = 0;
std::vector<not_null<CodeDigit*>> _digits;
int _currentIndex = 0;
base::unique_qptr<Ui::PopupMenu> _menu;
rpl::event_stream<QString> _codeCollected;
};
} // namespace Ui

View File

@@ -0,0 +1,175 @@
/*
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_email.h"
#include "intro/intro_code.h"
#include "intro/intro_code_input.h"
#include "lang/lang_keys.h"
#include "lottie/lottie_icon.h"
#include "settings/cloud_password/settings_cloud_password_common.h"
#include "settings/settings_common.h" // CreateLottieIcon.
#include "ui/rect.h"
#include "ui/vertical_list.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/labels.h"
#include "ui/wrap/vertical_layout.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "window/window_session_controller.h"
#include "styles/style_intro.h"
#include "styles/style_settings.h"
namespace Intro {
namespace details {
EmailWidget::EmailWidget(
QWidget *parent,
not_null<Main::Account*> account,
not_null<Data*> data)
: Step(parent, account, data)
, _inner(this) {
const auto content = _inner.get();
widthValue() | rpl::on_next([=](int w) {
content->resizeToWidth(st::introNextButton.width);
content->moveToLeft((w - content->width()) / 2, contentTop());
}, content->lifetime());
content->add(
object_ptr<Ui::FlatLabel>(
content,
tr::lng_intro_email_setup_title(),
st::introTitle),
style::margins(),
style::al_left);
Ui::AddSkip(content, st::lineWidth * 2);
content->add(
object_ptr<Ui::FlatLabel>(
content,
tr::lng_settings_cloud_login_email_about(),
st::introDescription),
style::margins(),
style::al_left);
{
const auto lottie = u"cloud_password/email"_q;
const auto size = st::settingsCloudPasswordIconSize / 3 * 2;
auto icon = Settings::CreateLottieIcon(
content,
{ .name = lottie, .sizeOverride = Size(size) },
style::margins());
content->add(std::move(icon.widget));
_showFinished.events(
) | rpl::on_next([animate = std::move(icon.animate)] {
animate(anim::repeat::once);
}, lifetime());
}
const auto newInput = Settings::CloudPassword::AddWrappedField(
content,
tr::lng_settings_cloud_login_email_placeholder(),
QString());
Ui::AddSkip(content);
const auto error = Settings::CloudPassword::AddError(content, nullptr);
newInput->changes() | rpl::on_next([=] {
error->hide();
}, newInput->lifetime());
newInput->submits() | rpl::on_next([=] {
submit();
}, newInput->lifetime());
newInput->setText(getData()->email);
if (newInput->hasText()) {
newInput->selectAll();
}
_setFocus.events() | rpl::on_next([=] {
newInput->setFocus();
}, newInput->lifetime());
_submitCallback = [=] {
const auto send = [=](const QString &email) {
getData()->email = email;
const auto done = [=](int length, const QString &pattern) {
_sentRequest = 0;
getData()->codeLength = length;
getData()->emailPatternSetup = pattern;
goNext<CodeWidget>();
};
const auto fail = [=](const QString &type) {
_sentRequest = 0;
newInput->setFocus();
newInput->showError();
newInput->selectAll();
error->show();
if (MTP::IsFloodError(type)) {
error->setText(tr::lng_flood_error(tr::now));
} else if (type == u"EMAIL_NOT_ALLOWED"_q) {
error->setText(
tr::lng_settings_error_email_not_alowed(tr::now));
} else if (type == u"EMAIL_INVALID"_q) {
error->setText(tr::lng_cloud_password_bad_email(tr::now));
} else if (type == u"EMAIL_HASH_EXPIRED"_q) {
// Show box?
error->setText(Lang::Hard::EmailConfirmationExpired());
} else {
error->setText(Lang::Hard::ServerError());
}
};
_sentRequest = api().request(MTPaccount_SendVerifyEmailCode(
MTP_emailVerifyPurposeLoginSetup(
MTP_string(getData()->phone),
MTP_bytes(getData()->phoneHash)),
MTP_string(email)
)).done([=](const MTPaccount_SentEmailCode &result) {
done(
result.data().vlength().v,
qs(result.data().vemail_pattern()));
}).fail([=](const MTP::Error &error) {
fail(error.type());
}).send();
};
const auto newText = newInput->getLastText();
if (newText.isEmpty()) {
newInput->setFocus();
newInput->showError();
} else {
send(newText);
}
};
}
void EmailWidget::submit() {
if (_submitCallback) {
_submitCallback();
}
}
void EmailWidget::setInnerFocus() {
_setFocus.fire({});
}
void EmailWidget::activate() {
Step::activate();
showChildren();
setInnerFocus();
_showFinished.fire({});
}
void EmailWidget::finished() {
Step::finished();
cancelled();
}
void EmailWidget::cancelled() {
api().request(base::take(_sentRequest)).cancel();
}
} // namespace details
} // namespace Intro

View File

@@ -0,0 +1,50 @@
/*
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 "intro/intro_step.h"
namespace Ui {
class VerticalLayout;
} // namespace Ui
namespace MTP {
class Sender;
} // namespace MTP
namespace Intro {
namespace details {
class EmailWidget final : public Step {
public:
EmailWidget(
QWidget *parent,
not_null<Main::Account*> account,
not_null<Data*> data);
void setInnerFocus() override;
void activate() override;
void finished() override;
void cancelled() override;
void submit() override;
bool hasBack() const override {
return true;
}
private:
object_ptr<Ui::VerticalLayout> _inner;
Fn<void()> _submitCallback = nullptr;
rpl::event_stream<> _showFinished;
rpl::event_stream<> _setFocus;
mtpRequestId _sentRequest = 0;
};
} // namespace details
} // namespace Intro

View File

@@ -0,0 +1,410 @@
/*
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_password_check.h"
#include "intro/intro_widget.h"
#include "core/core_cloud_password.h"
#include "ui/boxes/confirm_box.h"
#include "boxes/abstract_box.h"
#include "boxes/passcode_box.h"
#include "lang/lang_keys.h"
#include "intro/intro_signup.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/fields/password_input.h"
#include "main/main_account.h"
#include "base/random.h"
#include "styles/style_intro.h"
#include "styles/style_boxes.h"
namespace Intro {
namespace details {
PasswordCheckWidget::PasswordCheckWidget(
QWidget *parent,
not_null<Main::Account*> account,
not_null<Data*> data)
: Step(parent, account, data)
, _passwordState(getData()->pwdState)
, _pwdField(this, st::introPassword, tr::lng_signin_password())
, _pwdHint(this, st::introPasswordHint)
, _codeField(this, st::introPassword, tr::lng_signin_code())
, _toRecover(this, tr::lng_signin_recover(tr::now))
, _toPassword(this, tr::lng_signin_try_password(tr::now)) {
Expects(_passwordState.hasPassword);
Lang::Updated(
) | rpl::on_next([=] {
refreshLang();
}, lifetime());
_toRecover->addClickHandler([=] { toRecover(); });
_toPassword->addClickHandler([=] { toPassword(); });
connect(_pwdField, &Ui::PasswordInput::changed, [=] { hideError(); });
_codeField->changes(
) | rpl::on_next([=] {
hideError();
}, _codeField->lifetime());
setTitleText(tr::lng_signin_title());
updateDescriptionText();
if (_passwordState.hint.isEmpty()) {
_pwdHint->hide();
} else {
_pwdHint->setText(tr::lng_signin_hint(
tr::now,
lt_password_hint,
_passwordState.hint));
}
_codeField->hide();
_toPassword->hide();
setMouseTracking(true);
}
void PasswordCheckWidget::refreshLang() {
if (_toRecover) {
_toRecover->setText(tr::lng_signin_recover(tr::now));
}
if (_toPassword) {
_toPassword->setText(
tr::lng_signin_try_password(tr::now));
}
if (!_passwordState.hint.isEmpty()) {
_pwdHint->setText(tr::lng_signin_hint(
tr::now,
lt_password_hint,
_passwordState.hint));
}
updateControlsGeometry();
}
int PasswordCheckWidget::errorTop() const {
return contentTop() + st::introErrorBelowLinkTop;
}
void PasswordCheckWidget::resizeEvent(QResizeEvent *e) {
Step::resizeEvent(e);
updateControlsGeometry();
}
void PasswordCheckWidget::updateControlsGeometry() {
_pwdField->moveToLeft(contentLeft(), contentTop() + st::introPasswordTop);
_pwdHint->moveToLeft(contentLeft() + st::buttonRadius, contentTop() + st::introPasswordHintTop);
_codeField->moveToLeft(contentLeft(), contentTop() + st::introStepFieldTop);
auto linkTop = _codeField->y() + _codeField->height() + st::introLinkTop;
_toRecover->moveToLeft(contentLeft() + st::buttonRadius, linkTop);
_toPassword->moveToLeft(contentLeft() + st::buttonRadius, linkTop);
}
void PasswordCheckWidget::setInnerFocus() {
if (_pwdField->isHidden()) {
_codeField->setFocusFast();
} else {
_pwdField->setFocusFast();
}
}
void PasswordCheckWidget::activate() {
if (_pwdField->isHidden() && _codeField->isHidden()) {
Step::activate();
_pwdField->show();
_pwdHint->show();
_toRecover->show();
}
setInnerFocus();
}
void PasswordCheckWidget::cancelled() {
api().request(base::take(_sentRequest)).cancel();
}
void PasswordCheckWidget::pwdSubmitDone(
bool recover,
const MTPauth_Authorization &result) {
_sentRequest = 0;
if (recover) {
cSetPasswordRecovered(true);
}
finish(result);
}
void PasswordCheckWidget::pwdSubmitFail(const MTP::Error &error) {
if (MTP::IsFloodError(error)) {
_sentRequest = 0;
showError(tr::lng_flood_error());
_pwdField->showError();
return;
}
_sentRequest = 0;
const auto &type = error.type();
if (type == u"PASSWORD_HASH_INVALID"_q
|| type == u"SRP_PASSWORD_CHANGED"_q) {
showError(tr::lng_signin_bad_password());
_pwdField->selectAll();
_pwdField->showError();
} else if (type == u"PASSWORD_EMPTY"_q
|| type == u"AUTH_KEY_UNREGISTERED"_q) {
goBack();
} else if (type == u"SRP_ID_INVALID"_q) {
handleSrpIdInvalid();
} else {
if (Logs::DebugEnabled()) { // internal server error
showError(rpl::single(type + ": " + error.description()));
} else {
showError(rpl::single(Lang::Hard::ServerError()));
}
_pwdField->setFocus();
}
}
void PasswordCheckWidget::handleSrpIdInvalid() {
const auto now = crl::now();
if (_lastSrpIdInvalidTime > 0
&& now - _lastSrpIdInvalidTime < Core::kHandleSrpIdInvalidTimeout) {
_passwordState.mtp.request.id = 0;
showError(rpl::single(Lang::Hard::ServerError()));
} else {
_lastSrpIdInvalidTime = now;
requestPasswordData();
}
}
void PasswordCheckWidget::checkPasswordHash() {
if (_passwordState.mtp.request.id) {
passwordChecked();
} else {
requestPasswordData();
}
}
void PasswordCheckWidget::requestPasswordData() {
api().request(base::take(_sentRequest)).cancel();
_sentRequest = api().request(
MTPaccount_GetPassword()
).done([=](const MTPaccount_Password &result) {
_sentRequest = 0;
result.match([&](const MTPDaccount_password &data) {
base::RandomAddSeed(bytes::make_span(data.vsecure_random().v));
_passwordState = Core::ParseCloudPasswordState(data);
passwordChecked();
});
}).send();
}
void PasswordCheckWidget::passwordChecked() {
const auto check = Core::ComputeCloudPasswordCheck(
_passwordState.mtp.request,
_passwordHash);
if (!check) {
return serverError();
}
_passwordState.mtp.request.id = 0;
_sentRequest = api().request(
MTPauth_CheckPassword(check.result)
).done([=](const MTPauth_Authorization &result) {
pwdSubmitDone(false, result);
}).fail([=](const MTP::Error &error) {
pwdSubmitFail(error);
}).handleFloodErrors().send();
}
void PasswordCheckWidget::serverError() {
showError(rpl::single(Lang::Hard::ServerError()));
}
void PasswordCheckWidget::codeSubmitDone(
const QString &code,
const MTPBool &result) {
auto fields = PasscodeBox::CloudFields::From(_passwordState);
fields.fromRecoveryCode = code;
fields.hasRecovery = false;
fields.mtp.curRequest = {};
fields.hasPassword = false;
auto box = Box<PasscodeBox>(&api().instance(), nullptr, fields);
const auto boxShared = std::make_shared<base::weak_qptr<PasscodeBox>>();
box->newAuthorization(
) | rpl::on_next([=](const MTPauth_Authorization &result) {
if (boxShared) {
(*boxShared)->closeBox();
}
pwdSubmitDone(true, result);
}, lifetime());
*boxShared = Ui::show(std::move(box));
}
void PasswordCheckWidget::codeSubmitFail(const MTP::Error &error) {
if (MTP::IsFloodError(error)) {
showError(tr::lng_flood_error());
_codeField->showError();
return;
}
_sentRequest = 0;
const auto &type = error.type();
if (type == u"PASSWORD_EMPTY"_q
|| type == u"AUTH_KEY_UNREGISTERED"_q) {
goBack();
} else if (type == u"PASSWORD_RECOVERY_NA"_q) {
recoverStartFail(error);
} else if (type == u"PASSWORD_RECOVERY_EXPIRED"_q) {
_emailPattern = QString();
toPassword();
} else if (type == u"CODE_INVALID"_q) {
showError(tr::lng_signin_wrong_code());
_codeField->selectAll();
_codeField->showError();
} else {
if (Logs::DebugEnabled()) { // internal server error
showError(rpl::single(type + ": " + error.description()));
} else {
showError(rpl::single(Lang::Hard::ServerError()));
}
_codeField->setFocus();
}
}
void PasswordCheckWidget::recoverStarted(const MTPauth_PasswordRecovery &result) {
_emailPattern = qs(result.c_auth_passwordRecovery().vemail_pattern());
updateDescriptionText();
}
void PasswordCheckWidget::recoverStartFail(const MTP::Error &error) {
_pwdField->show();
_pwdHint->show();
_codeField->hide();
_pwdField->setFocus();
updateDescriptionText();
update();
hideError();
}
void PasswordCheckWidget::toRecover() {
if (_passwordState.hasRecovery) {
if (_sentRequest) {
api().request(base::take(_sentRequest)).cancel();
}
hideError();
_toRecover->hide();
_toPassword->show();
_pwdField->hide();
_pwdHint->hide();
_pwdField->setText(QString());
_codeField->show();
_codeField->setFocus();
updateDescriptionText();
if (_emailPattern.isEmpty()) {
api().request(
MTPauth_RequestPasswordRecovery()
).done([=](const MTPauth_PasswordRecovery &result) {
recoverStarted(result);
}).fail([=](const MTP::Error &error) {
recoverStartFail(error);
}).send();
}
} else {
const auto box = Ui::show(
Ui::MakeInformBox(tr::lng_signin_no_email_forgot()));
box->boxClosing(
) | rpl::on_next([=] {
showReset();
}, box->lifetime());
}
}
void PasswordCheckWidget::toPassword() {
const auto box = Ui::show(
Ui::MakeInformBox(tr::lng_signin_cant_email_forgot()));
box->boxClosing(
) | rpl::on_next([=] {
showReset();
}, box->lifetime());
}
void PasswordCheckWidget::showReset() {
if (_sentRequest) {
api().request(base::take(_sentRequest)).cancel();
}
_toRecover->show();
_toPassword->hide();
_pwdField->show();
_pwdHint->show();
_codeField->hide();
_codeField->setText(QString());
_pwdField->setFocus();
showResetButton();
updateDescriptionText();
update();
}
void PasswordCheckWidget::updateDescriptionText() {
auto pwdHidden = _pwdField->isHidden();
auto emailPattern = _emailPattern;
setDescriptionText(pwdHidden
? tr::lng_signin_recover_desc(
lt_email,
rpl::single(Ui::Text::WrapEmailPattern(emailPattern)),
tr::marked)
: tr::lng_signin_desc(tr::marked));
}
void PasswordCheckWidget::submit() {
if (_sentRequest) {
return;
}
if (_pwdField->isHidden()) {
auto code = _codeField->getLastText().trimmed();
if (code.isEmpty()) {
_codeField->showError();
return;
}
const auto send = crl::guard(this, [=] {
_sentRequest = api().request(MTPauth_CheckRecoveryPassword(
MTP_string(code)
)).done([=](const MTPBool &result) {
codeSubmitDone(code, result);
}).fail([=](const MTP::Error &error) {
codeSubmitFail(error);
}).handleFloodErrors().send();
});
if (_passwordState.notEmptyPassport) {
const auto confirmed = [=](Fn<void()> &&close) {
send();
close();
};
Ui::show(Ui::MakeConfirmBox({
.text = tr::lng_cloud_password_passport_losing(),
.confirmed = confirmed,
.confirmText = tr::lng_continue(),
}));
} else {
send();
}
} else {
hideError();
const auto password = _pwdField->getLastText().toUtf8();
_passwordHash = Core::ComputeCloudPasswordHash(
_passwordState.mtp.request.algo,
bytes::make_span(password));
checkPasswordHash();
}
}
rpl::producer<QString> PasswordCheckWidget::nextButtonText() const {
return tr::lng_intro_submit();
}
} // namespace details
} // namespace Intro

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 "intro/intro_step.h"
#include "core/core_cloud_password.h"
#include "mtproto/sender.h"
#include "base/timer.h"
namespace Ui {
class InputField;
class PasswordInput;
class RoundButton;
class LinkButton;
} // namespace Ui
namespace Intro {
namespace details {
class PasswordCheckWidget final : public Step {
public:
PasswordCheckWidget(
QWidget *parent,
not_null<Main::Account*> account,
not_null<Data*> data);
void setInnerFocus() override;
void activate() override;
void cancelled() override;
void submit() override;
rpl::producer<QString> nextButtonText() const override;
bool hasBack() const override {
return true;
}
protected:
void resizeEvent(QResizeEvent *e) override;
private:
void toRecover();
void toPassword();
int errorTop() const override;
void showReset();
void refreshLang();
void updateControlsGeometry();
void pwdSubmitDone(bool recover, const MTPauth_Authorization &result);
void pwdSubmitFail(const MTP::Error &error);
void codeSubmitDone(const QString &code, const MTPBool &result);
void codeSubmitFail(const MTP::Error &error);
void recoverStartFail(const MTP::Error &error);
void recoverStarted(const MTPauth_PasswordRecovery &result);
void updateDescriptionText();
void handleSrpIdInvalid();
void requestPasswordData();
void checkPasswordHash();
void passwordChecked();
void serverError();
Core::CloudPasswordState _passwordState;
crl::time _lastSrpIdInvalidTime = 0;
bytes::vector _passwordHash;
QString _emailPattern;
object_ptr<Ui::PasswordInput> _pwdField;
object_ptr<Ui::FlatLabel> _pwdHint;
object_ptr<Ui::InputField> _codeField;
object_ptr<Ui::LinkButton> _toRecover;
object_ptr<Ui::LinkButton> _toPassword;
mtpRequestId _sentRequest = 0;
};
} // namespace details
} // namespace Intro

View File

@@ -0,0 +1,322 @@
/*
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_phone.h"
#include "lang/lang_keys.h"
#include "intro/intro_code.h"
#include "intro/intro_email.h"
#include "intro/intro_qr.h"
#include "styles/style_intro.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/wrap/fade_wrap.h"
#include "ui/widgets/fields/special_fields.h"
#include "main/main_account.h"
#include "main/main_domain.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "data/data_user.h"
#include "ui/boxes/confirm_box.h"
#include "boxes/abstract_box.h"
#include "boxes/phone_banned_box.h"
#include "core/application.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "countries/countries_instance.h" // Countries::Groups
namespace Intro {
namespace details {
namespace {
[[nodiscard]] bool AllowPhoneAttempt(const QString &phone) {
const auto digits = ranges::count_if(
phone,
[](QChar ch) { return ch.isNumber(); });
return (digits > 1);
}
[[nodiscard]] QString DigitsOnly(QString value) {
static const auto RegExp = QRegularExpression("[^0-9]");
return value.replace(RegExp, QString());
}
} // namespace
PhoneWidget::PhoneWidget(
QWidget *parent,
not_null<Main::Account*> account,
not_null<Data*> data)
: Step(parent, account, data)
, _country(
this,
getData()->controller->uiShow(),
st::introCountry)
, _code(this, st::introCountryCode)
, _phone(
this,
st::introPhone,
[](const QString &s) { return Countries::Groups(s); })
, _checkRequestTimer([=] { checkRequest(); }) {
_code->setAccessibleName(tr::lng_country_code(tr::now));
_phone->setAccessibleName(tr::lng_phone_number(tr::now));
_phone->frontBackspaceEvent(
) | rpl::on_next([=](not_null<QKeyEvent*> e) {
_code->startErasing(e);
}, _code->lifetime());
_country->codeChanged(
) | rpl::on_next([=](const QString &code) {
_code->codeSelected(code);
_phone->chooseCode(code);
}, _country->lifetime());
_code->codeChanged(
) | rpl::on_next([=](const QString &code) {
_country->onChooseCode(code);
_phone->chooseCode(code);
}, _code->lifetime());
_code->addedToNumber(
) | rpl::on_next([=](const QString &added) {
_phone->addedToNumber(added);
}, _phone->lifetime());
_code->spacePressed() | rpl::on_next([=] {
submit();
}, _code->lifetime());
connect(_phone, &Ui::PhonePartInput::changed, [=] { phoneChanged(); });
connect(_code, &Ui::CountryCodeInput::changed, [=] { phoneChanged(); });
setTitleText(tr::lng_phone_title());
setDescriptionText(tr::lng_phone_desc());
getData()->updated.events(
) | rpl::on_next([=] {
countryChanged();
}, lifetime());
setErrorCentered(true);
setupQrLogin();
if (!_country->chooseCountry(getData()->country)) {
_country->chooseCountry(u"US"_q);
}
_changed = false;
}
QString PhoneWidget::accessibilityName() {
return tr::lng_phone_title(tr::now);
}
void PhoneWidget::setupQrLogin() {
const auto qrLogin = Ui::CreateChild<Ui::LinkButton>(
this,
tr::lng_phone_to_qr(tr::now));
qrLogin->show();
DEBUG_LOG(("PhoneWidget.qrLogin link created and shown."));
rpl::combine(
sizeValue(),
qrLogin->widthValue()
) | rpl::on_next([=](QSize size, int qrLoginWidth) {
qrLogin->moveToLeft(
(size.width() - qrLoginWidth) / 2,
contentTop() + st::introQrLoginLinkTop);
}, qrLogin->lifetime());
qrLogin->setClickedCallback([=] {
goReplace<QrWidget>(Animate::Forward);
});
}
void PhoneWidget::resizeEvent(QResizeEvent *e) {
Step::resizeEvent(e);
_country->moveToLeft(contentLeft(), contentTop() + st::introStepFieldTop);
auto phoneTop = _country->y() + _country->height() + st::introPhoneTop;
_code->moveToLeft(contentLeft(), phoneTop);
_phone->moveToLeft(contentLeft() + _country->width() - st::introPhone.width, phoneTop);
}
void PhoneWidget::showPhoneError(rpl::producer<QString> text) {
_phone->showError();
showError(std::move(text));
}
void PhoneWidget::hidePhoneError() {
hideError();
}
void PhoneWidget::countryChanged() {
if (!_changed) {
selectCountry(getData()->country);
}
}
void PhoneWidget::phoneChanged() {
_changed = true;
hidePhoneError();
}
void PhoneWidget::submit() {
if (_sentRequest || isHidden()) {
return;
}
{
const auto hasCodeButWaitingPhone = _code->hasFocus()
&& (_code->getLastText().size() > 1)
&& _phone->getLastText().isEmpty();
if (hasCodeButWaitingPhone) {
_phone->hideError();
_phone->setFocus();
return;
}
}
const auto phone = fullNumber();
if (!AllowPhoneAttempt(phone)) {
showPhoneError(tr::lng_bad_phone());
_phone->setFocus();
return;
}
cancelNearestDcRequest();
// Check if such account is authorized already.
const auto phoneDigits = DigitsOnly(phone);
for (const auto &[index, existing] : Core::App().domain().accounts()) {
const auto raw = existing.get();
if (const auto session = raw->maybeSession()) {
if (raw->mtp().environment() == account().mtp().environment()
&& DigitsOnly(session->user()->phone()) == phoneDigits) {
crl::on_main(raw, [=] {
Core::App().domain().activate(raw);
});
return;
}
}
}
hidePhoneError();
_checkRequestTimer.callEach(1000);
_sentPhone = phone;
api().instance().setUserPhone(_sentPhone);
_sentRequest = api().request(MTPauth_SendCode(
MTP_string(_sentPhone),
MTP_int(ApiId),
MTP_string(ApiHash),
MTP_codeSettings(
MTP_flags(0),
MTPVector<MTPbytes>(),
MTPstring(),
MTPBool())
)).done([=](const MTPauth_SentCode &result) {
phoneSubmitDone(result);
}).fail([=](const MTP::Error &error) {
phoneSubmitFail(error);
}).handleFloodErrors().send();
}
void PhoneWidget::stopCheck() {
_checkRequestTimer.cancel();
}
void PhoneWidget::checkRequest() {
auto status = api().instance().state(_sentRequest);
if (status < 0) {
auto leftms = -status;
if (leftms >= 1000) {
api().request(base::take(_sentRequest)).cancel();
}
}
if (!_sentRequest && status == MTP::RequestSent) {
stopCheck();
}
}
void PhoneWidget::phoneSubmitDone(const MTPauth_SentCode &result) {
stopCheck();
_sentRequest = 0;
result.match([&](const MTPDauth_sentCode &data) {
fillSentCodeData(data);
getData()->phone = DigitsOnly(_sentPhone);
getData()->phoneHash = qba(data.vphone_code_hash());
if (getData()->emailStatus == EmailStatus::SetupRequired) {
return goNext<EmailWidget>();
}
const auto next = data.vnext_type();
if (next && next->type() == mtpc_auth_codeTypeCall) {
getData()->callStatus = CallStatus::Waiting;
getData()->callTimeout = data.vtimeout().value_or(60);
} else {
getData()->callStatus = CallStatus::Disabled;
getData()->callTimeout = 0;
}
goNext<CodeWidget>();
}, [&](const MTPDauth_sentCodeSuccess &data) {
finish(data.vauthorization());
}, [](const MTPDauth_sentCodePaymentRequired &) {
LOG(("API Error: Unexpected auth.sentCodePaymentRequired "
"(PhoneWidget::phoneSubmitDone)."));
});
}
void PhoneWidget::phoneSubmitFail(const MTP::Error &error) {
if (MTP::IsFloodError(error)) {
stopCheck();
_sentRequest = 0;
showPhoneError(tr::lng_flood_error());
return;
}
stopCheck();
_sentRequest = 0;
auto &err = error.type();
if (err == u"PHONE_NUMBER_FLOOD"_q) {
Ui::show(Ui::MakeInformBox(tr::lng_error_phone_flood()));
} else if (err == u"PHONE_NUMBER_INVALID"_q) { // show error
showPhoneError(tr::lng_bad_phone());
} else if (err == u"PHONE_NUMBER_BANNED"_q) {
Ui::ShowPhoneBannedError(getData()->controller, _sentPhone);
} else if (Logs::DebugEnabled()) { // internal server error
showPhoneError(rpl::single(err + ": " + error.description()));
} else {
showPhoneError(rpl::single(Lang::Hard::ServerError()));
}
}
QString PhoneWidget::fullNumber() const {
return _code->getLastText() + _phone->getLastText();
}
void PhoneWidget::selectCountry(const QString &country) {
_country->chooseCountry(country);
}
void PhoneWidget::setInnerFocus() {
_phone->setFocusFast();
}
void PhoneWidget::activate() {
Step::activate();
showChildren();
setInnerFocus();
}
void PhoneWidget::finished() {
Step::finished();
_checkRequestTimer.cancel();
apiClear();
cancelled();
}
void PhoneWidget::cancelled() {
api().request(base::take(_sentRequest)).cancel();
}
} // namespace details
} // namespace Intro

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 "ui/countryinput.h"
#include "intro/intro_step.h"
#include "base/timer.h"
namespace Ui {
class PhonePartInput;
class CountryCodeInput;
class RoundButton;
class FlatLabel;
} // namespace Ui
namespace Intro {
namespace details {
class PhoneWidget final : public Step {
public:
PhoneWidget(
QWidget *parent,
not_null<Main::Account*> account,
not_null<Data*> data);
QString accessibilityName() override;
void selectCountry(const QString &country);
void setInnerFocus() override;
void activate() override;
void finished() override;
void cancelled() override;
void submit() override;
bool hasBack() const override {
return true;
}
protected:
void resizeEvent(QResizeEvent *e) override;
private:
void setupQrLogin();
void phoneChanged();
void checkRequest();
void countryChanged();
void phoneSubmitDone(const MTPauth_SentCode &result);
void phoneSubmitFail(const MTP::Error &error);
QString fullNumber() const;
void stopCheck();
void showPhoneError(rpl::producer<QString> text);
void hidePhoneError();
bool _changed = false;
object_ptr<CountryInput> _country;
object_ptr<Ui::CountryCodeInput> _code;
object_ptr<Ui::PhonePartInput> _phone;
QString _sentPhone;
mtpRequestId _sentRequest = 0;
base::Timer _checkRequestTimer;
};
} // namespace details
} // namespace Intro

View File

@@ -0,0 +1,542 @@
/*
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 "data/components/passkeys.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_app_config.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 "platform/platform_webauthn.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()
);
class QrWidget final : public Ui::RpWidget {
public:
using RpWidget::RpWidget;
QAccessible::Role accessibilityRole() override {
return QAccessible::Role::Graphic;
}
QString accessibilityName() override {
return tr::lng_intro_qr_title(tr::now);
}
};
auto result = Ui::CreateChild<QrWidget>(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::on_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::on_next([=](QImage &&image) {
state->center = std::move(image);
}, result->lifetime());
result->paintRequest(
) | rpl::on_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::on_next([=](const MTPUpdates &updates) {
checkForTokenUpdate(updates);
}, lifetime());
setupControls();
account->mtp().mainDcIdValue(
) | rpl::on_next([=] {
api().request(base::take(_requestId)).cancel();
refreshCode();
}, lifetime());
account->appConfig().value(
) | rpl::filter([=] {
return !_passkey;
}) | rpl::on_next([=] {
setupPasskeyLink();
}, lifetime());
}
QString QrWidget::accessibilityName() {
return tr::lng_intro_qr_title(tr::now);
}
QString QrWidget::accessibilityDescription() {
const auto phrases = {
tr::lng_intro_qr_step1,
tr::lng_intro_qr_step2,
tr::lng_intro_qr_step3,
};
auto result = QString();
auto index = 0;
for (const auto &phrase : phrases) {
result.append(QString::number(++index)).append(". ").append(phrase(tr::now)).append('\n');
}
return result;
}
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::on_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::on_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(tr::rich),
st::introQrStep),
st::introQrStepMargins);
const auto number = Ui::CreateChild<Ui::FlatLabel>(
steps,
rpl::single(tr::semibold(QString::number(++index) + ".")),
st::defaultFlatLabel);
rpl::combine(
number->widthValue(),
label->positionValue()
) | rpl::on_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::on_next([=](QSize size, int stepsWidth) {
steps->moveToLeft(
(size.width() - stepsWidth) / 2,
contentTop() + st::introQrStepsTop);
}, steps->lifetime());
_skip = Ui::CreateChild<Ui::LinkButton>(
this,
tr::lng_intro_qr_phone(tr::now));
rpl::combine(
sizeValue(),
_skip->widthValue()
) | rpl::on_next([=](QSize size, int skipWidth) {
_skip->moveToLeft(
(size.width() - skipWidth) / 2,
contentTop() + st::introQrSkipTop);
}, _skip->lifetime());
_skip->setClickedCallback([=] { submit(); });
}
void QrWidget::setupPasskeyLink() {
Expects(!_passkey);
if (!account().appConfig().settingsDisplayPasskeys()
|| !Platform::WebAuthn::IsSupported()) {
return;
}
_passkey = Ui::CreateChild<Ui::LinkButton>(
this,
tr::lng_intro_qr_passkey(tr::now));
_passkey->show();
rpl::combine(
sizeValue(),
_passkey->widthValue()
) | rpl::on_next([=](QSize size, int passkeyWidth) {
_passkey->moveToLeft(
(size.width() - passkeyWidth) / 2,
(contentTop()
+ st::introQrSkipTop
+ 1.5 * st::normalFont->height));
}, _passkey->lifetime());
_passkey->setClickedCallback([=] {
const auto initialDc = api().instance().mainDcId();
::Data::InitPasskeyLogin(api(), [=](
const ::Data::Passkey::LoginData &loginData) {
Platform::WebAuthn::Login(loginData, [=](
Platform::WebAuthn::LoginResult result) {
if (result.userHandle.isEmpty()) {
using Error = Platform::WebAuthn::Error;
if (result.error == Error::UnsignedBuild) {
showError(
tr::lng_settings_passkeys_unsigned_error());
}
return;
}
::Data::FinishPasskeyLogin(
api(),
initialDc,
result,
[=](const MTPauth_Authorization &auth) { done(auth); },
[=](QString error) {
if (error == u"SESSION_PASSWORD_NEEDED"_q) {
sendCheckPasswordRequest();
} else {
showError(rpl::single(error));
}
});
});
});
});
}
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();
if (_skip) {
_skip->setFocus(Qt::OtherFocusReason);
}
}
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

View File

@@ -0,0 +1,68 @@
/*
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/countryinput.h"
#include "intro/intro_step.h"
#include "base/timer.h"
namespace Ui {
class LinkButton;
} // namespace Ui
namespace Intro {
namespace details {
class QrWidget final : public Step {
public:
QrWidget(
QWidget *parent,
not_null<Main::Account*> account,
not_null<Data*> data);
QString accessibilityName() override;
QString accessibilityDescription() override;
void activate() override;
void finished() override;
void cancelled() override;
void submit() override;
rpl::producer<QString> nextButtonText() const override;
bool hasBack() const override {
return true;
}
private:
int errorTop() const override;
void sendCheckPasswordRequest();
void setupControls();
void setupPasskeyLink();
void refreshCode();
void checkForTokenUpdate(const MTPUpdates &updates);
void checkForTokenUpdate(const MTPUpdate &update);
void handleTokenResult(const MTPauth_LoginToken &result);
void showTokenError(const MTP::Error &error);
void importTo(MTP::DcId dcId, const QByteArray &token);
void showToken(const QByteArray &token);
void done(const MTPauth_Authorization &authorization);
rpl::event_stream<QByteArray> _qrCodes;
Ui::LinkButton *_skip = nullptr;
Ui::LinkButton *_passkey = nullptr;
base::Timer _refreshTimer;
mtpRequestId _requestId = 0;
bool _forceRefresh = false;
};
[[nodiscard]] QImage TelegramLogoImage();
} // namespace details
} // namespace Intro

View File

@@ -0,0 +1,214 @@
/*
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_signup.h"
#include "boxes/abstract_box.h"
#include "intro/intro_widget.h"
#include "core/file_utilities.h"
#include "ui/boxes/confirm_box.h"
#include "lang/lang_keys.h"
#include "ui/controls/userpic_button.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/labels.h"
#include "styles/style_intro.h"
#include "styles/style_boxes.h"
namespace Intro {
namespace details {
SignupWidget::SignupWidget(
QWidget *parent,
not_null<Main::Account*> account,
not_null<Data*> data)
: Step(parent, account, data)
, _photo(
this,
data->controller,
Ui::UserpicButton::Role::ChoosePhoto,
st::defaultUserpicButton)
, _first(this, st::introName, tr::lng_signup_firstname())
, _last(this, st::introName, tr::lng_signup_lastname())
, _invertOrder(langFirstNameGoesSecond()) {
_photo->showCustomOnChosen();
Lang::Updated(
) | rpl::on_next([=] {
refreshLang();
}, lifetime());
if (_invertOrder) {
setTabOrder(_last, _first);
} else {
setTabOrder(_first, _last);
}
setErrorCentered(true);
setTitleText(tr::lng_signup_title());
setDescriptionText(tr::lng_signup_desc());
setMouseTracking(true);
}
void SignupWidget::finishInit() {
showTerms();
}
void SignupWidget::refreshLang() {
_invertOrder = langFirstNameGoesSecond();
if (_invertOrder) {
setTabOrder(_last, _first);
} else {
setTabOrder(_first, _last);
}
updateControlsGeometry();
}
void SignupWidget::resizeEvent(QResizeEvent *e) {
Step::resizeEvent(e);
updateControlsGeometry();
}
void SignupWidget::updateControlsGeometry() {
auto photoRight = contentLeft() + st::introNextButton.width;
auto photoTop = contentTop() + st::introPhotoTop;
_photo->moveToLeft(photoRight - _photo->width(), photoTop);
auto firstTop = contentTop() + st::introStepFieldTop;
auto secondTop = firstTop + st::introName.heightMin + st::introPhoneTop;
if (_invertOrder) {
_last->moveToLeft(contentLeft(), firstTop);
_first->moveToLeft(contentLeft(), secondTop);
} else {
_first->moveToLeft(contentLeft(), firstTop);
_last->moveToLeft(contentLeft(), secondTop);
}
}
void SignupWidget::setInnerFocus() {
if (_invertOrder || _last->hasFocus()) {
_last->setFocusFast();
} else {
_first->setFocusFast();
}
}
void SignupWidget::activate() {
Step::activate();
_first->show();
_last->show();
_photo->show();
setInnerFocus();
}
void SignupWidget::cancelled() {
api().request(base::take(_sentRequest)).cancel();
}
void SignupWidget::nameSubmitDone(const MTPauth_Authorization &result) {
finish(result);
}
void SignupWidget::nameSubmitFail(const MTP::Error &error) {
if (MTP::IsFloodError(error)) {
showError(tr::lng_flood_error());
if (_invertOrder) {
_first->setFocus();
} else {
_last->setFocus();
}
return;
}
auto &err = error.type();
if (err == u"PHONE_NUMBER_FLOOD"_q) {
Ui::show(Ui::MakeInformBox(tr::lng_error_phone_flood()));
} else if (err == u"PHONE_NUMBER_INVALID"_q
|| err == u"PHONE_NUMBER_BANNED"_q
|| err == u"PHONE_CODE_EXPIRED"_q
|| err == u"PHONE_CODE_EMPTY"_q
|| err == u"PHONE_CODE_INVALID"_q
|| err == u"PHONE_NUMBER_OCCUPIED"_q) {
goBack();
} else if (err == "FIRSTNAME_INVALID") {
showError(tr::lng_bad_name());
_first->setFocus();
} else if (err == "LASTNAME_INVALID") {
showError(tr::lng_bad_name());
_last->setFocus();
} else {
if (Logs::DebugEnabled()) { // internal server error
showError(rpl::single(err + ": " + error.description()));
} else {
showError(rpl::single(Lang::Hard::ServerError()));
}
if (_invertOrder) {
_last->setFocus();
} else {
_first->setFocus();
}
}
}
void SignupWidget::submit() {
if (_sentRequest) {
return;
}
if (_invertOrder) {
if ((_last->hasFocus() || _last->getLastText().trimmed().length()) && !_first->getLastText().trimmed().length()) {
_first->setFocus();
return;
} else if (!_last->getLastText().trimmed().length()) {
_last->setFocus();
return;
}
} else {
if ((_first->hasFocus() || _first->getLastText().trimmed().length()) && !_last->getLastText().trimmed().length()) {
_last->setFocus();
return;
} else if (!_first->getLastText().trimmed().length()) {
_first->setFocus();
return;
}
}
const auto send = [&] {
hideError();
_firstName = _first->getLastText().trimmed();
_lastName = _last->getLastText().trimmed();
_sentRequest = api().request(MTPauth_SignUp(
MTP_flags(0),
MTP_string(getData()->phone),
MTP_bytes(getData()->phoneHash),
MTP_string(_firstName),
MTP_string(_lastName)
)).done([=](const MTPauth_Authorization &result) {
nameSubmitDone(result);
}).fail([=](const MTP::Error &error) {
nameSubmitFail(error);
}).handleFloodErrors().send();
};
if (_termsAccepted
|| getData()->termsLock.text.text.isEmpty()
|| !getData()->termsLock.popup) {
send();
} else {
acceptTerms(crl::guard(this, [=] {
_termsAccepted = true;
send();
}));
}
}
rpl::producer<QString> SignupWidget::nextButtonText() const {
return tr::lng_intro_finish();
}
} // namespace details
} // namespace Intro

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 "intro/intro_step.h"
namespace Ui {
class RoundButton;
class InputField;
class UserpicButton;
} // namespace Ui
namespace Intro {
namespace details {
class SignupWidget final : public Step {
public:
SignupWidget(
QWidget *parent,
not_null<Main::Account*> account,
not_null<Data*> data);
void finishInit() override;
void setInnerFocus() override;
void activate() override;
void cancelled() override;
void submit() override;
rpl::producer<QString> nextButtonText() const override;
protected:
void resizeEvent(QResizeEvent *e) override;
private:
void refreshLang();
void updateControlsGeometry();
void nameSubmitDone(const MTPauth_Authorization &result);
void nameSubmitFail(const MTP::Error &error);
object_ptr<Ui::UserpicButton> _photo;
object_ptr<Ui::InputField> _first;
object_ptr<Ui::InputField> _last;
QString _firstName, _lastName;
mtpRequestId _sentRequest = 0;
bool _invertOrder = false;
bool _termsAccepted = false;
};
} // namespace details
} // namespace Intro

View File

@@ -0,0 +1,55 @@
/*
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_start.h"
#include "lang/lang_keys.h"
#include "intro/intro_qr.h"
#include "intro/intro_phone.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "main/main_account.h"
#include "main/main_app_config.h"
namespace Intro {
namespace details {
StartWidget::StartWidget(
QWidget *parent,
not_null<Main::Account*> account,
not_null<Data*> data)
: Step(parent, account, data, true) {
setMouseTracking(true);
setTitleText(rpl::single(u"Telegram Desktop"_q));
setDescriptionText(tr::lng_intro_about());
show();
}
void StartWidget::submit() {
account().destroyStaleAuthorizationKeys();
goNext<QrWidget>();
}
rpl::producer<QString> StartWidget::nextButtonText() const {
return tr::lng_start_msgs();
}
rpl::producer<> StartWidget::nextButtonFocusRequests() const {
return _nextButtonFocusRequests.events();
}
void StartWidget::activate() {
Step::activate();
setInnerFocus();
}
void StartWidget::setInnerFocus() {
_nextButtonFocusRequests.fire({});
}
} // namespace details
} // namespace Intro

View File

@@ -0,0 +1,40 @@
/*
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 "intro/intro_step.h"
namespace Ui {
class FlatLabel;
class LinkButton;
class RoundButton;
} // namespace Ui
namespace Intro {
namespace details {
class StartWidget : public Step {
public:
StartWidget(
QWidget *parent,
not_null<Main::Account*> account,
not_null<Data*> data);
void submit() override;
rpl::producer<QString> nextButtonText() const override;
rpl::producer<> nextButtonFocusRequests() const override;
void activate() override;
void setInnerFocus() override;
private:
rpl::event_stream<> _nextButtonFocusRequests;
};
} // namespace details
} // namespace Intro

View File

@@ -0,0 +1,652 @@
/*
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_step.h"
#include "intro/intro_widget.h"
#include "intro/intro_signup.h"
#include "storage/localstorage.h"
#include "storage/storage_account.h"
#include "lang/lang_keys.h"
#include "lang/lang_instance.h"
#include "lang/lang_cloud_manager.h"
#include "main/main_account.h"
#include "main/main_app_config.h"
#include "main/main_domain.h"
#include "main/main_session.h"
#include "main/main_session_settings.h"
#include "boxes/abstract_box.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "apiwrap.h"
#include "api/api_peer_photo.h"
#include "mainwindow.h"
#include "ui/boxes/confirm_box.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/labels.h"
#include "ui/wrap/fade_wrap.h"
#include "ui/effects/slide_animation.h"
#include "ui/ui_utility.h"
#include "data/data_user.h"
#include "data/data_auto_download.h"
#include "data/data_session.h"
#include "data/data_chat_filters.h"
#include "window/window_controller.h"
#include "styles/style_intro.h"
#include "styles/style_window.h"
namespace Intro {
namespace details {
namespace {
void PrepareSupportMode(not_null<Main::Session*> session) {
using ::Data::AutoDownload::Full;
anim::SetDisabled(true);
Core::App().settings().setDesktopNotify(false);
Core::App().settings().setSoundNotify(false);
Core::App().settings().setFlashBounceNotify(false);
Core::App().saveSettings();
session->settings().autoDownload() = Full::FullDisabled();
session->saveSettings();
}
} // namespace
Step::CoverAnimation::~CoverAnimation() = default;
Step::Step(
QWidget *parent,
not_null<Main::Account*> account,
not_null<Data*> data,
bool hasCover)
: RpWidget(parent)
, _account(account)
, _data(data)
, _hasCover(hasCover)
, _title(this, _hasCover ? st::introCoverTitle : st::introTitle)
, _description(
this,
object_ptr<Ui::FlatLabel>(
this,
_hasCover
? st::introCoverDescription
: st::introDescription)) {
hide();
style::PaletteChanged(
) | rpl::on_next([=] {
if (!_coverMask.isNull()) {
_coverMask = QPixmap();
prepareCoverMask();
}
}, lifetime());
_errorText.value(
) | rpl::on_next([=](const QString &text) {
refreshError(text);
}, lifetime());
_titleText.value(
) | rpl::on_next([=](const QString &text) {
_title->setText(text);
accessibilityNameChanged();
updateLabelsPosition();
}, lifetime());
_descriptionText.value(
) | rpl::on_next([=](const TextWithEntities &text) {
const auto label = _description->entity();
const auto hasSpoiler = ranges::contains(
text.entities,
EntityType::Spoiler,
&EntityInText::type);
label->setMarkedText(text);
label->setAttribute(Qt::WA_TransparentForMouseEvents, hasSpoiler);
accessibilityDescriptionChanged();
updateLabelsPosition();
}, lifetime());
}
Step::~Step() = default;
MTP::Sender &Step::api() const {
if (!_api) {
_api.emplace(&_account->mtp());
}
return *_api;
}
void Step::apiClear() {
_api.reset();
}
rpl::producer<QString> Step::nextButtonText() const {
return tr::lng_intro_next();
}
rpl::producer<const style::RoundButton*> Step::nextButtonStyle() const {
return rpl::single((const style::RoundButton*)(nullptr));
}
rpl::producer<> Step::nextButtonFocusRequests() const {
return rpl::never();
}
void Step::goBack() {
if (_goCallback) {
_goCallback(nullptr, StackAction::Back, Animate::Back);
}
}
void Step::goNext(Step *step) {
if (_goCallback) {
_goCallback(step, StackAction::Forward, Animate::Forward);
}
}
void Step::goReplace(Step *step, Animate animate) {
if (_goCallback) {
_goCallback(step, StackAction::Replace, animate);
}
}
void Step::finish(const MTPauth_Authorization &auth, QImage &&photo) {
auth.match([&](const MTPDauth_authorization &data) {
if (data.vuser().type() != mtpc_user
|| !data.vuser().c_user().is_self()) {
showError(rpl::single(Lang::Hard::ServerError())); // wtf?
return;
}
finish(data.vuser(), std::move(photo));
}, [&](const MTPDauth_authorizationSignUpRequired &data) {
if (const auto terms = data.vterms_of_service()) {
terms->match([&](const MTPDhelp_termsOfService &data) {
getData()->termsLock = Window::TermsLock::FromMTP(
nullptr,
data);
});
} else {
getData()->termsLock = Window::TermsLock();
}
goReplace<SignupWidget>(Animate::Forward);
});
}
void Step::finish(const MTPUser &user, QImage &&photo) {
if (user.type() != mtpc_user
|| !user.c_user().is_self()
|| !user.c_user().vid().v) {
// No idea what to do here.
// We could've reset intro and MTP, but this really should not happen.
Ui::show(Ui::MakeInformBox(
"Internal error: bad user.is_self() after sign in."));
return;
}
// Check if such account is authorized already.
for (const auto &[index, existing] : Core::App().domain().accounts()) {
const auto raw = existing.get();
if (const auto session = raw->maybeSession()) {
if (raw->mtp().environment() == _account->mtp().environment()
&& UserId(user.c_user().vid()) == session->userId()) {
_account->logOut();
crl::on_main(raw, [=] {
Core::App().domain().activate(raw);
Local::sync();
});
return;
}
}
}
api().request(MTPmessages_GetDialogFilters(
)).done([=](const MTPmessages_DialogFilters &result) {
const auto &d = result.data();
createSession(user, photo, d.vfilters().v, d.is_tags_enabled());
}).fail([=] {
createSession(user, photo, QVector<MTPDialogFilter>(), false);
}).send();
}
void Step::createSession(
const MTPUser &user,
QImage photo,
const QVector<MTPDialogFilter> &filters,
bool tagsEnabled) {
// Save the default language if we've suggested some other and user ignored it.
const auto currentId = Lang::Id();
const auto defaultId = Lang::DefaultLanguageId();
const auto suggested = Lang::CurrentCloudManager().suggestedLanguage();
if (currentId.isEmpty() && !suggested.isEmpty() && suggested != defaultId) {
Lang::GetInstance().switchToId(Lang::DefaultLanguage());
Local::writeLangPack();
}
auto settings = std::make_unique<Main::SessionSettings>();
const auto hasFilters = ranges::contains(
filters,
mtpc_dialogFilter,
&MTPDialogFilter::type);
settings->setDialogsFiltersEnabled(hasFilters);
const auto account = _account;
account->createSession(user, std::move(settings));
// "this" is already deleted here by creating the main widget.
account->local().enforceModernStorageIdBots();
account->local().writeMtpData();
auto &session = account->session();
session.data().chatsFilters().setPreloaded(filters, tagsEnabled);
if (hasFilters) {
session.saveSettingsDelayed();
}
if (!photo.isNull()) {
session.api().peerPhoto().upload(
session.user(),
{ std::move(photo) });
}
account->appConfig().refresh();
if (session.supportMode()) {
PrepareSupportMode(&session);
}
Local::sync();
}
void Step::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
paintAnimated(p, e->rect());
}
void Step::resizeEvent(QResizeEvent *e) {
updateLabelsPosition();
}
void Step::updateLabelsPosition() {
Ui::SendPendingMoveResizeEvents(_description->entity());
if (hasCover()) {
_title->moveToLeft((width() - _title->width()) / 2, contentTop() + st::introCoverTitleTop);
_description->moveToLeft((width() - _description->width()) / 2, contentTop() + st::introCoverDescriptionTop);
} else {
_title->moveToLeft(contentLeft() + st::buttonRadius, contentTop() + st::introTitleTop);
_description->resizeToWidth(st::introDescription.minWidth);
_description->moveToLeft(contentLeft() + st::buttonRadius, contentTop() + st::introDescriptionTop);
}
if (_error) {
if (_errorCentered) {
_error->entity()->resizeToWidth(width());
}
Ui::SendPendingMoveResizeEvents(_error->entity());
auto errorLeft = _errorCentered ? 0 : (contentLeft() + st::buttonRadius);
_error->moveToLeft(errorLeft, errorTop());
}
}
int Step::errorTop() const {
return contentTop() + st::introErrorTop;
}
void Step::setTitleText(rpl::producer<QString> titleText) {
_titleText = std::move(titleText);
}
void Step::setDescriptionText(v::text::data &&descriptionText) {
_descriptionText = v::text::take_marked(std::move(descriptionText));
}
void Step::showFinished() {
_a_show.stop();
_coverAnimation = CoverAnimation();
_slideAnimation.reset();
prepareCoverMask();
activate();
}
bool Step::paintAnimated(QPainter &p, QRect clip) {
if (_slideAnimation) {
_slideAnimation->paintFrame(p, (width() - st::introStepWidth) / 2, contentTop(), width());
if (!_slideAnimation->animating()) {
showFinished();
return false;
}
return true;
}
auto dt = _a_show.value(1.);
if (!_a_show.animating()) {
if (hasCover()) {
paintCover(p, 0);
}
if (_coverAnimation.title) {
showFinished();
}
if (!QRect(0, contentTop(), width(), st::introStepHeight).intersects(clip)) {
return true;
}
return false;
}
if (!_coverAnimation.clipping.isEmpty()) {
p.setClipRect(_coverAnimation.clipping);
}
auto progress = (hasCover() ? anim::easeOutCirc(1., dt) : anim::linear(1., dt));
auto arrivingAlpha = progress;
auto departingAlpha = 1. - progress;
auto showCoverMethod = progress;
auto hideCoverMethod = progress;
auto coverTop = (hasCover() ? anim::interpolate(-st::introCoverHeight, 0, showCoverMethod) : anim::interpolate(0, -st::introCoverHeight, hideCoverMethod));
paintCover(p, coverTop);
auto positionReady = hasCover() ? showCoverMethod : hideCoverMethod;
_coverAnimation.title->paintFrame(p, positionReady, departingAlpha, arrivingAlpha);
_coverAnimation.description->paintFrame(p, positionReady, departingAlpha, arrivingAlpha);
paintContentSnapshot(p, _coverAnimation.contentSnapshotWas, departingAlpha, showCoverMethod);
paintContentSnapshot(p, _coverAnimation.contentSnapshotNow, arrivingAlpha, 1. - hideCoverMethod);
return true;
}
void Step::fillSentCodeData(const MTPDauth_sentCode &data) {
const auto bad = [](const char *type) {
LOG(("API Error: Should not be '%1'.").arg(type));
};
getData()->codeByTelegram = false;
getData()->codeByFragmentUrl = QString();
data.vtype().match([&](const MTPDauth_sentCodeTypeApp &data) {
getData()->codeByTelegram = true;
getData()->codeLength = data.vlength().v;
}, [&](const MTPDauth_sentCodeTypeSms &data) {
getData()->codeLength = data.vlength().v;
}, [&](const MTPDauth_sentCodeTypeFragmentSms &data) {
getData()->codeByFragmentUrl = qs(data.vurl());
getData()->codeLength = data.vlength().v;
}, [&](const MTPDauth_sentCodeTypeCall &data) {
getData()->codeLength = data.vlength().v;
}, [&](const MTPDauth_sentCodeTypeFlashCall &) {
bad("FlashCall");
}, [&](const MTPDauth_sentCodeTypeMissedCall &) {
bad("MissedCall");
}, [&](const MTPDauth_sentCodeTypeFirebaseSms &) {
bad("FirebaseSms");
}, [&](const MTPDauth_sentCodeTypeEmailCode &data) {
getData()->emailPatternLogin = qs(data.vemail_pattern());
getData()->codeLength = data.vlength().v;
}, [&](const MTPDauth_sentCodeTypeSmsWord &) {
bad("SmsWord");
}, [&](const MTPDauth_sentCodeTypeSmsPhrase &) {
bad("SmsPhrase");
}, [&](const MTPDauth_sentCodeTypeSetUpEmailRequired &) {
getData()->emailStatus = EmailStatus::SetupRequired;
});
}
void Step::showDescription() {
_description->show(anim::type::normal);
}
void Step::hideDescription() {
_description->hide(anim::type::normal);
}
void Step::paintContentSnapshot(QPainter &p, const QPixmap &snapshot, float64 alpha, float64 howMuchHidden) {
if (!snapshot.isNull()) {
const auto contentTop = anim::interpolate(
height() - (snapshot.height() / style::DevicePixelRatio()),
height(),
howMuchHidden);
if (contentTop < height()) {
p.setOpacity(alpha);
p.drawPixmap(
QPoint(contentLeft(), contentTop),
snapshot,
QRect(
0,
0,
snapshot.width(),
(height() - contentTop) * style::DevicePixelRatio()));
}
}
}
void Step::prepareCoverMask() {
if (!_coverMask.isNull()) return;
auto maskWidth = style::DevicePixelRatio();
auto maskHeight = st::introCoverHeight * style::DevicePixelRatio();
auto mask = QImage(maskWidth, maskHeight, QImage::Format_ARGB32_Premultiplied);
auto maskInts = reinterpret_cast<uint32*>(mask.bits());
Assert(mask.depth() == (sizeof(uint32) << 3));
auto maskIntsPerLineAdded = (mask.bytesPerLine() >> 2) - maskWidth;
Assert(maskIntsPerLineAdded >= 0);
auto realHeight = static_cast<float64>(maskHeight - 1);
for (auto y = 0; y != maskHeight; ++y) {
auto color = anim::color(st::introCoverTopBg, st::introCoverBottomBg, y / realHeight);
auto colorInt = anim::getPremultiplied(color);
for (auto x = 0; x != maskWidth; ++x) {
*maskInts++ = colorInt;
}
maskInts += maskIntsPerLineAdded;
}
_coverMask = Ui::PixmapFromImage(std::move(mask));
}
void Step::paintCover(QPainter &p, int top) {
auto coverHeight = top + st::introCoverHeight;
if (coverHeight > 0) {
p.drawPixmap(
QRect(0, 0, width(), coverHeight),
_coverMask,
QRect(
0,
-top * style::DevicePixelRatio(),
_coverMask.width(),
coverHeight * style::DevicePixelRatio()));
}
auto left = 0;
auto right = 0;
if (width() < st::introCoverMaxWidth) {
auto iconsMaxSkip = st::introCoverMaxWidth - st::introCoverLeft.width() - st::introCoverRight.width();
auto iconsSkip = st::introCoverIconsMinSkip + (iconsMaxSkip - st::introCoverIconsMinSkip) * (width() - st::introStepWidth) / (st::introCoverMaxWidth - st::introStepWidth);
auto outside = iconsSkip + st::introCoverLeft.width() + st::introCoverRight.width() - width();
left = -outside / 2;
right = -outside - left;
}
if (top < 0) {
auto shown = float64(coverHeight) / st::introCoverHeight;
auto leftShown = qRound(shown * (left + st::introCoverLeft.width()));
left = leftShown - st::introCoverLeft.width();
auto rightShown = qRound(shown * (right + st::introCoverRight.width()));
right = rightShown - st::introCoverRight.width();
}
st::introCoverLeft.paint(p, left, coverHeight - st::introCoverLeft.height(), width());
st::introCoverRight.paint(p, width() - right - st::introCoverRight.width(), coverHeight - st::introCoverRight.height(), width());
auto planeLeft = (width() - st::introCoverIcon.width()) / 2 - st::introCoverIconLeft;
auto planeTop = top + st::introCoverIconTop;
if (top < 0 && !_hasCover) {
auto deltaLeft = -qRound(float64(st::introPlaneWidth / st::introPlaneHeight) * top);
// auto deltaTop = top;
planeLeft += deltaLeft;
// planeTop += top;
}
st::introCoverIcon.paint(p, planeLeft, planeTop, width());
}
int Step::contentLeft() const {
return (width() - st::introNextButton.width) / 2;
}
int Step::contentTop() const {
auto result = (height() - st::introHeight) / 2;
accumulate_max(result, st::introStepTopMin);
if (_hasCover) {
const auto currentHeightFull = result + st::introNextTop + st::introContentTopAdd;
auto added = 1. - std::clamp(
float64(currentHeightFull - st::windowMinHeight)
/ (st::introStepHeightFull - st::windowMinHeight),
0.,
1.);
result += qRound(added * st::introContentTopAdd);
}
return result;
}
void Step::setErrorCentered(bool centered) {
_errorCentered = centered;
_error.destroy();
}
void Step::showError(rpl::producer<QString> text) {
_errorText = std::move(text);
}
void Step::refreshError(const QString &text) {
if (text.isEmpty()) {
if (_error) _error->hide(anim::type::normal);
} else {
if (!_error) {
_error.create(
this,
object_ptr<Ui::FlatLabel>(
this,
_errorCentered
? st::introErrorCentered
: st::introError));
_error->hide(anim::type::instant);
}
_error->entity()->setText(text);
updateLabelsPosition();
_error->show(anim::type::normal);
}
}
void Step::prepareShowAnimated(Step *after) {
setInnerFocus();
if (hasCover() || after->hasCover()) {
_coverAnimation = prepareCoverAnimation(after);
prepareCoverMask();
} else {
auto leftSnapshot = after->prepareSlideAnimation();
auto rightSnapshot = prepareSlideAnimation();
_slideAnimation = std::make_unique<Ui::SlideAnimation>();
_slideAnimation->setSnapshots(std::move(leftSnapshot), std::move(rightSnapshot));
_slideAnimation->setOverflowHidden(false);
}
}
Step::CoverAnimation Step::prepareCoverAnimation(Step *after) {
Ui::SendPendingMoveResizeEvents(this);
auto result = CoverAnimation();
result.title = Ui::FlatLabel::CrossFade(
after->_title,
_title,
st::introBg);
result.description = Ui::FlatLabel::CrossFade(
after->_description->entity(),
_description->entity(),
st::introBg,
after->_description->pos(),
_description->pos());
result.contentSnapshotWas = after->prepareContentSnapshot();
result.contentSnapshotNow = prepareContentSnapshot();
return result;
}
QPixmap Step::prepareContentSnapshot() {
auto otherTop = _description->y() + _description->height();
auto otherRect = myrtlrect(contentLeft(), otherTop, st::introStepWidth, height() - otherTop);
return Ui::GrabWidget(this, otherRect);
}
QPixmap Step::prepareSlideAnimation() {
auto grabLeft = (width() - st::introStepWidth) / 2;
auto grabTop = contentTop();
return Ui::GrabWidget(
this,
QRect(grabLeft, grabTop, st::introStepWidth, st::introStepHeight));
}
void Step::showAnimated(Animate animate) {
setFocus();
show();
hideChildren();
if (_slideAnimation) {
auto slideLeft = (animate == Animate::Back);
_slideAnimation->start(
slideLeft,
[=] { update(0, contentTop(), width(), st::introStepHeight); },
st::introSlideDuration);
} else {
_a_show.start([this] { update(); }, 0., 1., st::introCoverDuration);
}
}
void Step::setShowAnimationClipping(QRect clipping) {
_coverAnimation.clipping = clipping;
}
void Step::setGoCallback(
Fn<void(Step *step, StackAction action, Animate animate)> callback) {
_goCallback = std::move(callback);
}
void Step::setShowResetCallback(Fn<void()> callback) {
_showResetCallback = std::move(callback);
}
void Step::setShowTermsCallback(Fn<void()> callback) {
_showTermsCallback = std::move(callback);
}
void Step::setCancelNearestDcCallback(Fn<void()> callback) {
_cancelNearestDcCallback = std::move(callback);
}
void Step::setAcceptTermsCallback(
Fn<void(Fn<void()> callback)> callback) {
_acceptTermsCallback = std::move(callback);
}
void Step::showFast() {
show();
showFinished();
}
bool Step::animating() const {
return (_slideAnimation && _slideAnimation->animating())
|| _a_show.animating();
}
bool Step::hasCover() const {
return _hasCover;
}
bool Step::hasBack() const {
return false;
}
void Step::activate() {
_title->show();
_description->show(anim::type::instant);
if (!_errorText.current().isEmpty()) {
_error->show(anim::type::instant);
}
}
void Step::cancelled() {
}
void Step::finished() {
hide();
}
} // namespace details
} // namespace Intro

View File

@@ -0,0 +1,225 @@
/*
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 "base/object_ptr.h"
#include "mtproto/sender.h"
#include "ui/text/text_variant.h"
#include "ui/rp_widget.h"
#include "ui/effects/animations.h"
namespace style {
struct RoundButton;
} // namespace style;
namespace Main {
class Account;
} // namespace Main;
namespace Ui {
class SlideAnimation;
class CrossFadeAnimation;
class FlatLabel;
template <typename Widget>
class FadeWrap;
} // namespace Ui
namespace Intro {
namespace details {
struct Data;
enum class StackAction;
enum class Animate;
class Step : public Ui::RpWidget {
public:
Step(
QWidget *parent,
not_null<Main::Account*> account,
not_null<Data*> data,
bool hasCover = false);
~Step();
QAccessible::Role accessibilityRole() override {
return QAccessible::Role::Dialog;
}
QString accessibilityName() override {
return _titleText.current();
}
QString accessibilityDescription() override {
return _descriptionText.current().text;
}
[[nodiscard]] Main::Account &account() const {
return *_account;
}
// It should not be called in StartWidget, in other steps it should be
// present and not changing.
[[nodiscard]] MTP::Sender &api() const;
void apiClear();
virtual void finishInit() {
}
virtual void setInnerFocus() {
setFocus();
}
void setGoCallback(
Fn<void(Step *step, StackAction action, Animate animate)> callback);
void setShowResetCallback(Fn<void()> callback);
void setShowTermsCallback(Fn<void()> callback);
void setCancelNearestDcCallback(Fn<void()> callback);
void setAcceptTermsCallback(
Fn<void(Fn<void()> callback)> callback);
void prepareShowAnimated(Step *after);
void showAnimated(Animate animate);
void showFast();
[[nodiscard]] bool animating() const;
void setShowAnimationClipping(QRect clipping);
[[nodiscard]] bool hasCover() const;
[[nodiscard]] virtual bool hasBack() const;
virtual void activate();
virtual void cancelled();
virtual void finished();
virtual void submit() = 0;
[[nodiscard]] virtual rpl::producer<QString> nextButtonText() const;
[[nodiscard]] virtual auto nextButtonStyle() const
-> rpl::producer<const style::RoundButton*>;
[[nodiscard]] virtual rpl::producer<> nextButtonFocusRequests() const;
[[nodiscard]] int contentLeft() const;
[[nodiscard]] int contentTop() const;
void setErrorCentered(bool centered);
void showError(rpl::producer<QString> text);
void hideError() {
showError(rpl::single(QString()));
}
protected:
void paintEvent(QPaintEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
void setTitleText(rpl::producer<QString> titleText);
void setDescriptionText(v::text::data &&descriptionText);
bool paintAnimated(QPainter &p, QRect clip);
void fillSentCodeData(const MTPDauth_sentCode &type);
void showDescription();
void hideDescription();
[[nodiscard]] not_null<Data*> getData() const {
return _data;
}
void finish(const MTPauth_Authorization &auth, QImage &&photo = {});
void finish(const MTPUser &user, QImage &&photo = {});
void createSession(
const MTPUser &user,
QImage photo,
const QVector<MTPDialogFilter> &filters,
bool tagsEnabled);
void goBack();
template <typename StepType>
void goNext() {
goNext(new StepType(parentWidget(), _account, _data));
}
template <typename StepType>
void goReplace(Animate animate) {
goReplace(new StepType(parentWidget(), _account, _data), animate);
}
void showResetButton() {
if (_showResetCallback) _showResetCallback();
}
void showTerms() {
if (_showTermsCallback) _showTermsCallback();
}
void acceptTerms(Fn<void()> callback) {
if (_acceptTermsCallback) {
_acceptTermsCallback(callback);
}
}
void cancelNearestDcRequest() {
if (_cancelNearestDcCallback) _cancelNearestDcCallback();
}
virtual int errorTop() const;
private:
struct CoverAnimation {
CoverAnimation() = default;
CoverAnimation(CoverAnimation &&other) = default;
CoverAnimation &operator=(CoverAnimation &&other) = default;
~CoverAnimation();
std::unique_ptr<Ui::CrossFadeAnimation> title;
std::unique_ptr<Ui::CrossFadeAnimation> description;
// From content top till the next button top.
QPixmap contentSnapshotWas;
QPixmap contentSnapshotNow;
QRect clipping;
};
void updateLabelsPosition();
void paintContentSnapshot(
QPainter &p,
const QPixmap &snapshot,
float64 alpha,
float64 howMuchHidden);
void refreshError(const QString &text);
void goNext(Step *step);
void goReplace(Step *step, Animate animate);
[[nodiscard]] CoverAnimation prepareCoverAnimation(Step *step);
[[nodiscard]] QPixmap prepareContentSnapshot();
[[nodiscard]] QPixmap prepareSlideAnimation();
void showFinished();
void prepareCoverMask();
void paintCover(QPainter &p, int top);
const not_null<Main::Account*> _account;
const not_null<Data*> _data;
mutable std::optional<MTP::Sender> _api;
bool _hasCover = false;
Fn<void(Step *step, StackAction action, Animate animate)> _goCallback;
Fn<void()> _showResetCallback;
Fn<void()> _showTermsCallback;
Fn<void()> _cancelNearestDcCallback;
Fn<void(Fn<void()> callback)> _acceptTermsCallback;
rpl::variable<QString> _titleText;
object_ptr<Ui::FlatLabel> _title;
rpl::variable<TextWithEntities> _descriptionText;
object_ptr<Ui::FadeWrap<Ui::FlatLabel>> _description;
bool _errorCentered = false;
rpl::variable<QString> _errorText;
object_ptr<Ui::FadeWrap<Ui::FlatLabel>> _error = { nullptr };
Ui::Animations::Simple _a_show;
CoverAnimation _coverAnimation;
std::unique_ptr<Ui::SlideAnimation> _slideAnimation;
QPixmap _coverMask;
};
} // namespace details
} // namespace Intro

View File

@@ -0,0 +1,906 @@
/*
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_widget.h"
#include "intro/intro_start.h"
#include "intro/intro_phone.h"
#include "intro/intro_qr.h"
#include "intro/intro_code.h"
#include "intro/intro_signup.h"
#include "intro/intro_password_check.h"
#include "lang/lang_keys.h"
#include "lang/lang_instance.h"
#include "lang/lang_cloud_manager.h"
#include "storage/localstorage.h"
#include "main/main_account.h"
#include "main/main_domain.h"
#include "main/main_session.h"
#include "mainwindow.h"
#include "history/history.h"
#include "history/history_item.h"
#include "data/data_user.h"
#include "data/components/promo_suggestions.h"
#include "countries/countries_instance.h"
#include "ui/boxes/confirm_box.h"
#include "ui/text/format_values.h" // Ui::FormatPhone
#include "ui/text/text_utilities.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/wrap/fade_wrap.h"
#include "ui/ui_utility.h"
#include "boxes/abstract_box.h"
#include "core/update_checker.h"
#include "core/application.h"
#include "mtproto/mtproto_dc_options.h"
#include "window/window_slide_animation.h"
#include "window/window_connecting_widget.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "window/section_widget.h"
#include "base/platform/base_platform_info.h"
#include "api/api_text_entities.h"
#include "styles/style_layers.h"
#include "styles/style_intro.h"
#include "base/qt/qt_common_adapters.h"
namespace Intro {
namespace {
using namespace ::Intro::details;
[[nodiscard]] QString ComputeNewAccountCountry() {
if (const auto parent
= Core::App().domain().maybeLastOrSomeAuthedAccount()) {
if (const auto session = parent->maybeSession()) {
const auto iso = Countries::Instance().countryISO2ByPhone(
session->user()->phone());
if (!iso.isEmpty()) {
return iso;
}
}
}
return Platform::SystemCountry();
}
} // namespace
Widget::Widget(
QWidget *parent,
not_null<Window::Controller*> controller,
not_null<Main::Account*> account,
EnterPoint point)
: RpWidget(parent)
, _account(account)
, _data(details::Data{ .controller = controller })
, _nextStyle(&st::introNextButton)
, _back(this, object_ptr<Ui::IconButton>(this, st::introBackButton))
, _settings(
this,
object_ptr<Ui::RoundButton>(
this,
tr::lng_menu_settings(),
st::defaultBoxButton))
, _next(
this,
object_ptr<Ui::RoundButton>(this, nullptr, *_nextStyle))
, _connecting(std::make_unique<Window::ConnectionState>(
this,
account,
rpl::single(true))) {
controller->setDefaultFloatPlayerDelegate(floatPlayerDelegate());
getData()->country = ComputeNewAccountCountry();
_account->mtpValue(
) | rpl::on_next([=](not_null<MTP::Instance*> instance) {
_api.emplace(instance);
crl::on_main(this, [=] { createLanguageLink(); });
}, lifetime());
switch (point) {
case EnterPoint::Start:
getNearestDC();
appendStep(new StartWidget(this, _account, getData()));
break;
case EnterPoint::Phone:
appendStep(new PhoneWidget(this, _account, getData()));
break;
case EnterPoint::Qr:
appendStep(new QrWidget(this, _account, getData()));
break;
default: Unexpected("Enter point in Intro::Widget::Widget.");
}
setupStep();
fixOrder();
if (_account->mtp().isTestMode()) {
_testModeLabel.create(
this,
object_ptr<Ui::FlatLabel>(
this,
u"Test Mode"_q,
st::defaultFlatLabel));
_testModeLabel->entity()->setTextColorOverride(
st::windowSubTextFg->c);
_testModeLabel->show(anim::type::instant);
}
Lang::CurrentCloudManager().firstLanguageSuggestion(
) | rpl::on_next([=] {
createLanguageLink();
}, lifetime());
_account->mtpUpdates(
) | rpl::on_next([=](const MTPUpdates &updates) {
handleUpdates(updates);
}, lifetime());
_back->entity()->setClickedCallback([=] { backRequested(); });
_back->entity()->setAccessibleName(tr::lng_go_back(tr::now));
_back->hide(anim::type::instant);
if (_changeLanguage) {
_changeLanguage->finishAnimating();
}
Lang::Updated(
) | rpl::on_next([=] {
refreshLang();
}, lifetime());
show();
showControls();
getStep()->showFast();
setInnerFocus();
cSetPasswordRecovered(false);
if (!Core::UpdaterDisabled()) {
Core::UpdateChecker checker;
checker.start();
rpl::merge(
rpl::single(rpl::empty),
checker.isLatest(),
checker.failed(),
checker.ready()
) | rpl::on_next([=] {
checkUpdateStatus();
}, lifetime());
}
}
rpl::producer<> Widget::showSettingsRequested() const {
return _settings->entity()->clicks() | rpl::to_empty;
}
not_null<Media::Player::FloatDelegate*> Widget::floatPlayerDelegate() {
return static_cast<Media::Player::FloatDelegate*>(this);
}
auto Widget::floatPlayerSectionDelegate()
-> not_null<Media::Player::FloatSectionDelegate*> {
return static_cast<Media::Player::FloatSectionDelegate*>(this);
}
not_null<Ui::RpWidget*> Widget::floatPlayerWidget() {
return this;
}
void Widget::floatPlayerToggleGifsPaused(bool paused) {
}
auto Widget::floatPlayerGetSection(Window::Column column)
-> not_null<Media::Player::FloatSectionDelegate*> {
return this;
}
void Widget::floatPlayerEnumerateSections(Fn<void(
not_null<Media::Player::FloatSectionDelegate*> widget,
Window::Column widgetColumn)> callback) {
callback(this, Window::Column::Second);
}
bool Widget::floatPlayerIsVisible(not_null<HistoryItem*> item) {
return false;
}
void Widget::floatPlayerDoubleClickEvent(not_null<const HistoryItem*> item) {
getData()->controller->invokeForSessionController(
&item->history()->peer->session().account(),
item->history()->peer,
[&](not_null<Window::SessionController*> controller) {
controller->showMessage(item);
});
}
QRect Widget::floatPlayerAvailableRect() {
return mapToGlobal(rect());
}
bool Widget::floatPlayerHandleWheelEvent(QEvent *e) {
return false;
}
void Widget::refreshLang() {
_changeLanguage.destroy();
createLanguageLink();
InvokeQueued(this, [this] { updateControlsGeometry(); });
}
void Widget::handleUpdates(const MTPUpdates &updates) {
updates.match([&](const MTPDupdateShort &data) {
handleUpdate(data.vupdate());
}, [&](const MTPDupdates &data) {
for (const auto &update : data.vupdates().v) {
handleUpdate(update);
}
}, [&](const MTPDupdatesCombined &data) {
for (const auto &update : data.vupdates().v) {
handleUpdate(update);
}
}, [](const auto &) {});
}
void Widget::handleUpdate(const MTPUpdate &update) {
update.match([&](const MTPDupdateDcOptions &data) {
_account->mtp().dcOptions().addFromList(data.vdc_options());
}, [&](const MTPDupdateConfig &data) {
_account->mtp().requestConfig();
if (_account->sessionExists()) {
_account->session().promoSuggestions().invalidate();
}
}, [&](const MTPDupdateServiceNotification &data) {
const auto text = TextWithEntities{
qs(data.vmessage()),
Api::EntitiesFromMTP(nullptr, data.ventities().v)
};
Ui::show(Ui::MakeInformBox(text));
}, [](const auto &) {});
}
void Widget::createLanguageLink() {
if (_changeLanguage
|| Core::App().domain().maybeLastOrSomeAuthedAccount()) {
return;
}
const auto createLink = [=](
const QString &text,
const QString &languageId) {
_changeLanguage.create(
this,
object_ptr<Ui::LinkButton>(this, text));
_changeLanguage->hide(anim::type::instant);
_changeLanguage->entity()->setClickedCallback([=] {
Lang::CurrentCloudManager().switchToLanguage(languageId);
});
_changeLanguage->toggle(
!_resetAccount && !_terms && _nextShown,
anim::type::normal);
updateControlsGeometry();
};
const auto currentId = Lang::LanguageIdOrDefault(Lang::Id());
const auto defaultId = Lang::DefaultLanguageId();
const auto suggested = Lang::CurrentCloudManager().suggestedLanguage();
if (currentId != defaultId) {
createLink(
Lang::GetOriginalValue(tr::lng_switch_to_this.base),
defaultId);
} else if (!suggested.isEmpty() && suggested != currentId && _api) {
_api->request(MTPlangpack_GetStrings(
MTP_string(Lang::CloudLangPackName()),
MTP_string(suggested),
MTP_vector<MTPstring>(1, MTP_string("lng_switch_to_this"))
)).done([=](const MTPVector<MTPLangPackString> &result) {
const auto strings = Lang::Instance::ParseStrings(result);
const auto i = strings.find(tr::lng_switch_to_this.base);
if (i != strings.end()) {
createLink(i->second, suggested);
}
}).send();
}
}
void Widget::checkUpdateStatus() {
Expects(!Core::UpdaterDisabled());
if (Core::UpdateChecker().state() == Core::UpdateChecker::State::Ready) {
if (_update) return;
_update.create(
this,
object_ptr<Ui::RoundButton>(
this,
tr::lng_menu_update(),
st::defaultBoxButton));
if (!_showAnimation) {
_update->setVisible(true);
}
const auto stepHasCover = getStep()->hasCover();
_update->toggle(!stepHasCover, anim::type::instant);
_update->entity()->setClickedCallback([] {
Core::checkReadyUpdate();
Core::Restart();
});
} else {
if (!_update) return;
_update.destroy();
}
updateControlsGeometry();
}
void Widget::setInnerFocus() {
if (getStep()->animating()) {
setFocus();
} else {
getStep()->setInnerFocus();
}
}
void Widget::setupStep() {
getStep()->nextButtonStyle(
) | rpl::on_next([=](const style::RoundButton *st) {
const auto nextStyle = st ? st : &st::introNextButton;
if (_nextStyle != nextStyle) {
_nextStyle = nextStyle;
const auto wasShown = _next->toggled();
_next.destroy();
_next.create(
this,
object_ptr<Ui::RoundButton>(this, nullptr, *nextStyle));
showControls();
updateControlsGeometry();
_next->toggle(wasShown, anim::type::instant);
}
}, getStep()->lifetime());
getStep()->nextButtonFocusRequests() | rpl::on_next([=] {
if (_next && !_next->isHidden()) {
_next->entity()->setFocus(Qt::OtherFocusReason);
}
}, getStep()->lifetime());
getStep()->finishInit();
}
void Widget::historyMove(StackAction action, Animate animate) {
Expects(_stepHistory.size() > 1);
if (getStep()->animating()) {
return;
}
auto wasStep = getStep((action == StackAction::Back) ? 0 : 1);
if (action == StackAction::Back) {
_stepHistory.pop_back();
wasStep->cancelled();
} else if (action == StackAction::Replace) {
_stepHistory.erase(_stepHistory.end() - 2);
}
if (_resetAccount) {
hideAndDestroy(std::exchange(_resetAccount, { nullptr }));
}
if (_terms) {
hideAndDestroy(std::exchange(_terms, { nullptr }));
}
setupStep();
getStep()->prepareShowAnimated(wasStep);
if (wasStep->hasCover() != getStep()->hasCover()) {
_nextTopFrom = wasStep->contentTop() + st::introNextTop;
_controlsTopFrom = wasStep->hasCover() ? st::introCoverHeight : 0;
_coverShownAnimation.start(
[this] { updateControlsGeometry(); },
0.,
1.,
st::introCoverDuration,
wasStep->hasCover() ? anim::linear : anim::easeOutCirc);
}
_stepLifetime.destroy();
if (action == StackAction::Forward || action == StackAction::Replace) {
wasStep->finished();
}
if (action == StackAction::Back || action == StackAction::Replace) {
delete base::take(wasStep);
}
_back->toggle(getStep()->hasBack(), anim::type::normal);
auto stepHasCover = getStep()->hasCover();
_settings->toggle(!stepHasCover, anim::type::normal);
if (_testModeLabel) {
_testModeLabel->toggle(!stepHasCover, anim::type::normal);
}
if (_update) {
_update->toggle(!stepHasCover, anim::type::normal);
}
setupNextButton();
if (_resetAccount) _resetAccount->show(anim::type::normal);
if (_terms) _terms->show(anim::type::normal);
getStep()->showAnimated(animate);
fixOrder();
}
void Widget::hideAndDestroy(object_ptr<Ui::FadeWrap<Ui::RpWidget>> widget) {
const auto weak = base::make_weak(widget.data());
widget->hide(anim::type::normal);
widget->shownValue(
) | rpl::on_next([=](bool shown) {
if (!shown && weak) {
weak->deleteLater();
}
}, widget->lifetime());
}
void Widget::fixOrder() {
_next->raise();
if (_update) _update->raise();
if (_changeLanguage) _changeLanguage->raise();
_settings->raise();
_back->raise();
floatPlayerRaiseAll();
_connecting->raise();
}
void Widget::moveToStep(Step *step, StackAction action, Animate animate) {
appendStep(step);
_back->raise();
_settings->raise();
if (_update) {
_update->raise();
}
_connecting->raise();
historyMove(action, animate);
}
void Widget::appendStep(Step *step) {
_stepHistory.push_back(step);
step->setGeometry(rect());
step->setGoCallback([=](Step *step, StackAction action, Animate animate) {
if (action == StackAction::Back) {
backRequested();
} else {
moveToStep(step, action, animate);
}
});
step->setShowResetCallback([=] {
showResetButton();
});
step->setShowTermsCallback([=] {
showTerms();
});
step->setCancelNearestDcCallback([=] {
if (_api) {
_api->request(base::take(_nearestDcRequestId)).cancel();
}
});
step->setAcceptTermsCallback([=](Fn<void()> callback) {
acceptTerms(callback);
});
}
void Widget::showResetButton() {
if (!_resetAccount) {
auto entity = object_ptr<Ui::RoundButton>(
this,
tr::lng_signin_reset_account(),
st::introResetButton);
_resetAccount.create(this, std::move(entity));
_resetAccount->hide(anim::type::instant);
_resetAccount->entity()->setClickedCallback([this] { resetAccount(); });
updateControlsGeometry();
}
_resetAccount->show(anim::type::normal);
if (_changeLanguage) {
_changeLanguage->hide(anim::type::normal);
}
}
void Widget::showTerms() {
if (getData()->termsLock.text.text.isEmpty()) {
_terms.destroy();
} else if (!_terms) {
auto entity = object_ptr<Ui::FlatLabel>(
this,
tr::lng_terms_signup(
lt_link,
tr::lng_terms_signup_link(tr::link),
tr::marked),
st::introTermsLabel);
_terms.create(this, std::move(entity));
_terms->entity()->overrideLinkClickHandler([=] {
showTerms(nullptr);
});
updateControlsGeometry();
_terms->hide(anim::type::instant);
}
if (_changeLanguage) {
_changeLanguage->toggle(
!_terms && !_resetAccount && _nextShown,
anim::type::normal);
}
}
void Widget::acceptTerms(Fn<void()> callback) {
showTerms(callback);
}
void Widget::resetAccount() {
if (_resetRequest || !_api) {
return;
}
const auto callback = crl::guard(this, [this] {
if (_resetRequest) {
return;
}
_resetRequest = _api->request(MTPaccount_DeleteAccount(
MTP_flags(0),
MTP_string("Forgot password"),
MTPInputCheckPasswordSRP()
)).done([=] {
_resetRequest = 0;
getData()->controller->hideLayer();
if (getData()->phone.isEmpty()) {
moveToStep(
new QrWidget(this, _account, getData()),
StackAction::Replace,
Animate::Back);
} else {
moveToStep(
new SignupWidget(this, _account, getData()),
StackAction::Replace,
Animate::Forward);
}
}).fail([=](const MTP::Error &error) {
_resetRequest = 0;
const auto &type = error.type();
if (type.startsWith(u"2FA_CONFIRM_WAIT_"_q)) {
const auto seconds = base::StringViewMid(
type,
u"2FA_CONFIRM_WAIT_"_q.size()).toInt();
const auto days = (seconds + 59) / 86400;
const auto hours = ((seconds + 59) % 86400) / 3600;
const auto minutes = ((seconds + 59) % 3600) / 60;
auto when = tr::lng_minutes(tr::now, lt_count, minutes);
if (days > 0) {
const auto daysCount = tr::lng_days(
tr::now,
lt_count,
days);
const auto hoursCount = tr::lng_hours(
tr::now,
lt_count,
hours);
when = tr::lng_signin_reset_in_days(
tr::now,
lt_days_count,
daysCount,
lt_hours_count,
hoursCount,
lt_minutes_count,
when);
} else if (hours > 0) {
const auto hoursCount = tr::lng_hours(
tr::now,
lt_count,
hours);
when = tr::lng_signin_reset_in_hours(
tr::now,
lt_hours_count,
hoursCount,
lt_minutes_count,
when);
}
Ui::show(Ui::MakeInformBox(tr::lng_signin_reset_wait(
tr::now,
lt_phone_number,
Ui::FormatPhone(getData()->phone),
lt_when,
when)));
} else if (type == u"2FA_RECENT_CONFIRM"_q) {
Ui::show(Ui::MakeInformBox(
tr::lng_signin_reset_cancelled()));
} else {
getData()->controller->hideLayer();
getStep()->showError(rpl::single(Lang::Hard::ServerError()));
}
}).send();
});
Ui::show(Ui::MakeConfirmBox({
.text = tr::lng_signin_sure_reset(),
.confirmed = callback,
.confirmText = tr::lng_signin_reset(),
.confirmStyle = &st::attentionBoxButton,
}));
}
void Widget::getNearestDC() {
if (!_api) {
return;
}
_nearestDcRequestId = _api->request(MTPhelp_GetNearestDc(
)).done([=](const MTPNearestDc &result) {
_nearestDcRequestId = 0;
const auto &nearest = result.c_nearestDc();
DEBUG_LOG(("Got nearest dc, country: %1, nearest: %2, this: %3"
).arg(qs(nearest.vcountry())
).arg(nearest.vnearest_dc().v
).arg(nearest.vthis_dc().v));
_account->suggestMainDcId(nearest.vnearest_dc().v);
const auto nearestCountry = qs(nearest.vcountry());
if (getData()->country != nearestCountry) {
getData()->country = nearestCountry;
getData()->updated.fire({});
}
}).send();
}
void Widget::showTerms(Fn<void()> callback) {
if (getData()->termsLock.text.text.isEmpty()) {
return;
}
const auto weak = base::make_weak(this);
const auto box = Ui::show(callback
? Box<Window::TermsBox>(
getData()->termsLock,
tr::lng_terms_agree(),
tr::lng_terms_decline())
: Box<Window::TermsBox>(
getData()->termsLock.text,
tr::lng_box_ok(),
nullptr));
box->setCloseByEscape(false);
box->setCloseByOutsideClick(false);
box->agreeClicks(
) | rpl::on_next([=] {
if (callback) {
callback();
}
if (box) {
box->closeBox();
}
}, box->lifetime());
box->cancelClicks(
) | rpl::on_next([=] {
const auto box = Ui::show(Box<Window::TermsBox>(
TextWithEntities{ tr::lng_terms_signup_sorry(tr::now) },
tr::lng_intro_finish(),
tr::lng_terms_decline()));
box->agreeClicks(
) | rpl::on_next([=] {
if (weak) {
showTerms(callback);
}
}, box->lifetime());
box->cancelClicks(
) | rpl::on_next([=] {
if (box) {
box->closeBox();
}
}, box->lifetime());
}, box->lifetime());
}
void Widget::showControls() {
getStep()->show();
setupNextButton();
_next->toggle(_nextShown, anim::type::instant);
_nextShownAnimation.stop();
_connecting->setForceHidden(false);
auto hasCover = getStep()->hasCover();
_settings->toggle(!hasCover, anim::type::instant);
if (_testModeLabel) {
_testModeLabel->toggle(!hasCover, anim::type::instant);
}
if (_update) {
_update->toggle(!hasCover, anim::type::instant);
}
if (_changeLanguage) {
_changeLanguage->toggle(
!_resetAccount && !_terms && _nextShown,
anim::type::instant);
}
if (_terms) {
_terms->show(anim::type::instant);
}
_back->toggle(getStep()->hasBack(), anim::type::instant);
}
void Widget::setupNextButton() {
_next->entity()->setClickedCallback([=] { getStep()->submit(); });
_next->entity()->setTextTransform(
Ui::RoundButton::TextTransform::NoTransform);
_next->entity()->setText(getStep()->nextButtonText(
) | rpl::filter([](const QString &text) {
return !text.isEmpty();
}));
getStep()->nextButtonText(
) | rpl::map([](const QString &text) {
return !text.isEmpty();
}) | rpl::filter([=](bool visible) {
return visible != _nextShown;
}) | rpl::on_next([=](bool visible) {
_next->toggle(visible, anim::type::normal);
_nextShown = visible;
if (_changeLanguage) {
_changeLanguage->toggle(
!_resetAccount && !_terms && _nextShown,
anim::type::normal);
}
_nextShownAnimation.start(
[=] { updateControlsGeometry(); },
_nextShown ? 0. : 1.,
_nextShown ? 1. : 0.,
st::slideDuration);
}, _stepLifetime);
}
void Widget::hideControls() {
getStep()->hide();
_next->hide(anim::type::instant);
_connecting->setForceHidden(true);
_settings->hide(anim::type::instant);
if (_testModeLabel) _testModeLabel->hide(anim::type::instant);
if (_update) _update->hide(anim::type::instant);
if (_changeLanguage) _changeLanguage->hide(anim::type::instant);
if (_terms) _terms->hide(anim::type::instant);
_back->hide(anim::type::instant);
}
void Widget::showAnimated(QPixmap oldContentCache, bool back) {
_showAnimation = nullptr;
showControls();
floatPlayerHideAll();
auto newContentCache = Ui::GrabWidget(this);
hideControls();
floatPlayerShowVisible();
_showAnimation = std::make_unique<Window::SlideAnimation>();
_showAnimation->setDirection(back
? Window::SlideDirection::FromLeft
: Window::SlideDirection::FromRight);
_showAnimation->setRepaintCallback([=] { update(); });
_showAnimation->setFinishedCallback([=] { showFinished(); });
_showAnimation->setPixmaps(oldContentCache, newContentCache);
_showAnimation->start();
show();
}
void Widget::showFinished() {
_showAnimation = nullptr;
showControls();
getStep()->activate();
}
void Widget::paintEvent(QPaintEvent *e) {
const auto trivial = (rect() == e->rect());
setMouseTracking(true);
QPainter p(this);
if (!trivial) {
p.setClipRect(e->rect());
}
if (_showAnimation) {
_showAnimation->paintContents(p);
return;
}
p.fillRect(e->rect(), st::windowBg);
}
void Widget::resizeEvent(QResizeEvent *e) {
if (_stepHistory.empty()) {
return;
}
for (const auto step : _stepHistory) {
step->setGeometry(rect());
}
updateControlsGeometry();
floatPlayerAreaUpdated();
}
void Widget::updateControlsGeometry() {
const auto skip = st::introSettingsSkip;
const auto shown = _coverShownAnimation.value(1.);
const auto controlsTop = anim::interpolate(
_controlsTopFrom,
getStep()->hasCover() ? st::introCoverHeight : 0,
shown);
_settings->moveToRight(skip, controlsTop + skip);
if (_testModeLabel) {
_testModeLabel->moveToRight(
skip + _settings->width() + skip,
_settings->y()
+ (_settings->height()
- _testModeLabel->height()) / 2);
}
if (_update) {
_update->moveToRight(
skip + _settings->width() + skip,
_settings->y());
}
_back->moveToLeft(0, controlsTop);
auto nextTopTo = getStep()->contentTop() + st::introNextTop;
auto nextTop = anim::interpolate(_nextTopFrom, nextTopTo, shown);
const auto shownAmount = _nextShownAnimation.value(_nextShown ? 1. : 0.);
const auto realNextTop = anim::interpolate(
nextTop + st::introNextSlide,
nextTop,
shownAmount);
_next->moveToLeft((width() - _next->width()) / 2, realNextTop);
getStep()->setShowAnimationClipping(shownAmount > 0
? QRect(0, 0, width(), realNextTop)
: QRect());
if (_changeLanguage) {
_changeLanguage->moveToLeft(
(width() - _changeLanguage->width()) / 2,
_next->y() + _next->height() + _changeLanguage->height());
}
if (_resetAccount) {
_resetAccount->moveToLeft(
(width() - _resetAccount->width()) / 2,
height() - st::introResetBottom - _resetAccount->height());
}
if (_terms) {
_terms->moveToLeft(
(width() - _terms->width()) / 2,
height() - st::introTermsBottom - _terms->height());
}
}
void Widget::keyPressEvent(QKeyEvent *e) {
if (_showAnimation || getStep()->animating()) return;
if (e->key() == Qt::Key_Escape || e->key() == Qt::Key_Back) {
if (getStep()->hasBack()) {
backRequested();
}
} else if (e->key() == Qt::Key_Enter
|| e->key() == Qt::Key_Return
|| e->key() == Qt::Key_Space) {
getStep()->submit();
}
}
void Widget::backRequested() {
if (_stepHistory.size() > 1) {
historyMove(StackAction::Back, Animate::Back);
} else if (const auto parent
= Core::App().domain().maybeLastOrSomeAuthedAccount()) {
Core::App().domain().activate(parent);
} else {
moveToStep(
Ui::CreateChild<StartWidget>(this, _account, getData()),
StackAction::Replace,
Animate::Back);
}
}
Widget::~Widget() {
for (auto step : base::take(_stepHistory)) {
delete step;
}
}
} // namespace Intro

View File

@@ -0,0 +1,222 @@
/*
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 "ui/rp_widget.h"
#include "ui/effects/animations.h"
#include "window/window_lock_widgets.h"
#include "core/core_cloud_password.h"
#include "media/player/media_player_float.h"
namespace Main {
class Account;
} // namespace Main
namespace Ui {
class IconButton;
class RoundButton;
class LinkButton;
class FlatLabel;
template <typename Widget>
class FadeWrap;
} // namespace Ui
namespace Window {
class ConnectionState;
class Controller;
class SlideAnimation;
} // namespace Window
namespace Intro {
namespace details {
enum class CallStatus {
Waiting,
Calling,
Called,
Disabled,
};
enum class EmailStatus {
None,
SetupRequired,
};
struct Data {
// Required for the UserpicButton.
const not_null<Window::Controller*> controller;
QString country;
QString phone;
QByteArray phoneHash;
CallStatus callStatus = CallStatus::Disabled;
int callTimeout = 0;
int codeLength = 5;
bool codeByTelegram = false;
QString codeByFragmentUrl;
EmailStatus emailStatus = EmailStatus::None;
QString email;
QString emailPatternSetup;
QString emailPatternLogin;
Core::CloudPasswordState pwdState;
Window::TermsLock termsLock;
rpl::event_stream<> updated;
};
enum class StackAction {
Back,
Forward,
Replace,
};
enum class Animate {
Back,
Forward,
};
class Step;
} // namespace details
enum class EnterPoint : uchar {
Start,
Phone,
Qr,
};
class Widget
: public Ui::RpWidget
, private Media::Player::FloatDelegate
, private Media::Player::FloatSectionDelegate {
public:
Widget(
QWidget *parent,
not_null<Window::Controller*> controller,
not_null<Main::Account*> account,
EnterPoint point);
~Widget();
void showAnimated(QPixmap oldContentCache, bool back = false);
void setInnerFocus();
[[nodiscard]] rpl::producer<> showSettingsRequested() const;
protected:
void paintEvent(QPaintEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
void keyPressEvent(QKeyEvent *e) override;
private:
void setupStep();
void refreshLang();
void showFinished();
void createLanguageLink();
void checkUpdateStatus();
void setupNextButton();
void handleUpdates(const MTPUpdates &updates);
void handleUpdate(const MTPUpdate &update);
void backRequested();
void updateControlsGeometry();
[[nodiscard]] not_null<details::Data*> getData() {
return &_data;
}
void fixOrder();
void showControls();
void hideControls();
void showResetButton();
void resetAccount();
void showTerms();
void acceptTerms(Fn<void()> callback);
void hideAndDestroy(object_ptr<Ui::FadeWrap<Ui::RpWidget>> widget);
[[nodiscard]] details::Step *getStep(int skip = 0) const {
Expects(skip >= 0);
Expects(skip < _stepHistory.size());
return _stepHistory[_stepHistory.size() - skip - 1];
}
void historyMove(details::StackAction action, details::Animate animate);
void moveToStep(
details::Step *step,
details::StackAction action,
details::Animate animate);
void appendStep(details::Step *step);
void getNearestDC();
void showTerms(Fn<void()> callback);
// FloatDelegate
[[nodiscard]] auto floatPlayerDelegate()
-> not_null<Media::Player::FloatDelegate*>;
[[nodiscard]] auto floatPlayerSectionDelegate()
-> not_null<Media::Player::FloatSectionDelegate*>;
not_null<Ui::RpWidget*> floatPlayerWidget() override;
void floatPlayerToggleGifsPaused(bool paused) override;
not_null<Media::Player::FloatSectionDelegate*> floatPlayerGetSection(
Window::Column column) override;
void floatPlayerEnumerateSections(Fn<void(
not_null<Media::Player::FloatSectionDelegate*> widget,
Window::Column widgetColumn)> callback) override;
bool floatPlayerIsVisible(not_null<HistoryItem*> item) override;
void floatPlayerDoubleClickEvent(
not_null<const HistoryItem*> item) override;
// FloatSectionDelegate
QRect floatPlayerAvailableRect() override;
bool floatPlayerHandleWheelEvent(QEvent *e) override;
const not_null<Main::Account*> _account;
std::optional<MTP::Sender> _api;
mtpRequestId _nearestDcRequestId = 0;
std::unique_ptr<Window::SlideAnimation> _showAnimation;
std::vector<details::Step*> _stepHistory;
rpl::lifetime _stepLifetime;
details::Data _data;
Ui::Animations::Simple _coverShownAnimation;
int _nextTopFrom = 0;
int _controlsTopFrom = 0;
const style::RoundButton *_nextStyle = nullptr;
object_ptr<Ui::FadeWrap<Ui::IconButton>> _back;
object_ptr<Ui::FadeWrap<Ui::RoundButton>> _update = { nullptr };
object_ptr<Ui::FadeWrap<Ui::RoundButton>> _settings;
object_ptr<Ui::FadeWrap<Ui::FlatLabel>> _testModeLabel = { nullptr };
object_ptr<Ui::FadeWrap<Ui::RoundButton>> _next;
object_ptr<Ui::FadeWrap<Ui::LinkButton>> _changeLanguage = { nullptr };
object_ptr<Ui::FadeWrap<Ui::RoundButton>> _resetAccount = { nullptr };
object_ptr<Ui::FadeWrap<Ui::FlatLabel>> _terms = { nullptr };
std::unique_ptr<Window::ConnectionState> _connecting;
bool _nextShown = true;
Ui::Animations::Simple _nextShownAnimation;
mtpRequestId _resetRequest = 0;
};
} // namespace Intro