diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 00167d31c..cf50921c9 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -82,6 +82,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_box_ok" = "OK"; "lng_box_done" = "Done"; +"lng_box_yes" = "Yes"; +"lng_box_no" = "No"; "lng_cancel" = "Cancel"; "lng_continue" = "Continue"; @@ -596,6 +598,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_cloud_password_remove" = "Remove cloud password"; "lng_cloud_password_set" = "Enable two-step verification"; "lng_cloud_password_edit" = "Change cloud password"; +"lng_cloud_password_reset_in" = "Reset password in"; +"lng_cloud_password_reset_ready" = "Reset password"; +"lng_cloud_password_reset_cancel" = "Cancel password reset"; "lng_cloud_password_enter_old" = "Enter current password"; "lng_cloud_password_enter_first" = "Enter a password"; "lng_cloud_password_enter_new" = "Enter new password"; @@ -618,6 +623,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_cloud_password_passport_losing" = "Warning! All data saved in your Telegram Passport will be lost!"; "lng_cloud_password_resend" = "Resend code"; "lng_cloud_password_resent" = "Code was resent."; +"lng_cloud_password_reset_title" = "Reset password"; +"lng_cloud_password_reset_no_email" = "Since you didn't provide a recovery email when setting up your password, your remaining options are either to remember your password or wait 7 days until your password is reset."; +"lng_cloud_password_reset_with_email" = "If you don't have access to your recovery email, your remaining options are either to remember your password or wait 7 days until your password resets."; +"lng_cloud_password_reset_ok" = "Reset"; +"lng_cloud_password_reset_cancel_title" = "Cancel reset"; +"lng_cloud_password_reset_cancel_sure" = "Cancel the password reset process? If you request a new reset later, it will take another 7 days."; +"lng_cloud_password_reset_later" = "You recently requested a password reset that was cancelled. Please wait {duration} before making a new request."; "lng_connection_auto_connecting" = "Default (connecting...)"; "lng_connection_auto" = "Default ({transport} used)"; diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 45acdc0a7..14b57e4ad 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -4764,6 +4764,23 @@ void ApiWrap::reloadPasswordState() { }).send(); } +void ApiWrap::applyPendingReset(const MTPaccount_ResetPasswordResult &data) { + if (!_passwordState) { + reloadPasswordState(); + return; + } + data.match([&](const MTPDaccount_resetPasswordOk &data) { + reloadPasswordState(); + }, [&](const MTPDaccount_resetPasswordRequestedWait &data) { + const auto until = data.vuntil_date().v; + if (_passwordState->pendingResetDate != until) { + _passwordState->pendingResetDate = until; + _passwordStateChanges.fire_copy(*_passwordState); + } + }, [&](const MTPDaccount_resetPasswordFailedWait &data) { + }); +} + void ApiWrap::clearUnconfirmedPassword() { _passwordRequestId = request(MTPaccount_CancelPasswordEmail( )).done([=](const MTPBool &result) { diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index 9c3a58287..bdc5ad740 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -434,6 +434,7 @@ public: void clearPeerPhoto(not_null photo); void reloadPasswordState(); + void applyPendingReset(const MTPaccount_ResetPasswordResult &data); void clearUnconfirmedPassword(); rpl::producer passwordState() const; std::optional passwordStateCurrent() const; diff --git a/Telegram/SourceFiles/boxes/passcode_box.cpp b/Telegram/SourceFiles/boxes/passcode_box.cpp index b8e6e06b2..f3cb007b3 100644 --- a/Telegram/SourceFiles/boxes/passcode_box.cpp +++ b/Telegram/SourceFiles/boxes/passcode_box.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "boxes/confirm_box.h" #include "boxes/confirm_phone_box.h" +#include "base/unixtime.h" #include "mainwindow.h" #include "apiwrap.h" #include "main/main_session.h" @@ -95,6 +96,56 @@ void TransferPasswordError( } } +void StartPendingReset( + not_null session, + not_null context, + Fn close) { + const auto weak = Ui::MakeWeak(context.get()); + session->api().request(MTPaccount_ResetPassword( + )).done([=](const MTPaccount_ResetPasswordResult &result) { + session->api().applyPendingReset(result); + result.match([&](const MTPDaccount_resetPasswordOk &data) { + }, [&](const MTPDaccount_resetPasswordRequestedWait &data) { + }, [&](const MTPDaccount_resetPasswordFailedWait &data) { + constexpr auto kMinute = 60; + constexpr auto kHour = 3600; + constexpr auto kDay = 86400; + const auto left = std::max( + data.vretry_date().v - base::unixtime::now(), + kMinute); + const auto days = (left / kDay); + const auto hours = (left / kHour); + const auto minutes = (left / kMinute); + const auto duration = days + ? tr::lng_group_call_duration_days(tr::now, lt_count, days) + : hours + ? tr::lng_group_call_duration_hours(tr::now, lt_count, hours) + : tr::lng_group_call_duration_minutes( + tr::now, + lt_count, + minutes); + if (const auto strong = weak.data()) { + strong->getDelegate()->show(Box( + tr::lng_cloud_password_reset_later( + tr::now, + lt_duration, + duration))); + } + }); + if (const auto strong = weak.data()) { + strong->closeBox(); + } + close(); + }).fail([=](const MTP::Error &error) { + if (const auto strong = weak.data()) { + strong->getDelegate()->show( + Box("Error: " + error.type())); + strong->closeBox(); + } + close(); + }).send(); +} + } // namespace PasscodeBox::CloudFields PasscodeBox::CloudFields::From( @@ -106,6 +157,7 @@ PasscodeBox::CloudFields PasscodeBox::CloudFields::From( result.hasRecovery = current.hasRecovery; result.notEmptyPassport = current.notEmptyPassport; result.hint = current.hint; + result.pendingResetDate = current.pendingResetDate; return result; } @@ -145,7 +197,8 @@ PasscodeBox::PasscodeBox( , _reenterPasscode(this, st::defaultInputField, tr::lng_cloud_password_confirm_new()) , _passwordHint(this, st::defaultInputField, fields.curRequest ? tr::lng_cloud_password_change_hint() : tr::lng_cloud_password_hint()) , _recoverEmail(this, st::defaultInputField, tr::lng_cloud_password_email()) -, _recover(this, tr::lng_signin_recover(tr::now)) { +, _recover(this, tr::lng_signin_recover(tr::now)) +, _showRecoverLink(_cloudFields.hasRecovery || !_cloudFields.pendingResetDate) { Expects(!_turningOff || _cloudFields.curRequest); if (!_cloudFields.hint.isEmpty()) { @@ -203,14 +256,14 @@ void PasscodeBox::prepare() { : _cloudPwd ? tr::lng_cloud_password_remove() : tr::lng_passcode_remove()); - setDimensions(st::boxWidth, st::passcodePadding.top() + _oldPasscode->height() + st::passcodeTextLine + ((_cloudFields.hasRecovery && !_hintText.isEmpty()) ? st::passcodeTextLine : 0) + st::passcodeAboutSkip + _aboutHeight + st::passcodePadding.bottom()); + setDimensions(st::boxWidth, st::passcodePadding.top() + _oldPasscode->height() + st::passcodeTextLine + ((_showRecoverLink && !_hintText.isEmpty()) ? st::passcodeTextLine : 0) + st::passcodeAboutSkip + _aboutHeight + st::passcodePadding.bottom()); } else { if (currentlyHave()) { _oldPasscode->show(); setTitle(_cloudPwd ? tr::lng_cloud_password_change() : tr::lng_passcode_change()); - setDimensions(st::boxWidth, st::passcodePadding.top() + _oldPasscode->height() + st::passcodeTextLine + ((_cloudFields.hasRecovery && !_hintText.isEmpty()) ? st::passcodeTextLine : 0) + _newPasscode->height() + st::passcodeLittleSkip + _reenterPasscode->height() + st::passcodeSkip + (_cloudPwd ? _passwordHint->height() + st::passcodeLittleSkip : 0) + st::passcodeAboutSkip + _aboutHeight + st::passcodePadding.bottom()); + setDimensions(st::boxWidth, st::passcodePadding.top() + _oldPasscode->height() + st::passcodeTextLine + ((_showRecoverLink && !_hintText.isEmpty()) ? st::passcodeTextLine : 0) + _newPasscode->height() + st::passcodeLittleSkip + _reenterPasscode->height() + st::passcodeSkip + (_cloudPwd ? _passwordHint->height() + st::passcodeLittleSkip : 0) + st::passcodeAboutSkip + _aboutHeight + st::passcodePadding.bottom()); } else { _oldPasscode->hide(); setTitle(_cloudPwd @@ -237,7 +290,9 @@ void PasscodeBox::prepare() { const auto has = currentlyHave(); _oldPasscode->setVisible(onlyCheck || has); - _recover->setVisible((onlyCheck || has) && _cloudPwd && _cloudFields.hasRecovery); + _recover->setVisible((onlyCheck || has) + && _cloudPwd + && _showRecoverLink); _newPasscode->setVisible(!onlyCheck); _reenterPasscode->setVisible(!onlyCheck); _passwordHint->setVisible(!onlyCheck && _cloudPwd); @@ -285,7 +340,7 @@ void PasscodeBox::paintEvent(QPaintEvent *e) { Painter p(this); int32 w = st::boxWidth - st::boxPadding.left() * 1.5; - int32 abouty = (_passwordHint->isHidden() ? ((_reenterPasscode->isHidden() ? (_oldPasscode->y() + (_cloudFields.hasRecovery && !_hintText.isEmpty() ? st::passcodeTextLine : 0)) : _reenterPasscode->y()) + st::passcodeSkip) : _passwordHint->y()) + _oldPasscode->height() + st::passcodeLittleSkip + st::passcodeAboutSkip; + int32 abouty = (_passwordHint->isHidden() ? ((_reenterPasscode->isHidden() ? (_oldPasscode->y() + (_showRecoverLink && !_hintText.isEmpty() ? st::passcodeTextLine : 0)) : _reenterPasscode->y()) + st::passcodeSkip) : _passwordHint->y()) + _oldPasscode->height() + st::passcodeLittleSkip + st::passcodeAboutSkip; p.setPen(st::boxTextFg); _about.drawLeft(p, st::boxPadding.left(), abouty, w, width()); @@ -317,7 +372,7 @@ void PasscodeBox::resizeEvent(QResizeEvent *e) { _oldPasscode->resize(w, _oldPasscode->height()); _oldPasscode->moveToLeft(st::boxPadding.left(), st::passcodePadding.top()); _newPasscode->resize(w, _newPasscode->height()); - _newPasscode->moveToLeft(st::boxPadding.left(), _oldPasscode->y() + ((_turningOff || has) ? (_oldPasscode->height() + st::passcodeTextLine + ((_cloudFields.hasRecovery && !_hintText.isEmpty()) ? st::passcodeTextLine : 0)) : 0)); + _newPasscode->moveToLeft(st::boxPadding.left(), _oldPasscode->y() + ((_turningOff || has) ? (_oldPasscode->height() + st::passcodeTextLine + ((_showRecoverLink && !_hintText.isEmpty()) ? st::passcodeTextLine : 0)) : 0)); _reenterPasscode->resize(w, _reenterPasscode->height()); _reenterPasscode->moveToLeft(st::boxPadding.left(), _newPasscode->y() + _newPasscode->height() + st::passcodeLittleSkip); _passwordHint->resize(w, _passwordHint->height()); @@ -379,7 +434,7 @@ void PasscodeBox::setPasswordFail(const QString &type) { _oldPasscode->setFocus(); _oldPasscode->showError(); _oldError = tr::lng_flood_error(tr::now); - if (_cloudFields.hasRecovery && _hintText.isEmpty()) { + if (_showRecoverLink && _hintText.isEmpty()) { _recover->hide(); } update(); @@ -913,7 +968,7 @@ void PasscodeBox::badOldPasscode() { _oldError = _cloudPwd ? tr::lng_cloud_password_wrong(tr::now) : tr::lng_passcode_wrong(tr::now); - if (_cloudFields.hasRecovery && _hintText.isEmpty()) { + if (_showRecoverLink && _hintText.isEmpty()) { _recover->hide(); } update(); @@ -922,7 +977,7 @@ void PasscodeBox::badOldPasscode() { void PasscodeBox::oldChanged() { if (!_oldError.isEmpty()) { _oldError = QString(); - if (_cloudFields.hasRecovery && _hintText.isEmpty()) { + if (_showRecoverLink && _hintText.isEmpty()) { _recover->show(); } update(); @@ -944,7 +999,21 @@ void PasscodeBox::emailChanged() { } void PasscodeBox::recoverByEmail() { - if (_pattern.isEmpty()) { + if (!_cloudFields.hasRecovery) { + const auto session = _session; + const auto confirmBox = std::make_shared>(); + const auto reset = crl::guard(this, [=] { + StartPendingReset(session, this, [=] { + if (const auto box = *confirmBox) { + box->closeBox(); + } + }); + }); + *confirmBox = getDelegate()->show(Box( + tr::lng_cloud_password_reset_no_email(tr::now), + tr::lng_cloud_password_reset_ok(tr::now), + reset)); + } else if (_pattern.isEmpty()) { _pattern = "-"; _api.request(MTPauth_RequestPasswordRecovery( )).done([=](const MTPauth_PasswordRecovery &result) { @@ -964,10 +1033,13 @@ void PasscodeBox::recoverExpired() { void PasscodeBox::recover() { if (_pattern == "-") return; + const auto weak = Ui::MakeWeak(this); const auto box = getDelegate()->show(Box( _session, _pattern, - _cloudFields.notEmptyPassport)); + _cloudFields.notEmptyPassport, + _cloudFields.pendingResetDate != 0, + [weak] { if (weak) { weak->closeBox(); } })); box->passwordCleared( ) | rpl::map_to( @@ -996,11 +1068,37 @@ RecoverBox::RecoverBox( QWidget*, not_null session, const QString &pattern, - bool notEmptyPassport) + bool notEmptyPassport, + bool hasPendingReset, + Fn closeParent) : _api(&session->mtp()) , _pattern(st::normalFont->elided(tr::lng_signin_recover_hint(tr::now, lt_recover_email, pattern), st::boxWidth - st::boxPadding.left() * 1.5)) , _notEmptyPassport(notEmptyPassport) -, _recoverCode(this, st::defaultInputField, tr::lng_signin_code()) { +, _recoverCode(this, st::defaultInputField, tr::lng_signin_code()) +, _noEmailAccess(this, tr::lng_signin_try_password(tr::now)) +, _closeParent(std::move(closeParent)) { + if (hasPendingReset) { + _noEmailAccess.destroy(); + } else { + _noEmailAccess->setClickedCallback([=] { + const auto confirmBox = std::make_shared>(); + const auto reset = crl::guard(this, [=] { + const auto closeParent = _closeParent; + StartPendingReset(session, this, [=] { + if (closeParent) { + closeParent(); + } + if (const auto box = *confirmBox) { + box->closeBox(); + } + }); + }); + *confirmBox = getDelegate()->show(Box( + tr::lng_cloud_password_reset_with_email(tr::now), + tr::lng_cloud_password_reset_ok(tr::now), + reset)); + }); + } } rpl::producer<> RecoverBox::passwordCleared() const { @@ -1017,7 +1115,13 @@ void RecoverBox::prepare() { addButton(tr::lng_passcode_submit(), [=] { submit(); }); addButton(tr::lng_cancel(), [=] { closeBox(); }); - setDimensions(st::boxWidth, st::passcodePadding.top() + st::passcodePadding.bottom() + st::passcodeTextLine + _recoverCode->height() + st::passcodeTextLine); + setDimensions( + st::boxWidth, + (st::passcodePadding.top() + + st::passcodePadding.bottom() + + st::passcodeTextLine + + _recoverCode->height() + + st::passcodeTextLine)); connect(_recoverCode, &Ui::InputField::changed, [=] { codeChanged(); }); connect(_recoverCode, &Ui::InputField::submitted, [=] { submit(); }); @@ -1044,6 +1148,9 @@ void RecoverBox::resizeEvent(QResizeEvent *e) { _recoverCode->resize(st::boxWidth - st::boxPadding.left() - st::boxPadding.right(), _recoverCode->height()); _recoverCode->moveToLeft(st::boxPadding.left(), st::passcodePadding.top() + st::passcodePadding.bottom() + st::passcodeTextLine); + if (_noEmailAccess) { + _noEmailAccess->moveToLeft(st::boxPadding.left(), _recoverCode->y() + _recoverCode->height() + (st::passcodeTextLine - _noEmailAccess->height()) / 2); + } } void RecoverBox::setInnerFocus() { @@ -1085,11 +1192,18 @@ void RecoverBox::submit() { } } -void RecoverBox::codeChanged() { - _error = QString(); +void RecoverBox::setError(const QString &error) { + _error = error; + if (_noEmailAccess) { + _noEmailAccess->setVisible(error.isEmpty()); + } update(); } +void RecoverBox::codeChanged() { + setError(QString()); +} + void RecoverBox::codeSubmitDone(const MTPauth_Authorization &result) { _submitRequest = 0; @@ -1102,8 +1216,7 @@ void RecoverBox::codeSubmitDone(const MTPauth_Authorization &result) { void RecoverBox::codeSubmitFail(const MTP::Error &error) { if (MTP::IsFloodError(error)) { _submitRequest = 0; - _error = tr::lng_flood_error(tr::now); - update(); + setError(tr::lng_flood_error(tr::now)); _recoverCode->showError(); return; } @@ -1121,18 +1234,14 @@ void RecoverBox::codeSubmitFail(const MTP::Error &error) { _recoveryExpired.fire({}); closeBox(); } else if (err == qstr("CODE_INVALID")) { - _error = tr::lng_signin_wrong_code(tr::now); - update(); + setError(tr::lng_signin_wrong_code(tr::now)); _recoverCode->selectAll(); _recoverCode->setFocus(); _recoverCode->showError(); } else { - if (Logs::DebugEnabled()) { // internal server error - _error = err + ": " + error.description(); - } else { - _error = Lang::Hard::ServerError(); - } - update(); + setError(Logs::DebugEnabled() // internal server error + ? (err + ": " + error.description()) + : Lang::Hard::ServerError()); _recoverCode->setFocus(); } } diff --git a/Telegram/SourceFiles/boxes/passcode_box.h b/Telegram/SourceFiles/boxes/passcode_box.h index 875bed383..1d17f5fab 100644 --- a/Telegram/SourceFiles/boxes/passcode_box.h +++ b/Telegram/SourceFiles/boxes/passcode_box.h @@ -39,6 +39,7 @@ public: QString hint; Core::SecureSecretAlgo newSecureSecretAlgo; bool turningOff = false; + TimeId pendingResetDate = 0; // Check cloud password for some action. Fn customCheckCallback; @@ -157,6 +158,7 @@ private: object_ptr _passwordHint; object_ptr _recoverEmail; object_ptr _recover; + bool _showRecoverLink = false; QString _oldError, _newError, _emailError; @@ -172,7 +174,9 @@ public: QWidget*, not_null session, const QString &pattern, - bool notEmptyPassport); + bool notEmptyPassport, + bool hasPendingReset, + Fn closeParent = nullptr); rpl::producer<> passwordCleared() const; rpl::producer<> recoveryExpired() const; @@ -192,6 +196,7 @@ private: void codeChanged(); void codeSubmitDone(const MTPauth_Authorization &result); void codeSubmitFail(const MTP::Error &error); + void setError(const QString &error); MTP::Sender _api; mtpRequestId _submitRequest = 0; @@ -200,6 +205,8 @@ private: bool _notEmptyPassport = false; object_ptr _recoverCode; + object_ptr _noEmailAccess; + Fn _closeParent; QString _error; diff --git a/Telegram/SourceFiles/core/core_cloud_password.cpp b/Telegram/SourceFiles/core/core_cloud_password.cpp index 576285f13..f01b48087 100644 --- a/Telegram/SourceFiles/core/core_cloud_password.cpp +++ b/Telegram/SourceFiles/core/core_cloud_password.cpp @@ -314,6 +314,7 @@ CloudPasswordState ParseCloudPasswordState( ParseSecureSecretAlgo(data.vnew_secure_algo())); result.unconfirmedPattern = qs(data.vemail_unconfirmed_pattern().value_or_empty()); + result.pendingResetDate = data.vpending_reset_date().value_or_empty(); return result; } diff --git a/Telegram/SourceFiles/core/core_cloud_password.h b/Telegram/SourceFiles/core/core_cloud_password.h index 33f9894ef..78e319e7b 100644 --- a/Telegram/SourceFiles/core/core_cloud_password.h +++ b/Telegram/SourceFiles/core/core_cloud_password.h @@ -130,6 +130,7 @@ struct CloudPasswordState { CloudPasswordAlgo newPassword; SecureSecretAlgo newSecureSecret; QString unconfirmedPattern; + TimeId pendingResetDate = 0; }; CloudPasswordState ParseCloudPasswordState( diff --git a/Telegram/SourceFiles/passport/passport_form_controller.cpp b/Telegram/SourceFiles/passport/passport_form_controller.cpp index 4f68164a9..336ef62fe 100644 --- a/Telegram/SourceFiles/passport/passport_form_controller.cpp +++ b/Telegram/SourceFiles/passport/passport_form_controller.cpp @@ -1001,7 +1001,8 @@ void FormController::recoverPassword() { const auto box = _view->show(Box( &_controller->session(), pattern, - _password.notEmptyPassport)); + _password.notEmptyPassport, + _password.pendingResetDate != 0)); box->passwordCleared( ) | rpl::start_with_next([=] { @@ -2636,6 +2637,7 @@ bool FormController::applyPassword(const MTPDaccount_password &result) { Core::ParseCloudPasswordAlgo(result.vnew_algo())); settings.newSecureAlgo = Core::ValidateNewSecureSecretAlgo( Core::ParseSecureSecretAlgo(result.vnew_secure_algo())); + settings.pendingResetDate = result.vpending_reset_date().value_or_empty(); openssl::AddRandomSeed(bytes::make_span(result.vsecure_random().v)); return applyPassword(std::move(settings)); } diff --git a/Telegram/SourceFiles/passport/passport_form_controller.h b/Telegram/SourceFiles/passport/passport_form_controller.h index 74db59cee..29568ed0b 100644 --- a/Telegram/SourceFiles/passport/passport_form_controller.h +++ b/Telegram/SourceFiles/passport/passport_form_controller.h @@ -280,6 +280,7 @@ struct PasswordSettings { bool hasRecovery = false; bool notEmptyPassport = false; bool unknownAlgo = false; + TimeId pendingResetDate = 0; bool operator==(const PasswordSettings &other) const { return (request == other.request) @@ -296,7 +297,8 @@ struct PasswordSettings { && (unconfirmedPattern == other.unconfirmedPattern) && (confirmedEmail == other.confirmedEmail) && (hasRecovery == other.hasRecovery) - && (unknownAlgo == other.unknownAlgo); + && (unknownAlgo == other.unknownAlgo) + && (pendingResetDate == other.pendingResetDate); } bool operator!=(const PasswordSettings &other) const { return !(*this == other); diff --git a/Telegram/SourceFiles/settings/settings_privacy_security.cpp b/Telegram/SourceFiles/settings/settings_privacy_security.cpp index 62d797dc7..0e2f84c8a 100644 --- a/Telegram/SourceFiles/settings/settings_privacy_security.cpp +++ b/Telegram/SourceFiles/settings/settings_privacy_security.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_common.h" #include "settings/settings_privacy_controllers.h" #include "base/timer_rpl.h" +#include "base/unixtime.h" #include "boxes/peer_list_box.h" #include "boxes/edit_privacy_box.h" #include "boxes/passcode_box.h" @@ -359,6 +360,10 @@ void SetupCloudPassword( ) | rpl::then(rpl::duplicate( unconfirmed )); + auto resetAt = session->api().passwordState( + ) | rpl::map([](const State &state) { + return state.pendingResetDate; + }); const auto label = container->add( object_ptr>( container, @@ -464,6 +469,118 @@ void SetupCloudPassword( _1 && !_2)); disable->entity()->addClickHandler(remove); + auto resetInSeconds = rpl::duplicate( + resetAt + ) | rpl::filter([](TimeId time) { + return time != 0; + }) | rpl::map([](TimeId time) { + return rpl::single( + rpl::empty_value() + ) | rpl::then(base::timer_each( + 999 + )) | rpl::map([=] { + const auto now = base::unixtime::now(); + return (time - now); + }) | rpl::distinct_until_changed( + ) | rpl::take_while([](TimeId left) { + return left > 0; + }) | rpl::then(rpl::single(TimeId(0))); + }) | rpl::flatten_latest( + ) | rpl::start_spawning(container->lifetime()); + + auto resetText = rpl::duplicate( + resetInSeconds + ) | rpl::map([](TimeId left) { + return (left > 0); + }) | rpl::distinct_until_changed( + ) | rpl::map([](bool waiting) { + return waiting + ? tr::lng_cloud_password_reset_in() + : tr::lng_cloud_password_reset_ready(); + }) | rpl::flatten_latest(); + + constexpr auto kMinute = 60; + constexpr auto kHour = 3600; + constexpr auto kDay = 86400; + auto resetLabel = rpl::duplicate( + resetInSeconds + ) | rpl::map([](TimeId left) { + return (left >= kDay) + ? ((left / kDay) * kDay) + : (left >= kHour) + ? ((left / kHour) * kHour) + : (left >= kMinute) + ? ((left / kMinute) * kMinute) + : left; + }) | rpl::distinct_until_changed( + ) | rpl::map([](TimeId left) { + const auto days = left / kDay; + const auto hours = left / kHour; + const auto minutes = left / kMinute; + return days + ? tr::lng_group_call_duration_days(tr::now, lt_count, days) + : hours + ? tr::lng_group_call_duration_hours(tr::now, lt_count, hours) + : minutes + ? tr::lng_group_call_duration_minutes(tr::now, lt_count, minutes) + : left + ? tr::lng_group_call_duration_seconds(tr::now, lt_count, left) + : QString(); + }); + + const auto reset = container->add( + object_ptr>( + container, + object_ptr