/* 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_passkeys.h" #include "settings/cloud_password/settings_cloud_password_common.h" #include "settings/settings_common_session.h" #include "data/components/passkeys.h" #include "data/data_session.h" #include "window/window_session_controller.h" #include "main/main_session.h" #include "platform/platform_webauthn.h" #include "ui/layers/generic_box.h" #include "ui/boxes/confirm_box.h" #include "ui/painter.h" #include "ui/rect.h" #include "ui/text/text_utilities.h" #include "ui/vertical_list.h" #include "ui/widgets/buttons.h" #include "ui/widgets/popup_menu.h" #include "ui/widgets/menu/menu_add_action_callback.h" #include "ui/widgets/menu/menu_add_action_callback_factory.h" #include "ui/wrap/vertical_layout.h" #include "lang/lang_keys.h" #include "lottie/lottie_icon.h" #include "base/unixtime.h" #include "data/stickers/data_custom_emoji.h" #include "ui/text/custom_emoji_instance.h" #include "styles/style_boxes.h" #include "styles/style_channel_earn.h" #include "styles/style_chat.h" #include "styles/style_layers.h" #include "styles/style_premium.h" #include "styles/style_settings.h" #include "styles/style_menu_icons.h" namespace Settings { class Passkeys : public Section { public: Passkeys( QWidget *parent, not_null controller); [[nodiscard]] rpl::producer title() override; [[nodiscard]] rpl::producer<> showFinishes() const { return _showFinished.events(); } private: void showFinished() override { _showFinished.fire({}); } void setupContent(not_null controller); rpl::event_stream<> _showFinished; }; void PasskeysNoneBox( not_null box, not_null session) { box->setWidth(st::boxWideWidth); box->setNoContentMargin(true); box->setCloseByEscape(true); box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); }); const auto content = box->verticalLayout().get(); Ui::AddSkip(content); { const auto &size = st::settingsCloudPasswordIconSize; auto icon = CreateLottieIcon( content, { .name = u"passkeys"_q, .sizeOverride = { size, size } }, st::settingLocalPasscodeIconPadding); const auto animate = std::move(icon.animate); box->addRow(std::move(icon.widget), style::al_top); box->showFinishes() | rpl::take(1) | rpl::start_with_next([=] { animate(anim::repeat::once); }, content->lifetime()); } Ui::AddSkip(content); Ui::AddSkip(content); box->addRow( object_ptr( content, tr::lng_settings_passkeys_none_title(), st::boxTitle), style::al_top); Ui::AddSkip(content); box->addRow( object_ptr( content, tr::lng_settings_passkeys_none_about(), st::channelEarnLearnDescription), style::al_top); Ui::AddSkip(content); Ui::AddSkip(content); Ui::AddSkip(content); { const auto padding = QMargins( st::settingsButton.padding.left(), st::boxRowPadding.top(), st::boxRowPadding.right(), st::boxRowPadding.bottom()); const auto iconLeft = st::settingsButton.iconLeft; const auto addEntry = [&]( rpl::producer title, rpl::producer about, const style::icon &icon) { const auto top = content->add( object_ptr( content, std::move(title), st::channelEarnSemiboldLabel), padding); Ui::AddSkip(content, st::channelEarnHistoryThreeSkip); content->add( object_ptr( content, std::move(about), st::channelEarnHistoryRecipientLabel), padding); const auto left = Ui::CreateChild( box->verticalLayout().get()); left->paintRequest( ) | rpl::start_with_next([=] { auto p = Painter(left); icon.paint(p, 0, 0, left->width()); }, left->lifetime()); left->resize(icon.size()); top->geometryValue( ) | rpl::start_with_next([=](const QRect &g) { left->moveToLeft( iconLeft, g.top() + st::channelEarnHistoryThreeSkip); }, left->lifetime()); }; addEntry( tr::lng_settings_passkeys_none_info1_title(), tr::lng_settings_passkeys_none_info1_about(), st::settingsIconPasskeysAboutIcon1); Ui::AddSkip(content); Ui::AddSkip(content); addEntry( tr::lng_settings_passkeys_none_info2_title(), tr::lng_settings_passkeys_none_info2_about(), st::settingsIconPasskeysAboutIcon2); Ui::AddSkip(content); Ui::AddSkip(content); addEntry( tr::lng_settings_passkeys_none_info3_title(), tr::lng_settings_passkeys_none_info3_about(), st::settingsIconPasskeysAboutIcon3); Ui::AddSkip(content); Ui::AddSkip(content); } Ui::AddSkip(content); Ui::AddSkip(content); { const auto &st = st::premiumPreviewDoubledLimitsBox; box->setStyle(st); auto button = object_ptr( box, tr::lng_settings_passkeys_none_button(), st::defaultActiveButton); button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); button->resizeToWidth(box->width() - st.buttonPadding.left() - st.buttonPadding.left()); button->setClickedCallback([=] { session->passkeys().initRegistration([=]( const Data::Passkey::RegisterData &data) { Platform::WebAuthn::RegisterKey(data, [=]( Platform::WebAuthn::RegisterResult result) { if (!result.success) { return; } session->passkeys().registerPasskey(result, [=] { box->closeBox(); }); }); }); }); box->addButton(std::move(button)); } } Passkeys::Passkeys( QWidget *parent, not_null controller) : Section(parent) { setupContent(controller); } rpl::producer Passkeys::title() { return tr::lng_settings_passkeys_title(); } void Passkeys::setupContent( not_null controller) { const auto content = Ui::CreateChild(this); const auto session = &controller->session(); CloudPassword::SetupHeader( content, u"passkeys"_q, showFinishes(), rpl::single(QString()), tr::lng_settings_passkeys_about()); const auto container = content->add( object_ptr(content)); const auto &st = st::peerListBoxItem; const auto rebuild = [=] { while (container->count()) { delete container->widgetAt(0); } for (const auto &passkey : session->passkeys().list()) { const auto button = container->add( object_ptr(container)); button->resize(button->width(), st.height); const auto menu = Ui::CreateChild( button, st::themesMenuToggle); menu->setClickedCallback([=] { const auto popup = Ui::CreateChild( menu, st::popupMenuWithIcons); const auto handler = [=, id = passkey.id] { controller->show(Ui::MakeConfirmBox({ .text = rpl::combine( tr::lng_settings_passkeys_delete_sure_about(), tr::lng_settings_passkeys_delete_sure_about2() ) | rpl::map([](QString a, QString b) { return a + "\n\n" + b; }), .confirmed = [=](Fn close) { session->passkeys().deletePasskey( id, close, [](QString) {}); }, .confirmText = tr::lng_box_delete(), .confirmStyle = &st::attentionBoxButton, .title = tr::lng_settings_passkeys_delete_sure_title(), })); }; Ui::Menu::CreateAddActionCallback(popup)({ .text = tr::lng_proxy_menu_delete(tr::now), .handler = handler, .icon = &st::menuIconDeleteAttention, .isAttention = true, }); popup->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight); const auto menuGlobal = menu->mapToGlobal( QPoint(menu->width(), menu->height())); popup->popup(menuGlobal); }); button->widthValue() | rpl::start_with_next([=](int width) { menu->moveToRight(0, (st.height - menu->height()) / 2, width); }, button->lifetime()); const auto iconSize = st::settingsIconPasskeys.width(); const auto emoji = iconSize; const auto iconLeft = st::settingsButton.iconLeft; auto emojiInstance = passkey.softwareEmojiId ? session->data().customEmojiManager().create( passkey.softwareEmojiId, [=] { button->update(); }, Data::CustomEmojiSizeTag::Large, emoji) : nullptr; const auto emojiPtr = emojiInstance.get(); button->lifetime().add([emoji = std::move(emojiInstance)] {}); const auto formatDateTime = [](TimeId timestamp) { const auto dt = base::unixtime::parse(timestamp); return tr::lng_mediaview_date_time( tr::now, lt_date, langDayOfMonthFull(dt.date()), lt_time, QLocale().toString(dt.time(), QLocale::ShortFormat)); }; const auto date = (passkey.lastUsageDate > 0) ? tr::lng_settings_passkeys_last_used( tr::now, lt_date, formatDateTime(passkey.lastUsageDate)) : tr::lng_settings_passkeys_created( tr::now, lt_date, formatDateTime(passkey.date)); button->paintOn([=, name = passkey.name](QPainter &p) { if (emojiPtr) { const auto emojiY = (st.height - iconSize) / 2; emojiPtr->paint(p, { .textColor = st.nameFg->c, .now = crl::now(), .position = QPoint(iconLeft, emojiY), }); } const auto textLeft = st::settingsButton.padding.left(); p.setFont(st.nameStyle.font); p.setPen(st.nameFg); p.drawText(textLeft, st.namePosition.y() + st.nameStyle.font->ascent, name); p.setFont(st::contactsStatusFont); p.setPen(st.statusFg); p.drawText( textLeft, st.statusPosition.y() + st::contactsStatusFont->ascent, date); }); button->showChildren(); } container->showChildren(); container->resizeToWidth(content->width()); }; session->passkeys().requestList( ) | rpl::start_with_next(rebuild, content->lifetime()); rebuild(); AddButtonWithIcon( content, tr::lng_settings_passkeys_button(), st::settingsButtonActive, { &st::settingsIconPasskeys } )->setClickedCallback([=] { controller->show(Box(PasskeysNoneBox, session)); }); Ui::ResizeFitChild(this, content); } Type PasskeysId() { return Passkeys::Id(); } } // namespace Settings