/* 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 "core/local_url_handlers.h" #include "api/api_authorizations.h" #include "api/api_confirm_phone.h" #include "api/api_chat_filters.h" #include "api/api_chat_invite.h" #include "api/api_premium.h" #include "base/qthelp_regex.h" #include "base/qthelp_url.h" #include "lang/lang_cloud_manager.h" #include "lang/lang_keys.h" #include "core/update_checker.h" #include "core/application.h" #include "core/click_handler_types.h" #include "dialogs/ui/dialogs_suggestions.h" #include "boxes/background_preview_box.h" #include "ui/boxes/confirm_box.h" #include "ui/boxes/edit_birthday_box.h" #include "ui/integration.h" #include "payments/payments_non_panel_process.h" #include "boxes/peers/edit_peer_info_box.h" #include "boxes/share_box.h" #include "boxes/connection_box.h" #include "boxes/gift_premium_box.h" #include "boxes/edit_privacy_box.h" #include "boxes/premium_preview_box.h" #include "boxes/sticker_set_box.h" #include "boxes/star_gift_box.h" #include "boxes/language_box.h" #include "passport/passport_form_controller.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" #include "ui/vertical_list.h" #include "data/components/credits.h" #include "data/data_birthday.h" #include "data/data_channel.h" #include "data/data_document.h" #include "data/data_session.h" #include "data/data_user.h" #include "media/player/media_player_instance.h" #include "media/view/media_view_open_common.h" #include "window/window_session_controller.h" #include "window/window_session_controller_link_info.h" #include "window/window_controller.h" #include "window/window_peer_menu.h" #include "window/themes/window_theme_editor_box.h" // GenerateSlug. #include "payments/payments_checkout_process.h" #include "settings/settings_active_sessions.h" #include "settings/settings_credits.h" #include "settings/settings_credits_graphics.h" #include "settings/settings_information.h" #include "settings/settings_global_ttl.h" #include "settings/settings_folders.h" #include "settings/settings_main.h" #include "settings/settings_privacy_controllers.h" #include "settings/settings_privacy_security.h" #include "settings/settings_chat.h" #include "settings/settings_premium.h" #include "storage/storage_account.h" #include "mainwidget.h" #include "main/main_account.h" #include "main/main_app_config.h" #include "main/main_domain.h" #include "main/main_session.h" #include "main/main_session_settings.h" #include "info/info_controller.h" #include "info/info_memento.h" #include "inline_bots/bot_attach_web_view.h" #include "history/history.h" #include "history/history_item.h" #include "apiwrap.h" #include // AyuGram includes #include "ayu/ayu_url_handlers.h" namespace Core { namespace { using Match = qthelp::RegularExpressionMatch; class PersonalChannelController final : public PeerListController { public: explicit PersonalChannelController( not_null window); ~PersonalChannelController(); Main::Session &session() const override; void prepare() override; void rowClicked(not_null row) override; [[nodiscard]] rpl::producer> chosen() const; private: const not_null _window; rpl::event_stream> _chosen; mtpRequestId _requestId = 0; }; PersonalChannelController::PersonalChannelController( not_null window) : _window(window) { } PersonalChannelController::~PersonalChannelController() { if (_requestId) { _window->session().api().request(_requestId).cancel(); } } Main::Session &PersonalChannelController::session() const { return _window->session(); } void PersonalChannelController::prepare() { setDescription(object_ptr( nullptr, tr::lng_contacts_loading(), computeListSt().about)); using Flag = MTPchannels_GetAdminedPublicChannels::Flag; _requestId = _window->session().api().request( MTPchannels_GetAdminedPublicChannels( MTP_flags(Flag::f_for_personal)) ).done([=](const MTPmessages_Chats &result) { _requestId = 0; setDescription(nullptr); const auto &chats = result.match([](const auto &data) { return data.vchats().v; }); const auto owner = &_window->session().data(); for (const auto &chat : chats) { if (const auto peer = owner->processChat(chat)) { const auto rowId = peer->id.value; const auto channel = peer->asChannel(); if (channel && !delegate()->peerListFindRow(rowId)) { auto row = std::make_unique(peer); row->setCustomStatus(tr::lng_chat_status_subscribers( tr::now, lt_count, channel->membersCount())); delegate()->peerListAppendRow(std::move(row)); } } } if (!delegate()->peerListFullRowsCount()) { auto none = rpl::combine( tr::lng_settings_channel_no_yet(Ui::Text::WithEntities), tr::lng_settings_channel_start() ) | rpl::map([](TextWithEntities &&text, const QString &link) { return text.append('\n').append(Ui::Text::Link(link)); }); auto label = object_ptr( nullptr, std::move(none), computeListSt().about); label->setClickHandlerFilter([=](const auto &...) { _window->showNewChannel(); return false; }); setDescription(std::move(label)); } delegate()->peerListRefreshRows(); }).send(); } void PersonalChannelController::rowClicked(not_null row) { if (const auto channel = row->peer()->asChannel()) { _chosen.fire_copy(channel); } } auto PersonalChannelController::chosen() const -> rpl::producer> { return _chosen.events(); } Window::SessionController *ApplyAccountIndex( not_null controller, int accountIndex) { if (accountIndex <= 0) { return nullptr; } const auto list = Core::App().domain().orderedAccounts(); if (accountIndex > int(list.size())) { return nullptr; } const auto account = list[accountIndex - 1]; if (account == &controller->session().account()) { return controller; } else if (const auto window = Core::App().windowFor({ account })) { if (&window->account() != account) { Core::App().domain().maybeActivate(account); if (&window->account() != account) { return nullptr; } } const auto session = window->sessionController(); if (session) { return session; } } return nullptr; } void SavePersonalChannel( not_null window, ChannelData *channel) { const auto self = window->session().user(); const auto history = channel ? channel->owner().history(channel->id).get() : nullptr; const auto item = history ? history->lastServerMessage() : nullptr; const auto channelId = channel ? peerToChannel(channel->id) : ChannelId(); const auto messageId = item ? item->id : MsgId(); if (self->personalChannelId() != channelId || (messageId && self->personalChannelMessageId() != messageId)) { self->setPersonalChannel(channelId, messageId); self->session().api().request(MTPaccount_UpdatePersonalChannel( channel ? channel->inputChannel : MTP_inputChannelEmpty() )).done(crl::guard(window, [=] { window->showToast((channel ? tr::lng_settings_channel_saved : tr::lng_settings_channel_removed)(tr::now)); })).fail(crl::guard(window, [=](const MTP::Error &error) { window->showToast(u"Error: "_q + error.type()); })).send(); } } bool JoinGroupByHash( Window::SessionController *controller, const Match &match, const QVariant &context) { if (!controller) { return false; } Api::CheckChatInvite(controller, match->captured(1)); controller->window().activate(); return true; } bool JoinFilterBySlug( Window::SessionController *controller, const Match &match, const QVariant &context) { if (!controller) { return false; } Api::CheckFilterInvite(controller, match->captured(1)); controller->window().activate(); return true; } bool ShowStickerSet( Window::SessionController *controller, const Match &match, const QVariant &context) { if (!controller) { return false; } Core::App().hideMediaView(); controller->show(Box( controller->uiShow(), StickerSetIdentifier{ .shortName = match->captured(2) }, (match->captured(1) == "addemoji" ? Data::StickersType::Emoji : Data::StickersType::Stickers))); controller->window().activate(); return true; } bool ShowTheme( Window::SessionController *controller, const Match &match, const QVariant &context) { if (!controller) { return false; } const auto fromMessageId = context.value().itemId; Core::App().hideMediaView(); controller->session().data().cloudThemes().resolve( &controller->window(), match->captured(1), fromMessageId); controller->window().activate(); return true; } void ShowLanguagesBox(Window::SessionController *controller) { static auto Guard = base::binary_guard(); Guard = LanguageBox::Show(controller); } void ShowPhonePrivacyBox(Window::SessionController *controller) { static auto Guard = base::binary_guard(); auto guard = base::binary_guard(); using Privacy = Api::UserPrivacy; const auto key = Privacy::Key::PhoneNumber; controller->session().api().userPrivacy().reload(key); const auto weak = base::make_weak(controller); auto shared = std::make_shared( guard.make_guard()); auto lifetime = std::make_shared(); controller->session().api().userPrivacy().value( key ) | rpl::take( 1 ) | rpl::start_with_next([=](const Privacy::Rule &value) mutable { using namespace ::Settings; const auto show = shared->alive(); if (lifetime) { base::take(lifetime)->destroy(); } if (show) { if (const auto controller = weak.get()) { controller->show(Box( controller, std::make_unique( controller), value)); } } }, *lifetime); Guard = std::move(guard); } bool SetLanguage( Window::SessionController *controller, const Match &match, const QVariant &context) { if (match->capturedView(1).isEmpty()) { ShowLanguagesBox(controller); } else { const auto languageId = match->captured(2); Lang::CurrentCloudManager().switchWithWarning(languageId); } if (controller) { controller->window().activate(); } return true; } bool ShareUrl( Window::SessionController *controller, const Match &match, const QVariant &context) { if (!controller) { return false; } const auto params = url_parse_params( match->captured(1), qthelp::UrlParamNameTransform::ToLower); const auto url = params.value(u"url"_q); if (url.isEmpty() || url.trimmed().startsWith('@')) { // Don't allow to insert an inline bot query by share url link. return false; } const auto text = params.value("text"); const auto chosen = [=](not_null thread) { const auto content = controller->content(); return content->shareUrl(thread, url, text); }; Window::ShowChooseRecipientBox(controller, chosen); controller->window().activate(); return true; } bool ConfirmPhone( Window::SessionController *controller, const Match &match, const QVariant &context) { if (!controller) { return false; } const auto params = url_parse_params( match->captured(1), qthelp::UrlParamNameTransform::ToLower); const auto phone = params.value(u"phone"_q); const auto hash = params.value(u"hash"_q); if (phone.isEmpty() || hash.isEmpty()) { return false; } controller->session().api().confirmPhone().resolve( controller, phone, hash); controller->window().activate(); return true; } bool ApplySocksProxy( Window::SessionController *controller, const Match &match, const QVariant &context) { auto params = url_parse_params( match->captured(1), qthelp::UrlParamNameTransform::ToLower); ProxiesBoxController::ShowApplyConfirmation( controller, MTP::ProxyData::Type::Socks5, params); if (controller) { controller->window().activate(); } return true; } bool ApplyMtprotoProxy( Window::SessionController *controller, const Match &match, const QVariant &context) { auto params = url_parse_params( match->captured(1), qthelp::UrlParamNameTransform::ToLower); ProxiesBoxController::ShowApplyConfirmation( controller, MTP::ProxyData::Type::Mtproto, params); if (controller) { controller->window().activate(); } return true; } bool ShowPassportForm( Window::SessionController *controller, const QMap ¶ms) { if (!controller) { return false; } const auto botId = params.value("bot_id", QString()).toULongLong(); const auto scope = params.value("scope", QString()); const auto callback = params.value("callback_url", QString()); const auto publicKey = params.value("public_key", QString()); const auto nonce = params.value( Passport::NonceNameByScope(scope), QString()); controller->showPassportForm(Passport::FormRequest( botId, scope, callback, publicKey, nonce)); return true; } bool ShowPassport( Window::SessionController *controller, const Match &match, const QVariant &context) { return ShowPassportForm( controller, url_parse_params( match->captured(1), qthelp::UrlParamNameTransform::ToLower)); } bool ShowWallPaper( Window::SessionController *controller, const Match &match, const QVariant &context) { if (!controller) { return false; } const auto params = url_parse_params( match->captured(1), qthelp::UrlParamNameTransform::ToLower); // const auto bg = params.value(u"bg_color"_q); const auto color = params.value(u"color"_q); const auto gradient = params.value(u"gradient"_q); const auto result = BackgroundPreviewBox::Start( controller, (!color.isEmpty() ? color : !gradient.isEmpty() ? gradient : params.value(u"slug"_q)), params); controller->window().activate(); return result; } [[nodiscard]] ChatAdminRights ParseRequestedAdminRights( const QString &value) { auto result = ChatAdminRights(); for (const auto &element : value.split(QRegularExpression(u"[+ ]"_q))) { if (element == u"change_info"_q) { result |= ChatAdminRight::ChangeInfo; } else if (element == u"post_messages"_q) { result |= ChatAdminRight::PostMessages; } else if (element == u"edit_messages"_q) { result |= ChatAdminRight::EditMessages; } else if (element == u"delete_messages"_q) { result |= ChatAdminRight::DeleteMessages; } else if (element == u"restrict_members"_q) { result |= ChatAdminRight::BanUsers; } else if (element == u"invite_users"_q) { result |= ChatAdminRight::InviteByLinkOrAdd; } else if (element == u"manage_topics"_q) { result |= ChatAdminRight::ManageTopics; } else if (element == u"pin_messages"_q) { result |= ChatAdminRight::PinMessages; } else if (element == u"promote_members"_q) { result |= ChatAdminRight::AddAdmins; } else if (element == u"manage_video_chats"_q) { result |= ChatAdminRight::ManageCall; } else if (element == u"anonymous"_q) { result |= ChatAdminRight::Anonymous; } else if (element == u"manage_chat"_q) { result |= ChatAdminRight::Other; } else { return {}; } } return result; } bool ResolveUsernameOrPhone( Window::SessionController *controller, const Match &match, const QVariant &context) { if (!controller) { return false; } const auto params = url_parse_params( match->captured(1), qthelp::UrlParamNameTransform::ToLower); if (params.contains(u"acc"_q)) { const auto switched = ApplyAccountIndex( controller, params.value(u"acc"_q).toInt()); if (switched) { controller = switched; } else { controller->showToast(u"Could not activate account %1."_q.arg( params.value(u"acc"_q))); return false; } } const auto domainParam = params.value(u"domain"_q); const auto appnameParam = params.value(u"appname"_q); const auto myContext = context.value(); if (domainParam == u"giftcode"_q && !appnameParam.isEmpty()) { const auto itemId = myContext.itemId; const auto item = controller->session().data().message(itemId); const auto fromId = item ? item->from()->id : PeerId(); const auto selfId = controller->session().userPeerId(); const auto toId = !item ? PeerId() : (fromId == selfId) ? item->history()->peer->id : selfId; ResolveGiftCode(controller, appnameParam, fromId, toId); return true; } // Fix t.me/s/username links. const auto webChannelPreviewLink = (domainParam == u"s"_q) && !appnameParam.isEmpty(); const auto domain = webChannelPreviewLink ? appnameParam : domainParam; const auto phone = params.value(u"phone"_q); const auto validDomain = [](const QString &domain) { return qthelp::regex_match( u"^[a-zA-Z0-9\\.\\_]+$"_q, domain, {} ).valid(); }; const auto validPhone = [](const QString &phone) { return qthelp::regex_match(u"^[0-9]+$"_q, phone, {}).valid(); }; if (domain == u"telegrampassport"_q) { return ShowPassportForm(controller, params); } else if (!validDomain(domain) && !validPhone(phone)) { return false; } using ResolveType = Window::ResolveType; auto resolveType = params.contains(u"profile"_q) ? ResolveType::Profile : ResolveType::Default; auto startToken = params.value(u"start"_q); auto referral = params.value(u"ref"_q); if (!startToken.isEmpty()) { resolveType = ResolveType::BotStart; if (referral.isEmpty()) { const auto appConfig = &controller->session().appConfig(); const auto &prefixes = appConfig->startRefPrefixes(); for (const auto &prefix : prefixes) { if (startToken.startsWith(prefix)) { referral = startToken.mid(prefix.size()); break; } } } } else if (params.contains(u"startgroup"_q)) { resolveType = ResolveType::AddToGroup; startToken = params.value(u"startgroup"_q); } else if (params.contains(u"startchannel"_q)) { resolveType = ResolveType::AddToChannel; } else if (params.contains(u"boost"_q)) { resolveType = ResolveType::Boost; } auto post = ShowAtUnreadMsgId; auto adminRights = ChatAdminRights(); if (resolveType == ResolveType::AddToGroup || resolveType == ResolveType::AddToChannel) { adminRights = ParseRequestedAdminRights(params.value(u"admin"_q)); } const auto postParam = params.value(u"post"_q); if (const auto postId = postParam.toInt()) { post = postId; } const auto storyParam = params.value(u"story"_q); const auto storyId = storyParam.toInt(); const auto appname = webChannelPreviewLink ? QString() : appnameParam; const auto commentParam = params.value(u"comment"_q); const auto commentId = commentParam.toInt(); const auto topicParam = params.value(u"topic"_q); const auto topicId = topicParam.toInt(); const auto threadParam = params.value(u"thread"_q); const auto threadId = topicId ? topicId : threadParam.toInt(); const auto gameParam = params.value(u"game"_q); const auto videot = params.value(u"t"_q); if (!gameParam.isEmpty() && validDomain(gameParam)) { startToken = gameParam; resolveType = ResolveType::ShareGame; } if (!appname.isEmpty()) { resolveType = ResolveType::BotApp; if (startToken.isEmpty() && params.contains(u"startapp"_q)) { startToken = params.value(u"startapp"_q); } } controller->window().activate(); controller->showPeerByLink(Window::PeerByLinkInfo{ .usernameOrId = domain, .phone = phone, .messageId = post, .storyId = storyId, .videoTimestamp = (!videot.isEmpty() ? ParseVideoTimestamp(videot) : std::optional()), .text = params.value(u"text"_q), .repliesInfo = commentId ? Window::RepliesByLinkInfo{ Window::CommentId{ commentId } } : threadId ? Window::RepliesByLinkInfo{ Window::ThreadId{ threadId } } : Window::RepliesByLinkInfo{ v::null }, .resolveType = resolveType, .referral = referral, .startToken = startToken, .startAdminRights = adminRights, .startAutoSubmit = myContext.botStartAutoSubmit, .botAppName = (appname.isEmpty() ? postParam : appname), .botAppForceConfirmation = myContext.mayShowConfirmation, .botAppFullScreen = (params.value(u"mode"_q) == u"fullscreen"_q), .attachBotUsername = params.value(u"attach"_q), .attachBotToggleCommand = (params.contains(u"startattach"_q) ? params.value(u"startattach"_q) : (appname.isEmpty() && params.contains(u"startapp"_q)) ? params.value(u"startapp"_q) : std::optional()), .attachBotMainOpen = (appname.isEmpty() && params.contains(u"startapp"_q)), .attachBotMainCompact = (appname.isEmpty() && params.contains(u"startapp"_q) && (params.value(u"mode"_q) == u"compact"_q)), .attachBotChooseTypes = InlineBots::ParseChooseTypes( params.value(u"choose"_q)), .voicechatHash = (params.contains(u"livestream"_q) ? std::make_optional(params.value(u"livestream"_q)) : params.contains(u"videochat"_q) ? std::make_optional(params.value(u"videochat"_q)) : params.contains(u"voicechat"_q) ? std::make_optional(params.value(u"voicechat"_q)) : std::nullopt), .clickFromMessageId = myContext.itemId, .clickFromBotWebviewContext = myContext.botWebviewContext, }); return true; } bool ResolvePrivatePost( Window::SessionController *controller, const Match &match, const QVariant &context) { if (!controller) { return false; } const auto params = url_parse_params( match->captured(1), qthelp::UrlParamNameTransform::ToLower); const auto channelId = ChannelId( params.value(u"channel"_q).toULongLong()); const auto msgId = params.value(u"post"_q).toInt(); const auto commentParam = params.value(u"comment"_q); const auto commentId = commentParam.toInt(); const auto topicParam = params.value(u"topic"_q); const auto topicId = topicParam.toInt(); const auto threadParam = params.value(u"thread"_q); const auto threadId = topicId ? topicId : threadParam.toInt(); if (!channelId || (msgId && !IsServerMsgId(msgId))) { return false; } const auto my = context.value(); controller->showPeerByLink(Window::PeerByLinkInfo{ .usernameOrId = channelId, .messageId = msgId, .repliesInfo = commentId ? Window::RepliesByLinkInfo{ Window::CommentId{ commentId } } : threadId ? Window::RepliesByLinkInfo{ Window::ThreadId{ threadId } } : Window::RepliesByLinkInfo{ v::null }, .clickFromMessageId = my.itemId, .clickFromBotWebviewContext = my.botWebviewContext, }); controller->window().activate(); return true; } bool ResolveSettings( Window::SessionController *controller, const Match &match, const QVariant &context) { const auto section = match->captured(1).mid(1).toLower(); const auto type = [&]() -> std::optional<::Settings::Type> { if (section == u"language"_q) { ShowLanguagesBox(controller); return {}; } else if (section == u"phone_privacy"_q) { ShowPhonePrivacyBox(controller); return {}; } else if (section == u"devices"_q) { return ::Settings::Sessions::Id(); } else if (section == u"folders"_q) { return ::Settings::Folders::Id(); } else if (section == u"privacy"_q) { return ::Settings::PrivacySecurity::Id(); } else if (section == u"themes"_q) { return ::Settings::Chat::Id(); } else if (section == u"change_number"_q) { controller->show( Ui::MakeInformBox(tr::lng_change_phone_error())); return {}; } else if (section == u"auto_delete"_q) { return ::Settings::GlobalTTLId(); } else if (section == u"information"_q) { return ::Settings::Information::Id(); } return ::Settings::Main::Id(); }(); if (type.has_value()) { if (!controller) { return false; } else if (*type == ::Settings::Sessions::Id()) { controller->session().api().authorizations().reload(); } controller->showSettings(*type); controller->window().activate(); } return true; } bool HandleUnknown( Window::SessionController *controller, const Match &match, const QVariant &context) { if (!controller) { return false; } const auto request = match->captured(1); const auto callback = crl::guard(controller, [=]( TextWithEntities message, bool updateRequired) { if (updateRequired) { const auto callback = [=](Fn &&close) { Core::UpdateApplication(); close(); }; controller->show(Ui::MakeConfirmBox({ .text = message, .confirmed = callback, .confirmText = tr::lng_menu_update(), })); } else { controller->show(Ui::MakeInformBox(message)); } }); controller->session().api().requestDeepLinkInfo(request, callback); return true; } bool OpenMediaTimestamp( Window::SessionController *controller, const Match &match, const QVariant &context) { if (!controller) { return false; } const auto position = match->captured(2).toInt(); if (position < 0) { return false; } const auto base = match->captured(1); if (base.startsWith(u"doc"_q)) { const auto parts = base.mid(3).split('_'); const auto documentId = parts.value(0).toULongLong(); const auto itemId = FullMsgId( PeerId(parts.value(1).toULongLong()), MsgId(parts.value(2).toLongLong())); const auto session = &controller->session(); const auto document = session->data().document(documentId); const auto context = session->data().message(itemId); const auto time = position * crl::time(1000); if (document->isVideoFile()) { controller->window().openInMediaView(Media::View::OpenRequest( controller, document, context, context ? context->topicRootId() : MsgId(0), false, time)); } else if (document->isSong() || document->isVoiceMessage()) { session->local().setMediaLastPlaybackPosition(documentId, time); Media::Player::instance()->play({ document, itemId }); } return true; } return false; } bool ShowInviteLink( Window::SessionController *controller, const Match &match, const QVariant &context) { if (!controller) { return false; } const auto base64link = match->captured(1).toLatin1(); const auto link = QString::fromUtf8(QByteArray::fromBase64(base64link)); if (link.isEmpty()) { return false; } QGuiApplication::clipboard()->setText(link); controller->showToast(tr::lng_group_invite_copied(tr::now)); return true; } bool OpenExternalLink( Window::SessionController *controller, const Match &match, const QVariant &context) { return Ui::Integration::Instance().handleUrlClick( match->captured(1), context); } bool CopyPeerId( Window::SessionController *controller, const Match &match, const QVariant &context) { TextUtilities::SetClipboardText({ match->captured(1) }); if (controller) { controller->showToast(u"ID copied to clipboard."_q); } return true; } bool ShowSearchTagsPromo( Window::SessionController *controller, const Match &match, const QVariant &context) { if (!controller) { return false; } ShowPremiumPreviewBox(controller, PremiumFeature::TagsForMessages); return true; } bool ShowEditBirthday( Window::SessionController *controller, const Match &match, const QVariant &context) { if (!controller) { return false; } else if (controller->showFrozenError()) { return true; } const auto user = controller->session().user(); const auto save = [=](Data::Birthday result) { user->setBirthday(result); using Flag = MTPaccount_UpdateBirthday::Flag; using BFlag = MTPDbirthday::Flag; user->session().api().request(MTPaccount_UpdateBirthday( MTP_flags(result ? Flag::f_birthday : Flag()), MTP_birthday( MTP_flags(result.year() ? BFlag::f_year : BFlag()), MTP_int(result.day()), MTP_int(result.month()), MTP_int(result.year())) )).done(crl::guard(controller, [=] { controller->showToast(tr::lng_settings_birthday_saved(tr::now)); })).fail(crl::guard(controller, [=](const MTP::Error &error) { const auto type = error.type(); controller->showToast(type.startsWith(u"FLOOD_WAIT_"_q) ? tr::lng_flood_error(tr::now) : (u"Error: "_q + error.type())); })).handleFloodErrors().send(); }; if (match->captured(1).isEmpty()) { controller->show(Box(Ui::EditBirthdayBox, user->birthday(), save)); } else { controller->show(Box([=](not_null box) { Ui::EditBirthdayBox(box, user->birthday(), save); const auto container = box->verticalLayout(); const auto session = &user->session(); const auto key = Api::UserPrivacy::Key::Birthday; session->api().userPrivacy().reload(key); auto isExactlyContacts = session->api().userPrivacy().value( key ) | rpl::map([=](const Api::UserPrivacy::Rule &value) { return (value.option == Api::UserPrivacy::Option::Contacts) && value.always.peers.empty() && !value.always.premiums && value.never.peers.empty(); }) | rpl::distinct_until_changed(); Ui::AddSkip(container); const auto link = u"internal:edit_privacy_birthday:from_box"_q; Ui::AddDividerText(container, rpl::conditional( std::move(isExactlyContacts), tr::lng_settings_birthday_contacts( lt_link, tr::lng_settings_birthday_contacts_link( ) | Ui::Text::ToLink(link), Ui::Text::WithEntities), tr::lng_settings_birthday_about( lt_link, tr::lng_settings_birthday_about_link( ) | Ui::Text::ToLink(link), Ui::Text::WithEntities))); })); } return true; } bool ShowEditBirthdayPrivacy( Window::SessionController *controller, const Match &match, const QVariant &context) { if (!controller) { return false; } const auto isFromBox = !match->captured(1).isEmpty(); auto syncLifetime = controller->session().api().userPrivacy().value( Api::UserPrivacy::Key::Birthday ) | rpl::take( 1 ) | rpl::start_with_next([=](const Api::UserPrivacy::Rule &value) { if (isFromBox) { using namespace ::Settings; class Controller final : public BirthdayPrivacyController { object_ptr setupAboveWidget( not_null controller, not_null parent, rpl::producer