init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Close stale issues and PRs / stale (push) Successful in 13s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Close stale issues and PRs / stale (push) Successful in 13s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
This commit is contained in:
195
Telegram/SourceFiles/intro/intro.style
Normal file
195
Telegram/SourceFiles/intro/intro.style
Normal 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 }};
|
||||
500
Telegram/SourceFiles/intro/intro_code.cpp
Normal file
500
Telegram/SourceFiles/intro/intro_code.cpp
Normal 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
|
||||
98
Telegram/SourceFiles/intro/intro_code.h
Normal file
98
Telegram/SourceFiles/intro/intro_code.h
Normal 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
|
||||
353
Telegram/SourceFiles/intro/intro_code_input.cpp
Normal file
353
Telegram/SourceFiles/intro/intro_code_input.cpp
Normal 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
|
||||
61
Telegram/SourceFiles/intro/intro_code_input.h
Normal file
61
Telegram/SourceFiles/intro/intro_code_input.h
Normal 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
|
||||
175
Telegram/SourceFiles/intro/intro_email.cpp
Normal file
175
Telegram/SourceFiles/intro/intro_email.cpp
Normal 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
|
||||
50
Telegram/SourceFiles/intro/intro_email.h
Normal file
50
Telegram/SourceFiles/intro/intro_email.h
Normal 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
|
||||
410
Telegram/SourceFiles/intro/intro_password_check.cpp
Normal file
410
Telegram/SourceFiles/intro/intro_password_check.cpp
Normal 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
|
||||
85
Telegram/SourceFiles/intro/intro_password_check.h
Normal file
85
Telegram/SourceFiles/intro/intro_password_check.h
Normal file
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "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
|
||||
322
Telegram/SourceFiles/intro/intro_phone.cpp
Normal file
322
Telegram/SourceFiles/intro/intro_phone.cpp
Normal 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
|
||||
77
Telegram/SourceFiles/intro/intro_phone.h
Normal file
77
Telegram/SourceFiles/intro/intro_phone.h
Normal 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
|
||||
542
Telegram/SourceFiles/intro/intro_qr.cpp
Normal file
542
Telegram/SourceFiles/intro/intro_qr.cpp
Normal 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
|
||||
68
Telegram/SourceFiles/intro/intro_qr.h
Normal file
68
Telegram/SourceFiles/intro/intro_qr.h
Normal 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
|
||||
214
Telegram/SourceFiles/intro/intro_signup.cpp
Normal file
214
Telegram/SourceFiles/intro/intro_signup.cpp
Normal 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
|
||||
58
Telegram/SourceFiles/intro/intro_signup.h
Normal file
58
Telegram/SourceFiles/intro/intro_signup.h
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "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
|
||||
55
Telegram/SourceFiles/intro/intro_start.cpp
Normal file
55
Telegram/SourceFiles/intro/intro_start.cpp
Normal 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
|
||||
40
Telegram/SourceFiles/intro/intro_start.h
Normal file
40
Telegram/SourceFiles/intro/intro_start.h
Normal 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
|
||||
652
Telegram/SourceFiles/intro/intro_step.cpp
Normal file
652
Telegram/SourceFiles/intro/intro_step.cpp
Normal 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
|
||||
225
Telegram/SourceFiles/intro/intro_step.h
Normal file
225
Telegram/SourceFiles/intro/intro_step.h
Normal 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
|
||||
906
Telegram/SourceFiles/intro/intro_widget.cpp
Normal file
906
Telegram/SourceFiles/intro/intro_widget.cpp
Normal 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
|
||||
222
Telegram/SourceFiles/intro/intro_widget.h
Normal file
222
Telegram/SourceFiles/intro/intro_widget.h
Normal 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
|
||||
Reference in New Issue
Block a user