tdesktop/Telegram/SourceFiles/settings/cloud_password/settings_cloud_password_inp...

581 lines
16 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
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 "settings/cloud_password/settings_cloud_password_input.h"
#include "api/api_cloud_password.h"
#include "base/qt_signal_producer.h"
#include "base/timer.h"
#include "base/unixtime.h"
#include "core/core_cloud_password.h"
#include "lang/lang_keys.h"
#include "lottie/lottie_icon.h"
#include "settings/cloud_password/settings_cloud_password_common.h"
#include "settings/cloud_password/settings_cloud_password_email_confirm.h"
#include "settings/cloud_password/settings_cloud_password_hint.h"
#include "settings/cloud_password/settings_cloud_password_manage.h"
#include "ui/boxes/confirm_box.h"
#include "ui/text/format_values.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/input_fields.h"
#include "ui/widgets/labels.h"
#include "ui/wrap/vertical_layout.h"
#include "window/window_session_controller.h"
#include "styles/style_boxes.h"
#include "styles/style_layers.h"
#include "styles/style_settings.h"
/*
Available actions for follow states.
CreatePassword:
Continue to CreateHint.
Back to Start.
ChangePassword:
Continue to ChangeHint.
Back to Manage.
CheckPassword:
Continue to Manage.
Recover to EmailConfirm.
Reset and wait (+ Cancel reset).
Reset now and Back to Settings.
Back to Settings.
RecreateResetPassword:
Continue to RecreateResetHint.
Clear password and Back to Settings.
Back to Settings.
*/
namespace Settings {
namespace CloudPassword {
namespace {
struct Icon {
not_null<Lottie::Icon*> icon;
Fn<void()> update;
};
Icon CreateInteractiveLottieIcon(
not_null<Ui::VerticalLayout*> container,
Lottie::IconDescriptor &&descriptor,
style::margins padding) {
auto object = object_ptr<Ui::RpWidget>(container);
const auto raw = object.data();
const auto width = descriptor.sizeOverride.width();
raw->resize(QRect(
QPoint(),
descriptor.sizeOverride).marginsAdded(padding).size());
auto owned = Lottie::MakeIcon(std::move(descriptor));
const auto icon = owned.get();
raw->lifetime().add([kept = std::move(owned)]{});
raw->paintRequest(
) | rpl::start_with_next([=] {
auto p = QPainter(raw);
const auto left = (raw->width() - width) / 2;
icon->paint(p, left, padding.top());
}, raw->lifetime());
container->add(std::move(object));
return { .icon = icon, .update = [=] { raw->update(); } };
}
[[nodiscard]] not_null<Ui::LinkButton*> AddLinkButton(
not_null<Ui::VerticalLayout*> content,
not_null<Ui::PasswordInput*> input) {
const auto button = Ui::CreateChild<Ui::LinkButton>(
content.get(),
QString());
rpl::merge(
content->geometryValue(),
input->geometryValue()
) | rpl::start_with_next([=] {
const auto topLeft = input->mapTo(content, input->pos());
button->moveToLeft(
input->pos().x(),
topLeft.y() + input->height() + st::passcodeTextLine);
}, button->lifetime());
return button;
}
} // namespace
class Input : public TypedAbstractStep<Input> {
public:
using TypedAbstractStep::TypedAbstractStep;
[[nodiscard]] rpl::producer<QString> title() override;
void setupContent();
protected:
[[nodiscard]] rpl::producer<std::vector<Type>> removeTypes() override;
private:
void setupRecoverButton(
not_null<Ui::VerticalLayout*> container,
not_null<Ui::LinkButton*> button,
not_null<Ui::FlatLabel*> info,
Fn<void()> recoverCallback);
rpl::variable<std::vector<Type>> _removesFromStack;
rpl::lifetime _requestLifetime;
};
rpl::producer<std::vector<Type>> Input::removeTypes() {
return _removesFromStack.value();
}
rpl::producer<QString> Input::title() {
return tr::lng_settings_cloud_password_password_title();
}
void Input::setupContent() {
const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
auto currentStepData = stepData();
const auto currentStepDataPassword = base::take(currentStepData.password);
const auto currentStepProcessRecover = base::take(
currentStepData.processRecover);
setStepData(currentStepData);
const auto currentState = cloudPassword().stateCurrent();
const auto hasPassword = !currentStepProcessRecover.setNewPassword
&& (currentState ? currentState->hasPassword : false);
const auto isCheck = currentStepData.currentPassword.isEmpty()
&& hasPassword
&& !currentStepProcessRecover.setNewPassword;
if (currentStepProcessRecover.setNewPassword) {
_removesFromStack = std::vector<Type>{
CloudPasswordEmailConfirmId()
};
}
const auto icon = CreateInteractiveLottieIcon(
content,
{
.name = u"cloud_password/password_input"_q,
.sizeOverride = {
st::settingsCloudPasswordIconSize,
st::settingsCloudPasswordIconSize
},
},
st::settingLocalPasscodeIconPadding);
SetupHeader(
content,
QString(),
rpl::never<>(),
isCheck
? tr::lng_settings_cloud_password_check_subtitle()
: hasPassword
? tr::lng_settings_cloud_password_manage_password_change()
: tr::lng_settings_cloud_password_password_subtitle(),
tr::lng_cloud_password_about());
AddSkip(content, st::settingLocalPasscodeDescriptionBottomSkip);
const auto newInput = AddPasswordField(
content,
isCheck
? tr::lng_cloud_password_enter_old()
: tr::lng_cloud_password_enter_new(),
currentStepDataPassword);
const auto reenterInput = isCheck
? (Ui::PasswordInput*)(nullptr)
: AddPasswordField(
content,
tr::lng_cloud_password_confirm_new(),
currentStepDataPassword).get();
const auto error = AddError(content, newInput);
if (reenterInput) {
QObject::connect(reenterInput, &Ui::MaskedInputField::changed, [=] {
error->hide();
});
}
if (isCheck) {
AddSkipInsteadOfField(content);
const auto hint = currentState ? currentState->hint : QString();
const auto hintInfo = Ui::CreateChild<Ui::FlatLabel>(
error->parentWidget(),
tr::lng_signin_hint(tr::now, lt_password_hint, hint),
st::defaultFlatLabel);
hintInfo->setVisible(!hint.isEmpty());
error->geometryValue(
) | rpl::start_with_next([=](const QRect &r) {
hintInfo->setGeometry(r);
}, hintInfo->lifetime());
error->shownValue(
) | rpl::start_with_next([=](bool shown) {
if (shown) {
hintInfo->hide();
} else {
hintInfo->setVisible(!hint.isEmpty());
}
}, hintInfo->lifetime());
auto recoverCallback = [=] {
if (_requestLifetime) {
return;
}
const auto state = cloudPassword().stateCurrent();
if (!state) {
return;
}
if (state->hasRecovery) {
_requestLifetime = cloudPassword().requestPasswordRecovery(
) | rpl::start_with_next_error([=](const QString &pattern) {
_requestLifetime.destroy();
auto data = stepData();
data.processRecover = currentStepProcessRecover;
data.processRecover.emailPattern = pattern;
setStepData(std::move(data));
showOther(CloudPasswordEmailConfirmId());
}, [=](const QString &type) {
_requestLifetime.destroy();
error->show();
if (MTP::IsFloodError(type)) {
error->setText(tr::lng_flood_error(tr::now));
} else {
error->setText(Lang::Hard::ServerError());
}
});
} else {
const auto callback = [=](Fn<void()> close) {
if (_requestLifetime) {
return;
}
close();
_requestLifetime = cloudPassword().resetPassword(
) | rpl::start_with_error_done([=](const QString &type) {
_requestLifetime.destroy();
}, [=] {
_requestLifetime.destroy();
});
};
controller()->show(Ui::MakeConfirmBox({
.text = tr::lng_cloud_password_reset_no_email(),
.confirmed = callback,
.confirmText = tr::lng_cloud_password_reset_ok(),
.cancelText = tr::lng_cancel(),
.confirmStyle = &st::attentionBoxButton,
}));
}
};
const auto recover = AddLinkButton(content, newInput);
const auto resetInfo = Ui::CreateChild<Ui::FlatLabel>(
content,
QString(),
st::boxDividerLabel);
recover->geometryValue(
) | rpl::start_with_next([=](const QRect &r) {
resetInfo->moveToLeft(r.x(), r.y() + st::passcodeTextLine);
}, resetInfo->lifetime());
setupRecoverButton(
content,
recover,
resetInfo,
std::move(recoverCallback));
} else if (currentStepProcessRecover.setNewPassword && reenterInput) {
const auto skip = AddLinkButton(content, reenterInput);
skip->setText(tr::lng_settings_auto_night_disable(tr::now));
skip->setClickedCallback([=] {
if (_requestLifetime) {
return;
}
_requestLifetime = cloudPassword().recoverPassword(
currentStepProcessRecover.checkedCode,
QString(),
QString()
) | rpl::start_with_error_done([=](const QString &type) {
_requestLifetime.destroy();
error->show();
if (MTP::IsFloodError(type)) {
error->setText(tr::lng_flood_error(tr::now));
} else {
error->setText(Lang::Hard::ServerError());
}
}, [=] {
_requestLifetime.destroy();
controller()->show(
Ui::MakeInformBox(tr::lng_cloud_password_removed()));
setStepData(StepData());
showBack();
});
});
AddSkip(content);
}
if (!newInput->text().isEmpty()) {
icon.icon->jumpTo(icon.icon->framesCount() / 2, icon.update);
}
const auto checkPassword = [=](const QString &pass) {
if (_requestLifetime) {
return;
}
_requestLifetime = cloudPassword().check(
pass
) | rpl::start_with_error_done([=](const QString &type) {
_requestLifetime.destroy();
newInput->setFocus();
newInput->showError();
newInput->selectAll();
error->show();
if (MTP::IsFloodError(type)) {
error->setText(tr::lng_flood_error(tr::now));
} else if (type == u"PASSWORD_HASH_INVALID"_q
|| type == u"SRP_PASSWORD_CHANGED"_q) {
error->setText(tr::lng_cloud_password_wrong(tr::now));
} else {
error->setText(Lang::Hard::ServerError());
}
}, [=] {
_requestLifetime.destroy();
if (const auto state = cloudPassword().stateCurrent()) {
if (state->pendingResetDate > 0) {
auto lifetime = rpl::lifetime();
lifetime = cloudPassword().cancelResetPassword(
) | rpl::start_with_next([] {});
}
}
auto data = stepData();
data.currentPassword = pass;
setStepData(std::move(data));
showOther(CloudPasswordManageId());
});
};
const auto button = AddDoneButton(
content,
isCheck ? tr::lng_passcode_check_button() : tr::lng_continue());
button->setClickedCallback([=] {
const auto newText = newInput->text();
const auto reenterText = isCheck ? QString() : reenterInput->text();
if (newText.isEmpty()) {
newInput->setFocus();
newInput->showError();
} else if (reenterInput && reenterText.isEmpty()) {
reenterInput->setFocus();
reenterInput->showError();
} else if (reenterInput && (newText != reenterText)) {
reenterInput->setFocus();
reenterInput->showError();
reenterInput->selectAll();
error->show();
error->setText(tr::lng_cloud_password_differ(tr::now));
} else if (isCheck) {
checkPassword(newText);
} else {
auto data = stepData();
data.processRecover = currentStepProcessRecover;
data.password = newText;
setStepData(std::move(data));
showOther(CloudPasswordHintId());
}
});
base::qt_signal_producer(
newInput.get(),
&QLineEdit::textChanged // Covers Undo.
) | rpl::map([=] {
return newInput->text().isEmpty();
}) | rpl::distinct_until_changed(
) | rpl::start_with_next([=](bool empty) {
const auto from = icon.icon->frameIndex();
const auto to = empty ? 0 : (icon.icon->framesCount() / 2 - 1);
icon.icon->animate(icon.update, from, to);
}, content->lifetime());
const auto submit = [=] {
if (!reenterInput || reenterInput->hasFocus()) {
button->clicked({}, Qt::LeftButton);
} else {
reenterInput->setFocus();
}
};
QObject::connect(newInput, &Ui::MaskedInputField::submitted, submit);
if (reenterInput) {
using namespace Ui;
QObject::connect(reenterInput, &MaskedInputField::submitted, submit);
}
setFocusCallback([=] {
if (isCheck || newInput->text().isEmpty()) {
newInput->setFocus();
} else if (reenterInput->text().isEmpty()) {
reenterInput->setFocus();
} else {
newInput->setFocus();
}
});
Ui::ResizeFitChild(this, content);
}
void Input::setupRecoverButton(
not_null<Ui::VerticalLayout*> container,
not_null<Ui::LinkButton*> button,
not_null<Ui::FlatLabel*> info,
Fn<void()> recoverCallback) {
struct Status {
enum class SuggestAction {
Recover,
Reset,
CancelReset,
};
SuggestAction suggest = SuggestAction::Recover;
TimeId left = 0;
};
struct State {
base::Timer timer;
rpl::variable<Status> status;
};
const auto state = container->lifetime().make_state<State>();
const auto updateStatus = [=] {
const auto passwordState = cloudPassword().stateCurrent();
const auto date = passwordState ? passwordState->pendingResetDate : 0;
const auto left = (date - base::unixtime::now());
state->status = Status{
.suggest = ((left > 0)
? Status::SuggestAction::CancelReset
: date
? Status::SuggestAction::Reset
: Status::SuggestAction::Recover),
.left = left,
};
};
state->timer.setCallback(updateStatus);
updateStatus();
state->status.value(
) | rpl::start_with_next([=](const Status &status) {
switch (status.suggest) {
case Status::SuggestAction::Recover: {
info->setText(QString());
button->setText(tr::lng_signin_recover(tr::now));
} break;
case Status::SuggestAction::Reset: {
info->setText(QString());
button->setText(tr::lng_cloud_password_reset_ready(tr::now));
} break;
case Status::SuggestAction::CancelReset: {
info->setText(
tr::lng_settings_cloud_password_reset_in(
tr::now,
lt_duration,
Ui::FormatResetCloudPasswordIn(status.left)));
button->setText(
tr::lng_cloud_password_reset_cancel_title(tr::now));
} break;
}
}, container->lifetime());
cloudPassword().state(
) | rpl::start_with_next([=](const Core::CloudPasswordState &passState) {
updateStatus();
state->timer.cancel();
if (passState.pendingResetDate) {
state->timer.callEach(999);
}
}, container->lifetime());
button->setClickedCallback([=] {
const auto passState = cloudPassword().stateCurrent();
if (_requestLifetime || !passState) {
return;
}
updateStatus();
const auto suggest = state->status.current().suggest;
if (suggest == Status::SuggestAction::Recover) {
recoverCallback();
} else if (suggest == Status::SuggestAction::CancelReset) {
const auto cancel = [=](Fn<void()> close) {
if (_requestLifetime) {
return;
}
close();
_requestLifetime = cloudPassword().cancelResetPassword(
) | rpl::start_with_error_done([=](const QString &error) {
_requestLifetime.destroy();
}, [=] {
_requestLifetime.destroy();
});
};
controller()->show(Ui::MakeConfirmBox({
.text = tr::lng_cloud_password_reset_cancel_sure(),
.confirmed = cancel,
.confirmText = tr::lng_box_yes(),
.cancelText = tr::lng_box_no(),
}));
} else if (suggest == Status::SuggestAction::Reset) {
_requestLifetime = cloudPassword().resetPassword(
) | rpl::start_with_next_error_done([=](
Api::CloudPassword::ResetRetryDate retryDate) {
_requestLifetime.destroy();
const auto left = std::max(
retryDate - base::unixtime::now(),
60);
controller()->show(Ui::MakeInformBox(
tr::lng_cloud_password_reset_later(
tr::now,
lt_duration,
Ui::FormatResetCloudPasswordIn(left))));
}, [=](const QString &type) {
_requestLifetime.destroy();
}, [=] {
_requestLifetime.destroy();
cloudPassword().reload();
using PasswordState = Core::CloudPasswordState;
_requestLifetime = cloudPassword().state(
) | rpl::filter([=](const PasswordState &s) {
return !s.hasPassword;
}) | rpl::take(
1
) | rpl::start_with_next([=](const PasswordState &s) {
_requestLifetime.destroy();
controller()->show(Ui::MakeInformBox(
tr::lng_cloud_password_removed()));
setStepData(StepData());
showBack();
});
});
}
});
}
} // namespace CloudPassword
Type CloudPasswordInputId() {
return CloudPassword::Input::Id();
}
} // namespace Settings