/* 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/builder/settings_privacy_security_builder.h" #include "api/api_authorizations.h" #include "api/api_blocked_peers.h" #include "api/api_cloud_password.h" #include "api/api_global_privacy.h" #include "api/api_self_destruct.h" #include "api/api_sensitive_content.h" #include "api/api_user_privacy.h" #include "api/api_websites.h" #include "apiwrap.h" #include "main/main_app_config.h" #include "ui/chat/chat_style.h" #include "base/timer_rpl.h" #include "boxes/edit_privacy_box.h" #include "boxes/self_destruction_box.h" #include "core/application.h" #include "core/core_cloud_password.h" #include "core/core_settings.h" #include "data/components/passkeys.h" #include "data/components/top_peers.h" #include "data/data_peer_values.h" #include "lang/lang_keys.h" #include "main/main_domain.h" #include "main/main_session.h" #include "platform/platform_webauthn.h" #include "settings/builder/settings_builder.h" #include "settings/cloud_password/settings_cloud_password_email_confirm.h" #include "settings/cloud_password/settings_cloud_password_input.h" #include "settings/cloud_password/settings_cloud_password_start.h" #include "settings/settings_active_sessions.h" #include "settings/settings_blocked_peers.h" #include "settings/settings_global_ttl.h" #include "settings/settings_local_passcode.h" #include "settings/settings_main.h" #include "settings/settings_passkeys.h" #include "settings/settings_privacy_controllers.h" #include "settings/settings_privacy_security.h" #include "settings/settings_websites.h" #include "storage/storage_domain.h" #include "ui/layers/generic_box.h" #include "ui/text/format_values.h" #include "ui/vertical_list.h" #include "ui/widgets/buttons.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" #include "window/window_session_controller.h" #include "styles/style_menu_icons.h" #include "styles/style_settings.h" namespace Settings::Builder { namespace { constexpr auto kUpdateTimeout = 60 * crl::time(1000); using Privacy = Api::UserPrivacy; void BuildSecuritySection( SectionBuilder &builder, rpl::producer<> updateTrigger) { const auto controller = builder.controller(); const auto showOther = builder.showOther(); const auto session = builder.session(); builder.addSkip(st::settingsPrivacySkip); builder.addSubsectionTitle({ .id = u"security/section"_q, .title = tr::lng_settings_security(), .keywords = { u"security"_q, u"password"_q, u"passcode"_q }, }); using State = Core::CloudPasswordState; enum class PasswordState { Loading, On, Off, Unconfirmed, }; auto passwordState = rpl::single( PasswordState::Loading ) | rpl::then(session->api().cloudPassword().state( ) | rpl::map([](const State &state) { return (!state.unconfirmedPattern.isEmpty()) ? PasswordState::Unconfirmed : state.hasPassword ? PasswordState::On : PasswordState::Off; })) | rpl::distinct_until_changed(); auto cloudPasswordLabel = rpl::duplicate( passwordState ) | rpl::map([=](PasswordState state) { return (state == PasswordState::Loading) ? tr::lng_profile_loading(tr::now) : (state == PasswordState::On) ? tr::lng_settings_cloud_password_on(tr::now) : tr::lng_settings_cloud_password_off(tr::now); }); builder.addButton({ .id = u"security/cloud_password"_q, .title = tr::lng_settings_cloud_password_start_title(), .icon = { &st::menuIcon2SV }, .label = std::move(cloudPasswordLabel), .onClick = [=, passwordState = base::duplicate(passwordState)] { const auto state = rpl::variable( base::duplicate(passwordState)).current(); if (state == PasswordState::Loading) { return; } else if (state == PasswordState::On) { showOther(CloudPasswordInputId()); } else if (state == PasswordState::Off) { showOther(CloudPasswordStartId()); } else if (state == PasswordState::Unconfirmed) { showOther(CloudPasswordEmailConfirmId()); } }, .keywords = { u"password"_q, u"2fa"_q, u"two-factor"_q }, }); session->api().cloudPassword().reload(); auto ttlLabel = rpl::combine( session->api().selfDestruct().periodDefaultHistoryTTL(), tr::lng_settings_ttl_after_off() ) | rpl::map([](int ttl, const QString &none) { return ttl ? Ui::FormatTTL(ttl) : none; }); builder.addButton({ .id = u"security/ttl"_q, .title = tr::lng_settings_ttl_title(), .icon = { &st::menuIconTTL }, .label = std::move(ttlLabel), .onClick = [showOther] { showOther(GlobalTTLId()); }, .keywords = { u"ttl"_q, u"auto-delete"_q, u"timer"_q }, }); builder.add([session, updateTrigger = rpl::duplicate(updateTrigger)](const WidgetContext &ctx) mutable { std::move(updateTrigger) | rpl::on_next([=] { session->api().selfDestruct().reload(); }, ctx.container->lifetime()); return SectionBuilder::WidgetToAdd{}; }); auto passcodeHas = rpl::single(rpl::empty) | rpl::then( session->domain().local().localPasscodeChanged() ) | rpl::map([=] { return session->domain().local().hasLocalPasscode(); }); auto passcodeLabel = rpl::combine( tr::lng_settings_cloud_password_on(), tr::lng_settings_cloud_password_off(), rpl::duplicate(passcodeHas) ) | rpl::map([](const QString &on, const QString &off, bool has) { return has ? on : off; }); builder.addButton({ .id = u"security/passcode"_q, .title = tr::lng_settings_passcode_title(), .icon = { &st::menuIconLock }, .label = std::move(passcodeLabel), .onClick = [=, passcodeHas = std::move(passcodeHas)]() mutable { if (rpl::variable(std::move(passcodeHas)).current()) { showOther(LocalPasscodeCheckId()); } else { showOther(LocalPasscodeCreateId()); } }, .keywords = { u"passcode"_q, u"lock"_q, u"pin"_q }, }); if (session->passkeys().possible()) { auto passkeysLabel = rpl::combine( tr::lng_profile_loading(), (rpl::single(rpl::empty_value()) | rpl::then(session->passkeys().requestList())) | rpl::map([=] { return session->passkeys().list().size(); }) ) | rpl::map([=](const QString &loading, int count) { return !session->passkeys().listKnown() ? loading : count == 1 ? session->passkeys().list().front().name : count ? QString::number(count) : tr::lng_settings_cloud_password_off(tr::now); }); auto passkeysShown = (rpl::single(rpl::empty_value()) | rpl::then(session->passkeys().requestList())) | rpl::map([=] { return Platform::WebAuthn::IsSupported() || !session->passkeys().list().empty(); }); builder.addButton({ .id = u"security/passkeys"_q, .title = tr::lng_settings_passkeys_title(), .icon = { &st::menuIconPermissions }, .label = std::move(passkeysLabel), .onClick = [=] { if (!session->passkeys().listKnown()) { return; } const auto count = session->passkeys().list().size(); if (count == 0) { controller->show(Box([=](not_null box) { PasskeysNoneBox(box, session); box->boxClosing() | rpl::on_next([=] { if (session->passkeys().list().size()) { controller->showSettings(PasskeysId()); } }, box->lifetime()); })); } else { controller->showSettings(PasskeysId()); } }, .keywords = { u"passkeys"_q, u"biometric"_q }, .shown = std::move(passkeysShown), }); } auto blockedCount = rpl::combine( session->api().blockedPeers().slice( ) | rpl::map([](const Api::BlockedPeers::Slice &data) { return data.total; }), tr::lng_settings_no_blocked_users() ) | rpl::map([](int count, const QString &none) { return count ? QString::number(count) : none; }); builder.addButton({ .id = u"security/blocked"_q, .title = tr::lng_settings_blocked_users(), .icon = { &st::menuIconBlock }, .label = std::move(blockedCount), .onClick = [=] { showOther(Blocked::Id()); }, .keywords = { u"blocked"_q, u"ban"_q }, }); builder.add([session, updateTrigger = rpl::duplicate(updateTrigger)](const WidgetContext &ctx) mutable { std::move(updateTrigger) | rpl::on_next([=] { session->api().blockedPeers().reload(); }, ctx.container->lifetime()); return SectionBuilder::WidgetToAdd{}; }); auto websitesCount = session->api().websites().totalValue(); auto websitesShown = rpl::duplicate(websitesCount) | rpl::map( rpl::mappers::_1 > 0); auto websitesLabel = rpl::duplicate( websitesCount ) | rpl::filter(rpl::mappers::_1 > 0) | rpl::map([](int count) { return QString::number(count); }); builder.addButton({ .id = u"security/websites"_q, .title = tr::lng_settings_logged_in(), .icon = { &st::menuIconIpAddress }, .label = std::move(websitesLabel), .onClick = [=] { showOther(Websites::Id()); }, .keywords = { u"websites"_q, u"bots"_q, u"logged"_q }, .shown = std::move(websitesShown), }); builder.add([session, updateTrigger = rpl::duplicate(updateTrigger)](const WidgetContext &ctx) mutable { std::move(updateTrigger) | rpl::on_next([=] { session->api().websites().reload(); }, ctx.container->lifetime()); return SectionBuilder::WidgetToAdd{}; }); auto sessionsCount = session->api().authorizations().totalValue( ) | rpl::map([](int count) { return count ? QString::number(count) : QString(); }); builder.addButton({ .id = u"security/sessions"_q, .title = tr::lng_settings_show_sessions(), .icon = { &st::menuIconDevices }, .label = std::move(sessionsCount), .onClick = [=] { showOther(Sessions::Id()); }, .keywords = { u"sessions"_q, u"devices"_q, u"active"_q }, }); builder.add([session, updateTrigger = std::move(updateTrigger)](const WidgetContext &ctx) mutable { std::move(updateTrigger) | rpl::on_next([=] { session->api().authorizations().reload(); }, ctx.container->lifetime()); return SectionBuilder::WidgetToAdd{}; }); builder.addSkip(); builder.addDividerText(tr::lng_settings_sessions_about()); } void BuildPrivacySection(SectionBuilder &builder) { const auto controller = builder.controller(); const auto session = builder.session(); builder.addSkip(st::settingsPrivacySkip); builder.addSubsectionTitle({ .id = u"privacy/section"_q, .title = tr::lng_settings_privacy_title(), .keywords = { u"privacy"_q, u"visibility"_q }, }); using Key = Privacy::Key; builder.addPrivacyButton({ .id = u"privacy/phone_number"_q, .title = tr::lng_settings_phone_number_privacy(), .key = Key::PhoneNumber, .controllerFactory = [=] { return std::make_unique(controller); }, .keywords = { u"phone"_q, u"number"_q }, }); builder.addPrivacyButton({ .id = u"privacy/last_seen"_q, .title = tr::lng_settings_last_seen(), .key = Key::LastSeen, .controllerFactory = [=] { return std::make_unique(session); }, .keywords = { u"last seen"_q, u"online"_q }, }); builder.addPrivacyButton({ .id = u"privacy/profile_photo"_q, .title = tr::lng_settings_profile_photo_privacy(), .key = Key::ProfilePhoto, .controllerFactory = [] { return std::make_unique(); }, .keywords = { u"photo"_q, u"avatar"_q }, }); builder.addPrivacyButton({ .id = u"privacy/forwards"_q, .title = tr::lng_settings_forwards_privacy(), .key = Key::Forwards, .controllerFactory = [=] { return std::make_unique(controller); }, .keywords = { u"forwards"_q, u"link"_q }, }); builder.addPrivacyButton({ .id = u"privacy/calls"_q, .title = tr::lng_settings_calls(), .key = Key::Calls, .controllerFactory = [] { return std::make_unique(); }, .keywords = { u"calls"_q, u"voice"_q }, }); builder.addPrivacyButton({ .id = u"privacy/voices"_q, .title = tr::lng_settings_voices_privacy(), .key = Key::Voices, .controllerFactory = [=] { return std::make_unique(session); }, .premium = true, .keywords = { u"voice"_q, u"messages"_q }, }); const auto privacy = &session->api().globalPrivacy(); auto messagesLabel = rpl::combine( privacy->newRequirePremium(), privacy->newChargeStars() ) | rpl::map([=](bool requirePremium, int chargeStars) { return chargeStars ? tr::lng_edit_privacy_paid() : requirePremium ? tr::lng_edit_privacy_contacts_and_premium() : tr::lng_edit_privacy_everyone(); }) | rpl::flatten_latest(); const auto messagesPremium = !session->appConfig().newRequirePremiumFree(); const auto messagesButton = builder.addButton({ .id = u"privacy/messages"_q, .title = tr::lng_settings_messages_privacy(), .st = &st::settingsButtonNoIcon, .label = rpl::duplicate(messagesLabel), .onClick = [=] { controller->show(Box(EditMessagesPrivacyBox, controller, QString())); }, .keywords = { u"messages"_q, u"new"_q, u"unknown"_q }, }); if (messagesPremium && messagesButton) { AddPrivacyPremiumStar( messagesButton, session, std::move(messagesLabel), st::settingsButtonNoIcon.padding); } builder.addPrivacyButton({ .id = u"privacy/birthday"_q, .title = tr::lng_settings_birthday_privacy(), .key = Key::Birthday, .controllerFactory = [] { return std::make_unique(); }, .keywords = { u"birthday"_q, u"age"_q }, }); builder.addPrivacyButton({ .id = u"privacy/gifts"_q, .title = tr::lng_settings_gifts_privacy(), .key = Key::GiftsAutoSave, .controllerFactory = [] { return std::make_unique(); }, .keywords = { u"gifts"_q }, }); builder.addPrivacyButton({ .id = u"privacy/bio"_q, .title = tr::lng_settings_bio_privacy(), .key = Key::About, .controllerFactory = [] { return std::make_unique(); }, .keywords = { u"bio"_q, u"about"_q }, }); builder.addPrivacyButton({ .id = u"privacy/saved_music"_q, .title = tr::lng_settings_saved_music_privacy(), .key = Key::SavedMusic, .controllerFactory = [] { return std::make_unique(); }, .keywords = { u"music"_q, u"saved"_q }, }); builder.addPrivacyButton({ .id = u"privacy/groups"_q, .title = tr::lng_settings_groups_invite(), .key = Key::Invites, .controllerFactory = [] { return std::make_unique(); }, .keywords = { u"groups"_q, u"invite"_q }, }); session->api().userPrivacy().reload(Privacy::Key::AddedByPhone); builder.addSkip(st::settingsPrivacySecurityPadding); builder.addDivider(); } void BuildArchiveAndMuteSection(SectionBuilder &builder) { const auto session = builder.session(); const auto privacy = &session->api().globalPrivacy(); privacy->reload(); auto shown = rpl::single( false ) | rpl::then(privacy->showArchiveAndMute( ) | rpl::filter(rpl::mappers::_1) | rpl::take(1)); auto premium = Data::AmPremiumValue(session); builder.scope([&] { builder.addSkip(); builder.addSubsectionTitle({ .id = u"privacy/new_unknown"_q, .title = tr::lng_settings_new_unknown(), .keywords = { u"unknown"_q, u"archive"_q, u"mute"_q }, }); const auto toggle = builder.addButton({ .id = u"privacy/archive_and_mute"_q, .title = tr::lng_settings_auto_archive(), .st = &st::settingsButtonNoIcon, .toggled = privacy->archiveAndMute(), .keywords = { u"archive"_q, u"mute"_q, u"unknown"_q }, }); if (toggle) { toggle->toggledChanges( ) | rpl::filter([=](bool toggled) { return toggled != privacy->archiveAndMuteCurrent(); }) | rpl::on_next([=](bool toggled) { privacy->updateArchiveAndMute(toggled); }, toggle->lifetime()); } builder.addSkip(); builder.addDividerText(tr::lng_settings_auto_archive_about()); }, rpl::combine( std::move(shown), std::move(premium), rpl::mappers::_1 || rpl::mappers::_2)); } void BuildBotsAndWebsitesSection(SectionBuilder &builder) { const auto controller = builder.controller(); const auto session = builder.session(); builder.addSkip(); builder.addSubsectionTitle({ .id = u"privacy/bots"_q, .title = tr::lng_settings_security_bots(), .keywords = { u"bots"_q, u"payment"_q, u"websites"_q }, }); builder.addButton({ .id = u"privacy/bots_payment"_q, .title = tr::lng_settings_clear_payment_info(), .st = &st::settingsButtonNoIcon, .onClick = [=] { controller->show(ClearPaymentInfoBox(session)); }, .keywords = { u"payment"_q, u"bots"_q, u"clear"_q }, }); builder.addSkip(); builder.addDivider(); } void BuildTopPeersSection(SectionBuilder &builder) { const auto session = builder.session(); builder.addSkip(); builder.addSubsectionTitle({ .id = u"privacy/top_peers"_q, .title = tr::lng_settings_top_peers_title(), .keywords = { u"suggest"_q, u"contacts"_q, u"frequent"_q }, }); const auto toggle = builder.addButton({ .id = u"privacy/top_peers_toggle"_q, .title = tr::lng_settings_top_peers_suggest(), .st = &st::settingsButtonNoIcon, .toggled = rpl::single( rpl::empty ) | rpl::then( session->topPeers().updates() ) | rpl::map([=] { return !session->topPeers().disabled(); }), .keywords = { u"suggest"_q, u"contacts"_q }, }); if (toggle) { toggle->toggledChanges( ) | rpl::filter([=](bool enabled) { return enabled == session->topPeers().disabled(); }) | rpl::on_next([=](bool enabled) { session->topPeers().toggleDisabled(!enabled); }, toggle->lifetime()); } builder.addSkip(); builder.addDividerText(tr::lng_settings_top_peers_about()); } void BuildSelfDestructionSection( SectionBuilder &builder, rpl::producer<> updateTrigger) { const auto controller = builder.controller(); const auto session = builder.session(); builder.addSkip(); builder.addSubsectionTitle({ .id = u"privacy/self_destruct"_q, .title = tr::lng_settings_destroy_title(), .keywords = { u"delete"_q, u"destroy"_q, u"inactive"_q, u"account"_q }, }); builder.add([session, updateTrigger = std::move(updateTrigger)](const WidgetContext &ctx) mutable { std::move(updateTrigger) | rpl::on_next([=] { session->api().selfDestruct().reload(); }, ctx.container->lifetime()); return SectionBuilder::WidgetToAdd{}; }); auto label = session->api().selfDestruct().daysAccountTTL( ) | rpl::map(SelfDestructionBox::DaysLabel); builder.addButton({ .id = u"privacy/self_destruct_button"_q, .title = tr::lng_settings_destroy_if(), .st = &st::settingsButtonNoIcon, .label = std::move(label), .onClick = [=] { controller->show(Box( session, SelfDestructionBox::Type::Account, session->api().selfDestruct().daysAccountTTL())); }, .keywords = { u"delete"_q, u"destroy"_q, u"inactive"_q }, }); builder.addSkip(); } void BuildConfirmationExtensions(SectionBuilder &builder) { const auto controller = builder.controller(); const auto hasExtensions = !Core::App().settings().noWarningExtensions().empty() || !Core::App().settings().ipRevealWarning(); if (!hasExtensions) { return; } builder.addSkip(); builder.addSubsectionTitle({ .id = u"privacy/file_confirmations"_q, .title = tr::lng_settings_file_confirmations(), .keywords = { u"extensions"_q, u"files"_q, u"confirmations"_q }, }); builder.addButton({ .id = u"privacy/file_confirmations_button"_q, .title = tr::lng_settings_edit_extensions(), .st = &st::settingsButtonNoIcon, .onClick = [=] { controller->show(Box(OpenFileConfirmationsBox)); }, .keywords = { u"extensions"_q, u"files"_q, u"confirmations"_q }, }); builder.addSkip(); builder.addDividerText(tr::lng_settings_edit_extensions_about()); } void BuildPrivacySecuritySectionContent(SectionBuilder &builder) { auto updateOnTick = rpl::single( ) | rpl::then(base::timer_each(kUpdateTimeout)); const auto trigger = [&] { return rpl::duplicate(updateOnTick); }; BuildSecuritySection(builder, trigger()); BuildPrivacySection(builder); BuildArchiveAndMuteSection(builder); BuildBotsAndWebsitesSection(builder); BuildConfirmationExtensions(builder); BuildTopPeersSection(builder); BuildSelfDestructionSection(builder, trigger()); } const auto kMeta = BuildHelper({ .id = PrivacySecurity::Id(), .parentId = Main::Id(), .title = &tr::lng_settings_section_privacy, .icon = &st::menuIconLock, }, [](SectionBuilder &builder) { BuildPrivacySecuritySectionContent(builder); }); } // namespace SectionBuildMethod PrivacySecuritySection = kMeta.build; } // namespace Settings::Builder