diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index a533e34dda..d5ca2f5b97 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1232,6 +1232,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_group_invite_context_copy" = "Copy"; "lng_group_invite_context_share" = "Share"; "lng_group_invite_context_edit" = "Edit"; +"lng_group_invite_context_qr" = "Get QR Code"; "lng_group_invite_context_revoke" = "Revoke"; "lng_group_invite_context_delete" = "Delete"; "lng_group_invite_context_delete_all" = "Delete all"; @@ -1261,6 +1262,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_group_invite_used_about" = "This link reached its usage limit."; "lng_group_invite_can_join_via_link#one" = "{count} person can join via this link."; "lng_group_invite_can_join_via_link#other" = "{count} people can join via this link."; +"lng_group_invite_qr_title" = "Invite by QR Code"; +"lng_group_invite_qr_about" = "Everyone on Telegram can scan this code to join your group."; +"lng_group_invite_qr_copied" = "QR Code copied to clipboard."; "lng_channel_public_link_copied" = "Link copied to clipboard."; "lng_context_about_private_link" = "This link will only work for members of this chat."; diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp index e7f3d1f1be..38dc102e3c 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.cpp @@ -38,17 +38,22 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_session_controller.h" #include "settings/settings_common.h" #include "mtproto/sender.h" +#include "qr/qr_generate.h" +#include "intro/intro_qr.h" // TelegramLogoImage #include "styles/style_boxes.h" #include "styles/style_layers.h" // st::boxDividerLabel. #include "styles/style_info.h" #include "styles/style_settings.h" #include +#include namespace { constexpr auto kFirstPage = 20; constexpr auto kPerPage = 100; +constexpr auto kShareQrSize = 768; +constexpr auto kShareQrPadding = 16; using LinkData = Api::InviteLink; @@ -113,6 +118,105 @@ private: return updated.link.isEmpty() || (!revoked && updated.revoked); } +QImage QrExact(const Qr::Data &data, int pixel) { + const auto image = [](int size) { + auto result = QImage( + size, + size, + QImage::Format_ARGB32_Premultiplied); + result.fill(Qt::transparent); + { + QPainter p(&result); + const auto skip = size / 12; + const auto logoSize = size - 2 * skip; + p.drawImage( + skip, + skip, + Intro::details::TelegramLogoImage().scaled( + logoSize, + logoSize, + Qt::IgnoreAspectRatio, + Qt::SmoothTransformation)); + } + return result; + }; + return Qr::ReplaceCenter( + Qr::Generate(data, pixel, st::windowFg->c), + image(Qr::ReplaceSize(data, pixel))); +} + +QImage Qr(const Qr::Data &data, int pixel, int max = 0) { + Expects(data.size > 0); + + if (max > 0 && data.size * pixel > max) { + pixel = std::max(max / data.size, 1); + } + return QrExact(data, pixel * style::DevicePixelRatio()); +} + +QImage Qr(const QString &text, int pixel, int max) { + return Qr(Qr::Encode(text), pixel, max); +} + +QImage QrForShare(const QString &text) { + const auto data = Qr::Encode(text); + const auto size = (kShareQrSize - 2 * kShareQrPadding); + const auto image = QrExact(data, size / data.size); + auto result = QImage( + kShareQrPadding * 2 + image.width(), + kShareQrPadding * 2 + image.height(), + QImage::Format_ARGB32_Premultiplied); + result.fill(Qt::white); + { + auto p = QPainter(&result); + p.drawImage(kShareQrPadding, kShareQrPadding, image); + } + return result; +} + +void QrBox( + not_null box, + const QString &link, + Fn share) { + box->setTitle(tr::lng_group_invite_qr_title()); + + box->addButton(tr::lng_about_done(), [=] { box->closeBox(); }); + + const auto qr = Qr( + link, + st::inviteLinkQrPixel, + st::boxWidth - st::boxRowPadding.left() - st::boxRowPadding.right()); + const auto size = qr.width() / style::DevicePixelRatio(); + const auto height = st::inviteLinkQrSkip * 2 + size; + const auto container = box->addRow( + object_ptr(box, height), + st::inviteLinkQrMargin); + const auto button = Ui::CreateChild(container); + button->resize(size, size); + button->paintRequest( + ) | rpl::start_with_next([=] { + QPainter(button).drawImage(QRect(0, 0, size, size), qr); + }, button->lifetime()); + container->widthValue( + ) | rpl::start_with_next([=](int width) { + button->move((width - size) / 2, st::inviteLinkQrSkip); + }, button->lifetime()); + button->setClickedCallback([=] { + share(QrForShare(link)); + }); + + box->addRow( + object_ptr( + box, + tr::lng_group_invite_qr_about(), + st::boxLabel), + st::inviteLinkQrValuePadding); + + box->addButton( + tr::lng_group_invite_context_copy(), + [=] { share(QrForShare(link)); }); +} + Controller::Controller( not_null peer, not_null admin, @@ -147,6 +251,9 @@ void Controller::addHeaderBlock(not_null container) { const auto shareLink = crl::guard(weak, [=] { ShareInviteLinkBox(_peer, link); }); + const auto getLinkQr = crl::guard(weak, [=] { + InviteLinkQrBox(link); + }); const auto revokeLink = crl::guard(weak, [=] { RevokeLink(_peer, admin, link); }); @@ -170,6 +277,9 @@ void Controller::addHeaderBlock(not_null container) { result->addAction( tr::lng_group_invite_context_share(tr::now), shareLink); + result->addAction( + tr::lng_group_invite_context_qr(tr::now), + getLinkQr); if (!admin->isBot()) { result->addAction( tr::lng_group_invite_context_edit(tr::now), @@ -473,12 +583,15 @@ SingleRowController::SingleRowController( void SingleRowController::prepare() { auto row = std::make_unique(_peer); + const auto raw = row.get(); std::move( _status ) | rpl::start_with_next([=](const QString &status) { raw->setCustomStatus(status); + delegate()->peerListUpdateRow(raw); }, _lifetime); + delegate()->peerListAppendRow(std::move(row)); delegate()->peerListRefreshRows(); } @@ -561,6 +674,11 @@ void AddPermanentLinkBlock( ShareInviteLinkBox(peer, current.link); } }); + const auto getLinkQr = crl::guard(weak, [=] { + if (const auto current = value->current(); !current.link.isEmpty()) { + InviteLinkQrBox(current.link); + } + }); const auto revokeLink = crl::guard(weak, [=] { const auto box = std::make_shared>(); const auto done = crl::guard(weak, [=] { @@ -595,6 +713,9 @@ void AddPermanentLinkBlock( result->addAction( tr::lng_group_invite_context_share(tr::now), shareLink); + result->addAction( + tr::lng_group_invite_context_qr(tr::now), + getLinkQr); if (!admin->isBot()) { result->addAction( tr::lng_group_invite_context_revoke(tr::now), @@ -790,6 +911,16 @@ void ShareInviteLinkBox(not_null peer, const QString &link) { Ui::LayerOption::KeepOther); } +void InviteLinkQrBox(const QString &link) { + Ui::show(Box(QrBox, link, [=](const QImage &image) { + auto mime = std::make_unique(); + mime->setImageData(image); + QGuiApplication::clipboard()->setMimeData(mime.release()); + + Ui::Toast::Show(tr::lng_group_invite_qr_copied(tr::now)); + })); +} + void EditLink( not_null peer, const Api::InviteLink &data) { diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.h b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.h index c8825e10f7..0d97c60be0 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.h +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_link.h @@ -34,6 +34,7 @@ void AddPermanentLinkBlock( void CopyInviteLink(const QString &link); void ShareInviteLinkBox(not_null peer, const QString &link); +void InviteLinkQrBox(const QString &link); void RevokeLink( not_null peer, not_null admin, diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp index 87c23d8407..dc78ef8af9 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_invite_links.cpp @@ -572,6 +572,9 @@ base::unique_qptr LinksController::createRowContextMenu( result->addAction(tr::lng_group_invite_context_share(tr::now), [=] { ShareInviteLinkBox(_peer, link); }); + result->addAction(tr::lng_group_invite_context_qr(tr::now), [=] { + InviteLinkQrBox(link); + }); result->addAction(tr::lng_group_invite_context_edit(tr::now), [=] { EditLink(_peer, data); }); diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index a8684372b2..f1076b875b 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -945,6 +945,11 @@ inviteLinkThreeDotsSkip: 12px; inviteLinkRevokedTitlePadding: margins(22px, 16px, 10px, 4px); inviteLinkLimitMargin: margins(22px, 8px, 22px, 8px); +inviteLinkQrPixel: 8px; +inviteLinkQrSkip: 24px; +inviteLinkQrMargin: margins(0px, 0px, 0px, 13px); +inviteLinkQrValuePadding: margins(22px, 0px, 22px, 12px); + infoAboutGigagroup: FlatLabel(defaultFlatLabel) { minWidth: 274px; } diff --git a/Telegram/SourceFiles/intro/intro_qr.cpp b/Telegram/SourceFiles/intro/intro_qr.cpp index 380889263f..c5bed139aa 100644 --- a/Telegram/SourceFiles/intro/intro_qr.cpp +++ b/Telegram/SourceFiles/intro/intro_qr.cpp @@ -32,24 +32,6 @@ namespace Intro { namespace details { namespace { -[[nodiscard]] QImage TelegramLogoImage() { - const auto size = QSize(st::introQrCenterSize, st::introQrCenterSize); - auto result = QImage( - size * style::DevicePixelRatio(), - QImage::Format_ARGB32_Premultiplied); - result.fill(Qt::transparent); - result.setDevicePixelRatio(style::DevicePixelRatio()); - { - auto p = QPainter(&result); - auto hq = PainterHighQualityEnabler(p); - p.setBrush(st::activeButtonBg); - p.setPen(Qt::NoPen); - p.drawEllipse(QRect(QPoint(), size)); - st::introQrPlane.paintInCenter(p, QRect(QPoint(), size)); - } - return result; -} - [[nodiscard]] QImage TelegramQrExact(const Qr::Data &data, int pixel) { return Qr::Generate(data, pixel, st::windowFg->c); } @@ -430,5 +412,23 @@ void QrWidget::cancelled() { api().request(base::take(_requestId)).cancel(); } +QImage TelegramLogoImage() { + const auto size = QSize(st::introQrCenterSize, st::introQrCenterSize); + auto result = QImage( + size * style::DevicePixelRatio(), + QImage::Format_ARGB32_Premultiplied); + result.fill(Qt::transparent); + result.setDevicePixelRatio(style::DevicePixelRatio()); + { + auto p = QPainter(&result); + auto hq = PainterHighQualityEnabler(p); + p.setBrush(st::activeButtonBg); + p.setPen(Qt::NoPen); + p.drawEllipse(QRect(QPoint(), size)); + st::introQrPlane.paintInCenter(p, QRect(QPoint(), size)); + } + return result; +} + } // namespace details } // namespace Intro diff --git a/Telegram/SourceFiles/intro/intro_qr.h b/Telegram/SourceFiles/intro/intro_qr.h index 87b9cd800b..563a4a4a87 100644 --- a/Telegram/SourceFiles/intro/intro_qr.h +++ b/Telegram/SourceFiles/intro/intro_qr.h @@ -59,5 +59,7 @@ private: }; +[[nodiscard]] QImage TelegramLogoImage(); + } // namespace details } // namespace Intro