diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index b1e49a37ff..a6d0a69823 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -4733,6 +4733,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_url_auth_phone_toast_bad_title" = "Login failed"; "lng_url_auth_phone_toast_bad" = "Please try logging in to {domain} again."; "lng_url_auth_phone_toast_bad_expired" = "This link is expired."; +"lng_url_auth_match_code_title" = "Tap the emoji shown on your other device"; +"lng_url_auth_match_code_info" = "Login request from {domain}"; "lng_bot_start" = "Start"; "lng_bot_choose_group" = "Select a Group"; diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index 215d198105..83308c0e17 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -809,11 +809,24 @@ backgroundConfirmCancel: RoundButton(backgroundConfirm) { urlAuthCheckbox: Checkbox(defaultBoxCheckbox) { width: 240px; } +urlAuthCodesTitle: FlatLabel(defaultPeerListAbout) { + textFg: boxTitleFg; + style: TextStyle(defaultTextStyle) { + font: boxTitleFont; + } +} urlAuthCheckboxAbout: FlatLabel(defaultPeerListAbout) { style: TextStyle(boxTextStyle) { font: font(12px); } } +urlAuthCodesButton: RoundButton(defaultLightButton) { + height: 58px; + textTop: 19px; + style: TextStyle(semiboldTextStyle) { + font: font(20px semibold); + } +} urlAuthBoxRowTopLabel: FlatLabel(boxLabel) { maxHeight: 30px; } diff --git a/Telegram/SourceFiles/boxes/url_auth_box.cpp b/Telegram/SourceFiles/boxes/url_auth_box.cpp index ec1fd8e6da..6fa8050d7c 100644 --- a/Telegram/SourceFiles/boxes/url_auth_box.cpp +++ b/Telegram/SourceFiles/boxes/url_auth_box.cpp @@ -38,14 +38,18 @@ namespace UrlAuthBox { namespace { using AnotherSessionFactory = Fn()>; +using OnUserChangedCallback = Fn)>; struct SwitchAccountResult { - not_null widget; + Ui::RpWidget *widget = nullptr; AnotherSessionFactory anotherSession; + OnUserChangedCallback setOnUserChanged; + Fn updateUserIdHint; }; [[nodiscard]] SwitchAccountResult AddAccountsMenu( - not_null parent) { + not_null parent, + UserId userIdHint = UserId()) { const auto session = &Core::App().domain().active().session(); const auto widget = Ui::CreateChild( parent, @@ -53,15 +57,33 @@ struct SwitchAccountResult { struct State { base::unique_qptr menu; UserData *currentUser = nullptr; + Fn onUserChanged; }; const auto state = widget->lifetime().make_state(); - state->currentUser = session->user(); + + const auto isCurrentTest = session->isTestMode(); + const auto findHintedUser = [&]() -> UserData* { + if (!userIdHint) { + return session->user().get(); + } + for (const auto &account : Core::App().domain().orderedAccounts()) { + if (!account->sessionExists() + || (account->session().isTestMode() != isCurrentTest)) { + continue; + } + if (account->session().userId() == userIdHint) { + return account->session().user(); + } + } + return session->user().get(); + }; + + state->currentUser = findHintedUser(); const auto userpic = Ui::CreateChild( parent, state->currentUser, st::restoreUserpicIcon); widget->setUserpic(userpic); - const auto isCurrentTest = session->isTestMode(); const auto filtered = [=] { auto result = std::vector>(); for (const auto &account : Core::App().domain().orderedAccounts()) { @@ -90,6 +112,9 @@ struct SwitchAccountResult { user, st::restoreUserpicIcon); widget->setUserpic(newUserpic); + if (state->onUserChanged) { + state->onUserChanged(); + } }); auto owned = base::make_unique_q( state->menu->menu(), @@ -116,6 +141,25 @@ struct SwitchAccountResult { return { widget, [=] { return &state->currentUser->session(); }, + [=](Fn callback) { state->onUserChanged = callback; }, + [=](UserId newUserIdHint) { + const auto isCurrentTest = session->isTestMode(); + for (const auto &acc : Core::App().domain().orderedAccounts()) { + if (!acc->sessionExists() + || (acc->session().isTestMode() != isCurrentTest)) { + continue; + } + if (acc->session().userId() == newUserIdHint) { + state->currentUser = acc->session().user(); + const auto next = Ui::CreateChild( + parent, + state->currentUser, + st::restoreUserpicIcon); + widget->setUserpic(next); + break; + } + } + }, }; } @@ -269,20 +313,26 @@ void RequestButton( }; const auto callback = [=](Result result) { if (!result.auth) { + session->api().request(MTPmessages_DeclineUrlAuth( + MTP_string(url) + )).send(); finishWithUrl(url, false); } else if (session->data().message(itemId)) { using Flag = MTPmessages_AcceptUrlAuth::Flag; const auto flags = Flag(0) | (result.allowWrite ? Flag::f_write_allowed : Flag(0)) | (result.sharePhone ? Flag::f_share_phone_number : Flag(0)) + | (result.matchCode.isEmpty() ? Flag(0) : Flag::f_match_code) | (Flag::f_peer | Flag::f_msg_id | Flag::f_button_id); session->api().request(MTPmessages_AcceptUrlAuth( MTP_flags(flags), inputPeer, MTP_int(itemId.msg), MTP_int(buttonId), - MTPstring(), // #TODO auth url - MTPstring() + MTPstring(), + result.matchCode.isEmpty() + ? MTPstring() + : MTP_string(result.matchCode) )).done([=](const MTPUrlAuthResult &result) { const auto accepted = result.match( [](const MTPDurlAuthResultAccepted &data) { @@ -328,8 +378,22 @@ void RequestUrl( : nullptr; const auto requestPhone = request.is_request_phone_number(); const auto domain = qs(request.vdomain()); + const auto userIdHint = request.vuser_id_hint() + ? peerToUser(peerFromUser(*request.vuser_id_hint())) + : UserId(); + const auto matchCodes = [&] { + auto result = QStringList(); + if (const auto codes = request.vmatch_codes()) { + for (const auto &code : codes->v) { + result.push_back(qs(code)); + } + } + return result; + }(); const auto box = std::make_shared>(); + const auto authAccepted = std::make_shared(false); const auto finishWithUrl = [=](const QString &url, bool accepted) { + *authAccepted = accepted; if (*box) { (*box)->closeBox(); } @@ -341,25 +405,31 @@ void RequestUrl( }; const auto anotherSessionFactory = std::make_shared(nullptr); + const auto resolveSession = [=] { + return (*anotherSessionFactory) ? (*anotherSessionFactory)() : session; + }; const auto sendRequest = [=](Result result) { if (!result.auth) { + resolveSession()->api().request(MTPmessages_DeclineUrlAuth( + MTP_string(url) + )).send(); finishWithUrl(url, false); } else { const auto sharePhone = result.sharePhone; using Flag = MTPmessages_AcceptUrlAuth::Flag; const auto flags = Flag::f_url | (result.allowWrite ? Flag::f_write_allowed : Flag(0)) - | (sharePhone ? Flag::f_share_phone_number : Flag(0)); - const auto currentSession = anotherSessionFactory - ? (*anotherSessionFactory)() - : session; - currentSession->api().request(MTPmessages_AcceptUrlAuth( + | (sharePhone ? Flag::f_share_phone_number : Flag(0)) + | (result.matchCode.isEmpty() ? Flag(0) : Flag::f_match_code); + resolveSession()->api().request(MTPmessages_AcceptUrlAuth( MTP_flags(flags), MTPInputPeer(), MTPint(), // msg_id MTPint(), // button_id MTP_string(url), - MTPstring() + result.matchCode.isEmpty() + ? MTPstring() + : MTP_string(result.matchCode) )).done([=](const MTPUrlAuthResult &result) { const auto accepted = result.match( [](const MTPDurlAuthResultAccepted &data) { @@ -408,6 +478,35 @@ void RequestUrl( const auto ip = qs(request.vip().value_or("Unknown IP")); const auto region = qs(request.vregion().value_or("Unknown region")); *box = show->show(Box([=](not_null box) { + const auto accountResult = box->lifetime().make_state< + SwitchAccountResult>(nullptr); + const auto matchCodesShared = box->lifetime().make_state< + rpl::variable>(matchCodes); + const auto reloadRequest = [=] { + using Flag = MTPmessages_RequestUrlAuth::Flag; + const auto currentSession = resolveSession(); + currentSession->api().request(MTPmessages_RequestUrlAuth( + MTP_flags(Flag::f_url), + MTPInputPeer(), + MTPint(), // msg_id + MTPint(), // button_id + MTP_string(url) + )).done([=](const MTPUrlAuthResult &result) { + result.match([&](const MTPDurlAuthResultRequest &data) { + const auto newUserId = data.vuser_id_hint() + ? peerToUser(peerFromUser(*data.vuser_id_hint())) + : UserId(); + accountResult->updateUserIdHint(newUserId); + auto newCodes = QStringList(); + if (const auto codes = data.vmatch_codes()) { + for (const auto &code : codes->v) { + newCodes.push_back(qs(code)); + } + } + *matchCodesShared = newCodes; + }, [](const auto &) {}); + }).send(); + }; const auto callback = [=](Result result) { if (!requestPhone) { return sendRequest(result); @@ -422,9 +521,7 @@ void RequestUrl( close(); }; }; - const auto currentSession = anotherSessionFactory - ? (*anotherSessionFactory)() - : session; + const auto currentSession = resolveSession(); const auto capitalized = [=](const QString &value) { return value.left(1).toUpper() + value.mid(1).toLower(); }; @@ -461,15 +558,26 @@ void RequestUrl( browser, device, ip, - region); + region, + matchCodesShared->value()); - const auto content = box->verticalLayout(); - const auto accountResult = AddAccountsMenu(content); - content->widthValue() | rpl::on_next([=, w = accountResult.widget] { + *accountResult = AddAccountsMenu(box->verticalLayout(), userIdHint); + box->verticalLayout()->widthValue( + ) | rpl::on_next([=, w = (*accountResult).widget] { w->moveToRight(st::lineWidth * 4, 0); - }, accountResult.widget->lifetime()); - *anotherSessionFactory = accountResult.anotherSession; + }, (*accountResult).widget->lifetime()); + *anotherSessionFactory = (*accountResult).anotherSession; + (*accountResult).setOnUserChanged(reloadRequest); })); + if (*box) { + (*box)->boxClosing() | rpl::on_next([=] { + if (!(*authAccepted)) { + resolveSession()->api().request(MTPmessages_DeclineUrlAuth( + MTP_string(url) + )).send(); + } + }, (*box)->lifetime()); + } } } // namespace UrlAuthBox diff --git a/Telegram/SourceFiles/boxes/url_auth_box_content.cpp b/Telegram/SourceFiles/boxes/url_auth_box_content.cpp index 94240b3891..ebd3c8f6ec 100644 --- a/Telegram/SourceFiles/boxes/url_auth_box_content.cpp +++ b/Telegram/SourceFiles/boxes/url_auth_box_content.cpp @@ -14,8 +14,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/vertical_list.h" #include "ui/widgets/buttons.h" #include "ui/widgets/checkbox.h" +#include "ui/widgets/horizontal_fit_container.h" #include "ui/widgets/labels.h" #include "ui/widgets/tooltip.h" +#include "ui/emoji_config.h" #include "ui/wrap/vertical_layout.h" #include "ui/ui_utility.h" #include "styles/style_boxes.h" @@ -26,6 +28,118 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace UrlAuthBox { namespace { +void PrepareFullWidthRoundButton( + not_null button, + not_null content, + const style::margins &padding) { + button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + button->setFullRadius(true); + const auto paddingHorizontal = padding.left() + padding.right(); + content->widthValue() | rpl::on_next([=](int w) { + button->resize(w - paddingHorizontal, button->height()); + }, button->lifetime()); +} + +void ShowMatchCodesBox( + not_null box, + const QString &domain, + const QStringList &codes, + Fn callback) { + box->setWidth(st::boxWidth); + box->setStyle(st::futureOwnerBox); + + const auto content = box->verticalLayout(); + const auto &buttonStyle = st::defaultLightButton; + + Ui::AddSkip(content); + content->add( + object_ptr( + content, + tr::lng_url_auth_match_code_title(), + st::urlAuthCodesTitle), + st::boxRowPadding, + style::al_top); + + Ui::AddSkip(content); + Ui::AddSkip(content); + + const auto buttons = content->add( + object_ptr( + content, + st::boxRowPadding.left() * 2), + st::boxRowPadding); + buttons->resize(0, st::urlAuthCodesButton.height); + + for (const auto &code : codes) { + auto emojiLength = 0; + const auto emoji = Ui::Emoji::Find(code, &emojiLength); + const auto emojiCode = (emoji && (emojiLength == code.size())); + const auto button = Ui::CreateChild( + buttons, + rpl::single(emojiCode ? QString() : code), + st::urlAuthCodesButton); + if (emojiCode) { + button->setTextFgOverride(QColor(Qt::transparent)); + const auto overlay = Ui::CreateChild(button); + overlay->setAttribute(Qt::WA_TransparentForMouseEvents); + overlay->show(); + button->sizeValue() | rpl::on_next([=](QSize size) { + overlay->resize(size); + }, overlay->lifetime()); + overlay->paintOn([=](QPainter &p) { + const auto size = Ui::Emoji::GetSizeLarge(); + const auto visible = size / style::DevicePixelRatio(); + Ui::Emoji::Draw( + p, + emoji, + size, + (overlay->width() - visible) / 2, + (overlay->height() - visible) / 2); + }); + } + button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + button->setFullRadius(true); + button->setClickedCallback([=] { + box->closeBox(); + callback(code); + }); + buttons->add(button); + } + + Ui::AddSkip(content); + + const auto domainUrl = qthelp::validate_url(domain); + if (!domainUrl.isEmpty()) { + Ui::AddSkip(content); + Ui::AddSkip(content); + content->add( + object_ptr( + content, + tr::lng_url_auth_login_title( + lt_domain, + rpl::single(Ui::Text::Link(domain, domainUrl)), + tr::marked), + st::urlAuthCheckboxAbout), + st::boxRowPadding, + style::al_top); + } + Ui::AddSkip(content); + Ui::AddSkip(content); + { + const auto &padding = st::boxRowPadding; + const auto button = content->add( + object_ptr( + content, + tr::lng_cancel(), + st::attentionBoxButton), + padding); + PrepareFullWidthRoundButton(button, content, padding); + button->setClickedCallback([=] { + box->closeBox(); + }); + } +} + } // namespace SwitchableUserpicButton::SwitchableUserpicButton( @@ -252,8 +366,10 @@ void ShowDetails( const QString &browser, const QString &platform, const QString &ip, - const QString ®ion) { + const QString ®ion, + rpl::producer matchCodes) { box->setWidth(st::boxWidth); + box->setStyle(st::futureOwnerBox); const auto content = box->verticalLayout(); @@ -342,13 +458,61 @@ void ShowDetails( Ui::AddSkip(content); } - box->addButton(tr::lng_url_auth_login_button(), [=] { - callback({ - .auth = true, - .allowWrite = (allowMessages && allowMessages->toggled()), + struct State { + QStringList matchCodes; + }; + const auto state = box->lifetime().make_state(); + std::move(matchCodes) | rpl::on_next([=](const QStringList &codes) { + state->matchCodes = codes; + }, box->lifetime()); + + { + const auto &padding = st::boxRowPadding; + const auto button = content->add( + object_ptr( + content, + tr::lng_url_auth_login_button(), + st::defaultLightButton), + padding); + PrepareFullWidthRoundButton(button, content, padding); + button->setClickedCallback([=] { + if (state->matchCodes.isEmpty()) { + callback({ + .auth = true, + .allowWrite = (allowMessages && allowMessages->toggled()), + }); + return; + } + box->uiShow()->show(Box([=]( + not_null matchCodesBox) { + ShowMatchCodesBox( + matchCodesBox, + domain, + state->matchCodes, + [=](QString matchCode) { + callback({ + .auth = true, + .allowWrite = (allowMessages + && allowMessages->toggled()), + .matchCode = std::move(matchCode), + }); + }); + })); }); - }); - box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); + } + { + const auto &padding = st::boxRowPadding; + const auto button = content->add( + object_ptr( + content, + tr::lng_suggest_action_decline(), + st::attentionBoxButton), + padding); + PrepareFullWidthRoundButton(button, content, padding); + button->setClickedCallback([=] { + box->closeBox(); + }); + } } } // namespace UrlAuthBox diff --git a/Telegram/SourceFiles/boxes/url_auth_box_content.h b/Telegram/SourceFiles/boxes/url_auth_box_content.h index ccaa0b480a..d2ad724c72 100644 --- a/Telegram/SourceFiles/boxes/url_auth_box_content.h +++ b/Telegram/SourceFiles/boxes/url_auth_box_content.h @@ -20,6 +20,7 @@ struct Result { bool auth : 1 = false; bool allowWrite : 1 = false; bool sharePhone : 1 = false; + QString matchCode; }; class SwitchableUserpicButton final : public Ui::RippleButton { @@ -69,6 +70,7 @@ void ShowDetails( const QString &browser, const QString &platform, const QString &ip, - const QString ®ion); + const QString ®ion, + rpl::producer matchCodes = rpl::single(QStringList())); } // namespace UrlAuthBox