diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt
index c2a6e1b95..5b11282a3 100644
--- a/Telegram/CMakeLists.txt
+++ b/Telegram/CMakeLists.txt
@@ -1092,6 +1092,8 @@ PRIVATE
settings/settings_information.h
settings/settings_intro.cpp
settings/settings_intro.h
+ settings/settings_local_passcode.cpp
+ settings/settings_local_passcode.h
settings/settings_main.cpp
settings/settings_main.h
settings/settings_notifications.cpp
diff --git a/Telegram/Resources/animations/local_passcode_enter.tgs b/Telegram/Resources/animations/local_passcode_enter.tgs
new file mode 100644
index 000000000..e46c05c6e
Binary files /dev/null and b/Telegram/Resources/animations/local_passcode_enter.tgs differ
diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 19f288c59..6e45bd766 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -647,6 +647,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_passcode_enter_new" = "Enter new passcode";
"lng_passcode_confirm_new" = "Re-enter new passcode";
"lng_passcode_about" = "When a local passcode is set, a lock icon appears at the top of your chats list. Click it to lock the app.\n\nNote: if you forget your local passcode, you'll need to relogin in Telegram Desktop.";
+"lng_passcode_about1" = "When a local passcode is set, a lock icon appears at the top of your chats list.";
+"lng_passcode_about2" = "Click it to lock Telegram Desktop.";
+"lng_passcode_about3" = "Note: if you forget your passcode, you'll need to log out of Telegram Desktop and log in again.";
"lng_passcode_differ" = "Passcodes are different";
"lng_passcode_wrong" = "Wrong passcode";
"lng_passcode_is_same" = "Passcode was not changed";
@@ -655,6 +658,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_passcode_submit" = "Submit";
"lng_passcode_logout" = "Log out";
"lng_passcode_need_unblock" = "You need to unlock me first.";
+"lng_passcode_create_button" = "Save Passcode";
+"lng_passcode_check_button" = "Submit";
+"lng_passcode_change_button" = "Save Passcode";
+"lng_passcode_create_title" = "Create Local Passcode";
+"lng_passcode_check_title" = "Enter Passcode";
+"lng_passcode_change_title" = "Enter Passcode";
"lng_cloud_password_waiting_code" = "Confirmation code sent to {email}...";
"lng_cloud_password_confirm" = "Confirm recovery email";
diff --git a/Telegram/Resources/qrc/telegram/animations.qrc b/Telegram/Resources/qrc/telegram/animations.qrc
index f228e20b0..abb233167 100644
--- a/Telegram/Resources/qrc/telegram/animations.qrc
+++ b/Telegram/Resources/qrc/telegram/animations.qrc
@@ -3,5 +3,6 @@
../../animations/change_number.tgs
../../animations/blocked_peers_empty.tgs
../../animations/filters.tgs
+ ../../animations/local_passcode_enter.tgs
diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style
index 0ec8e9ea3..90336e8ba 100644
--- a/Telegram/SourceFiles/settings/settings.style
+++ b/Telegram/SourceFiles/settings/settings.style
@@ -142,6 +142,19 @@ settingsCloudPasswordLabel: FlatLabel(defaultFlatLabel) {
}
settingsCloudPasswordLabelPadding: margins(22px, 8px, 10px, 8px);
+settingLocalPasscodeInputField: InputField(defaultInputField) {
+ width: 256px;
+}
+settingLocalPasscodeDescription: FlatLabel(changePhoneDescription) {
+ minWidth: 256px;
+}
+settingLocalPasscodeError: FlatLabel(changePhoneError) {
+ minWidth: 256px;
+}
+settingLocalPasscodeDescriptionBottomSkip: 15px;
+settingLocalPasscodeIconPadding: margins(0px, 19px, 0px, 5px);
+settingLocalPasscodeButtonPadding: margins(0px, 19px, 0px, 15px);
+
settingsInfoPhotoHeight: 161px;
settingsInfoPhotoSize: 100px;
settingsInfoPhoto: UserpicButton(defaultUserpicButton) {
diff --git a/Telegram/SourceFiles/settings/settings_local_passcode.cpp b/Telegram/SourceFiles/settings/settings_local_passcode.cpp
new file mode 100644
index 000000000..505625b28
--- /dev/null
+++ b/Telegram/SourceFiles/settings/settings_local_passcode.cpp
@@ -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
+*/
+#include "settings/settings_local_passcode.h"
+
+#include "lang/lang_keys.h"
+#include "lottie/lottie_icon.h"
+#include "main/main_domain.h"
+#include "main/main_session.h"
+#include "storage/storage_domain.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_settings.h"
+#include "styles/style_boxes.h"
+
+namespace Settings {
+namespace details {
+
+LocalPasscodeEnter::LocalPasscodeEnter(
+ QWidget *parent,
+ not_null controller)
+: AbstractSection(parent)
+, _controller(controller) {
+}
+
+rpl::producer LocalPasscodeEnter::title() {
+ return tr::lng_settings_passcode_title();
+}
+
+void LocalPasscodeEnter::setupContent() {
+ const auto content = Ui::CreateChild(this);
+
+ const auto isCreate = (enterType() == EnterType::Create);
+ const auto isCheck = (enterType() == EnterType::Check);
+
+ auto icon = CreateLottieIcon(
+ content,
+ {
+ .name = u"local_passcode_enter"_q,
+ .sizeOverride = {
+ st::changePhoneIconSize,
+ st::changePhoneIconSize,
+ },
+ },
+ st::settingLocalPasscodeIconPadding);
+ content->add(std::move(icon.widget));
+ _showFinished.events(
+ ) | rpl::start_with_next([animate = std::move(icon.animate)] {
+ animate(anim::repeat::once);
+ }, content->lifetime());
+
+ AddSkip(content);
+
+ content->add(
+ object_ptr>(
+ content,
+ object_ptr(
+ content,
+ isCreate
+ ? tr::lng_passcode_create_title()
+ : isCheck
+ ? tr::lng_passcode_check_title()
+ : tr::lng_passcode_change_title(),
+ st::changePhoneTitle)),
+ st::changePhoneTitlePadding);
+
+ const auto addDescription = [&](rpl::producer &&text) {
+ const auto &st = st::settingLocalPasscodeDescription;
+ content->add(
+ object_ptr>(
+ content,
+ object_ptr(content, std::move(text), st)),
+ st::changePhoneDescriptionPadding);
+ };
+
+ addDescription(tr::lng_passcode_about1());
+ AddSkip(content);
+ addDescription(tr::lng_passcode_about2());
+
+ AddSkip(content, st::settingLocalPasscodeDescriptionBottomSkip);
+
+ const auto addField = [&](rpl::producer &&text) {
+ const auto &st = st::settingLocalPasscodeInputField;
+ auto container = object_ptr(content);
+ container->resize(container->width(), st.heightMin);
+ const auto field = Ui::CreateChild(
+ container.data(),
+ st,
+ std::move(text));
+
+ container->geometryValue(
+ ) | rpl::start_with_next([=](const QRect &r) {
+ field->moveToLeft((r.width() - field->width()) / 2, 0);
+ }, container->lifetime());
+
+ content->add(std::move(container));
+ return field;
+ };
+
+ const auto addError = [&](not_null input) {
+ const auto error = content->add(
+ object_ptr>(
+ content,
+ object_ptr(
+ content,
+ // Set any text to resize.
+ tr::lng_language_name(tr::now),
+ st::settingLocalPasscodeError)),
+ st::changePhoneDescriptionPadding)->entity();
+ error->hide();
+ QObject::connect(input.get(), &Ui::MaskedInputField::changed, [=] {
+ error->hide();
+ });
+ return error;
+ };
+
+ const auto newPasscode = addField(tr::lng_passcode_enter_first());
+
+ const auto reenterPasscode = isCheck
+ ? (Ui::PasswordInput*)(nullptr)
+ : addField(tr::lng_passcode_confirm_new());
+ const auto reenterError = isCheck
+ ? (Ui::FlatLabel*)(nullptr)
+ : addError(reenterPasscode);
+
+ const auto button = content->add(
+ object_ptr>(
+ content,
+ object_ptr(
+ content,
+ isCreate
+ ? tr::lng_passcode_create_button()
+ : isCheck
+ ? tr::lng_passcode_check_button()
+ : tr::lng_passcode_change_button(),
+ st::changePhoneButton)),
+ st::settingLocalPasscodeButtonPadding)->entity();
+ button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
+ button->setClickedCallback([=] {
+ const auto newText = newPasscode->text();
+ const auto reenterText = reenterPasscode
+ ? reenterPasscode->text()
+ : QString();
+ if (isCreate) {
+ if (newText.isEmpty()) {
+ newPasscode->setFocus();
+ newPasscode->showError();
+ } else if (reenterText.isEmpty()) {
+ reenterPasscode->setFocus();
+ reenterPasscode->showError();
+ } else if (newText != reenterText) {
+ reenterPasscode->setFocus();
+ reenterPasscode->showError();
+ reenterPasscode->selectAll();
+ reenterError->show();
+ reenterError->setText(tr::lng_passcode_differ(tr::now));
+ } else {
+ // showOther
+ }
+ }
+ });
+
+ _setInnerFocus.events(
+ ) | rpl::start_with_next([=] {
+ if (newPasscode->text().isEmpty()) {
+ newPasscode->setFocus();
+ } else if (reenterPasscode && reenterPasscode->text().isEmpty()) {
+ reenterPasscode->setFocus();
+ } else {
+ newPasscode->setFocus();
+ }
+ }, content->lifetime());
+
+ Ui::ResizeFitChild(this, content);
+}
+
+void LocalPasscodeEnter::showFinished() {
+ _showFinished.fire({});
+}
+
+void LocalPasscodeEnter::setInnerFocus() {
+ _setInnerFocus.fire({});
+}
+
+LocalPasscodeEnter::~LocalPasscodeEnter() = default;
+
+} // namespace details
+} // namespace Settings
diff --git a/Telegram/SourceFiles/settings/settings_local_passcode.h b/Telegram/SourceFiles/settings/settings_local_passcode.h
new file mode 100644
index 000000000..ad12e1ee4
--- /dev/null
+++ b/Telegram/SourceFiles/settings/settings_local_passcode.h
@@ -0,0 +1,108 @@
+/*
+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 "settings/settings_common.h"
+
+namespace Settings {
+namespace details {
+
+class LocalPasscodeEnter : public AbstractSection {
+public:
+ enum class EnterType {
+ Create,
+ Check,
+ Change,
+ };
+
+ LocalPasscodeEnter(
+ QWidget *parent,
+ not_null controller);
+ ~LocalPasscodeEnter();
+
+ void showFinished() override;
+ void setInnerFocus() override;
+
+ [[nodiscard]] rpl::producer title() override;
+
+protected:
+ void setupContent();
+
+ [[nodiscard]] virtual EnterType enterType() const = 0;
+
+private:
+
+ const not_null _controller;
+
+ rpl::event_stream<> _showFinished;
+ rpl::event_stream<> _setInnerFocus;
+
+};
+
+} // namespace details
+
+class LocalPasscodeCreate;
+class LocalPasscodeCheck;
+class LocalPasscodeChange;
+
+template
+class TypedLocalPasscodeEnter : public details::LocalPasscodeEnter {
+public:
+ TypedLocalPasscodeEnter(
+ QWidget *parent,
+ not_null controller)
+ : details::LocalPasscodeEnter(parent, controller) {
+ setupContent();
+ }
+
+ [[nodiscard]] static Type Id() {
+ return &SectionMetaImplementation::Meta;
+ }
+ [[nodiscard]] Type id() const final override {
+ return Id();
+ }
+
+protected:
+ [[nodiscard]] EnterType enterType() const final override {
+ if constexpr (std::is_same_v) {
+ return EnterType::Create;
+ }
+ if constexpr (std::is_same_v) {
+ return EnterType::Check;
+ }
+ if constexpr (std::is_same_v) {
+ return EnterType::Change;
+ }
+ return EnterType::Create;
+ }
+
+};
+
+class LocalPasscodeCreate final
+ : public TypedLocalPasscodeEnter {
+public:
+ using TypedLocalPasscodeEnter::TypedLocalPasscodeEnter;
+
+};
+
+class LocalPasscodeCheck final
+ : public TypedLocalPasscodeEnter {
+public:
+ using TypedLocalPasscodeEnter::TypedLocalPasscodeEnter;
+
+};
+
+class LocalPasscodeChange final
+ : public TypedLocalPasscodeEnter {
+public:
+ using TypedLocalPasscodeEnter::TypedLocalPasscodeEnter;
+
+};
+
+} // namespace Settings
+