diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt
index a9e84983a..0e3f062fb 100644
--- a/Telegram/CMakeLists.txt
+++ b/Telegram/CMakeLists.txt
@@ -1482,6 +1482,8 @@ PRIVATE
support/support_templates.h
ui/boxes/edit_invite_link_session.cpp
ui/boxes/edit_invite_link_session.h
+ ui/boxes/profile_qr_box.cpp
+ ui/boxes/profile_qr_box.h
ui/chat/attach/attach_item_single_file_preview.cpp
ui/chat/attach/attach_item_single_file_preview.h
ui/chat/attach/attach_item_single_media_preview.cpp
diff --git a/Telegram/Resources/icons/plane_white.svg b/Telegram/Resources/icons/plane_white.svg
new file mode 100644
index 000000000..2a8761c70
--- /dev/null
+++ b/Telegram/Resources/icons/plane_white.svg
@@ -0,0 +1 @@
+
diff --git a/Telegram/Resources/icons/qr_mini.png b/Telegram/Resources/icons/qr_mini.png
new file mode 100644
index 000000000..b26188bcd
Binary files /dev/null and b/Telegram/Resources/icons/qr_mini.png differ
diff --git a/Telegram/Resources/icons/qr_mini@2x.png b/Telegram/Resources/icons/qr_mini@2x.png
new file mode 100644
index 000000000..e3e3992d2
Binary files /dev/null and b/Telegram/Resources/icons/qr_mini@2x.png differ
diff --git a/Telegram/Resources/icons/qr_mini@3x.png b/Telegram/Resources/icons/qr_mini@3x.png
new file mode 100644
index 000000000..46074860a
Binary files /dev/null and b/Telegram/Resources/icons/qr_mini@3x.png differ
diff --git a/Telegram/Resources/qrc/telegram/telegram.qrc b/Telegram/Resources/qrc/telegram/telegram.qrc
index c065c5fad..09f8b0fac 100644
--- a/Telegram/Resources/qrc/telegram/telegram.qrc
+++ b/Telegram/Resources/qrc/telegram/telegram.qrc
@@ -31,6 +31,7 @@
../../art/topic_icons/gray.svg
../../art/topic_icons/general.svg
../../icons/info/edit/links_subscription.svg
+ ../../icons/plane_white.svg
../../icons/calls/hands.lottie
diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style
index 7fb0f669a..9650b6b88 100644
--- a/Telegram/SourceFiles/boxes/boxes.style
+++ b/Telegram/SourceFiles/boxes/boxes.style
@@ -1120,3 +1120,8 @@ moderateBoxDividerLabel: FlatLabel(boxDividerLabel) {
selectLinkFg: windowActiveTextFg;
}
}
+
+profileQrCenterSize: 34px;
+profileQrBackgroundRadius: 12px;
+profileQrIcon: icon{{ "qr_mini", windowActiveTextFg }};
+profileQrBackgroundSkip: 36px;
diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style
index 2978d66c4..3be1bd88a 100644
--- a/Telegram/SourceFiles/info/info.style
+++ b/Telegram/SourceFiles/info/info.style
@@ -433,12 +433,16 @@ infoProfileSeparatorPadding: margins(
infoProfileLabeledButtonCopy: IconButton(defaultIconButton) {
width: 34px;
height: 34px;
- icon: icon {{ "menu/copy", infoIconFg }};
- iconOver: icon {{ "menu/copy", infoIconFg }};
+ icon: icon {{ "menu/copy", windowBgActive }};
+ iconOver: icon {{ "menu/copy", windowBgActive }};
rippleAreaPosition: point(0px, 0px);
rippleAreaSize: 34px;
ripple: defaultRippleAnimation;
}
+infoProfileLabeledButtonQr: IconButton(infoProfileLabeledButtonCopy) {
+ icon: icon {{ "menu/qr_code", windowBgActive }};
+ iconOver: icon {{ "menu/qr_code", windowBgActive }};
+}
infoIconInformation: icon {{ "info/info_information", infoIconFg }};
infoIconAddMember: icon {{ "info/info_add_member", infoIconFg }};
diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp
index e79a645d6..8b219334b 100644
--- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp
+++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp
@@ -51,6 +51,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "main/main_session.h"
#include "menu/menu_mute.h"
#include "support/support_helper.h"
+#include "ui/boxes/profile_qr_box.h"
#include "ui/boxes/report_box.h"
#include "ui/controls/userpic_button.h"
#include "ui/painter.h"
@@ -1102,10 +1103,14 @@ object_ptr DetailsFiller::setupInfo() {
usernameLine.text->setContextMenuHook(hook);
usernameLine.subtext->setContextMenuHook(hook);
const auto usernameLabel = usernameLine.text;
- if (user->isBot()) {
- const auto copyUsername = Ui::CreateChild(
- usernameLabel->parentWidget(),
- st::infoProfileLabeledButtonCopy);
+ if (user) {
+ const auto copyUsername = user->isBot()
+ ? Ui::CreateChild(
+ usernameLabel->parentWidget(),
+ st::infoProfileLabeledButtonCopy)
+ : Ui::CreateChild(
+ usernameLabel->parentWidget(),
+ st::infoProfileLabeledButtonQr);
result->sizeValue(
) | rpl::start_with_next([=] {
const auto s = usernameLabel->parentWidget()->size();
@@ -1114,6 +1119,12 @@ object_ptr DetailsFiller::setupInfo() {
(s.height() - copyUsername->height()) / 2);
}, copyUsername->lifetime());
copyUsername->setClickedCallback([=] {
+ if (!user->isBot()) {
+ controller->show(Box([=](not_null box) {
+ Ui::FillProfileQrBox(box, user);
+ }));
+ return false;
+ }
const auto link = user->session().createInternalLinkFull(
user->username());
if (!link.isEmpty()) {
diff --git a/Telegram/SourceFiles/ui/boxes/profile_qr_box.cpp b/Telegram/SourceFiles/ui/boxes/profile_qr_box.cpp
new file mode 100644
index 000000000..fbfa09605
--- /dev/null
+++ b/Telegram/SourceFiles/ui/boxes/profile_qr_box.cpp
@@ -0,0 +1,413 @@
+/*
+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 "ui/boxes/profile_qr_box.h"
+
+#include "core/application.h"
+#include "data/data_cloud_themes.h"
+#include "data/data_peer.h"
+#include "data/data_session.h"
+#include "data/data_user.h"
+#include "info/profile/info_profile_values.h"
+#include "lang/lang_keys.h"
+#include "main/main_session.h"
+#include "qr/qr_generate.h"
+#include "ui/controls/userpic_button.h"
+#include "ui/effects/animations.h"
+#include "ui/image/image_prepare.h"
+#include "ui/layers/generic_box.h"
+#include "ui/painter.h"
+#include "ui/rect.h"
+#include "ui/ui_utility.h"
+#include "ui/vertical_list.h"
+#include "ui/widgets/box_content_divider.h"
+#include "ui/widgets/buttons.h"
+#include "ui/wrap/vertical_layout.h"
+#include "window/window_controller.h"
+#include "window/window_session_controller.h"
+#include "styles/style_boxes.h"
+#include "styles/style_giveaway.h"
+#include "styles/style_intro.h"
+#include "styles/style_layers.h"
+#include "styles/style_settings.h"
+#include "styles/style_widgets.h"
+#include "styles/style_window.h"
+
+#include
+#include
+#include
+
+namespace Ui {
+namespace {
+
+using Colors = std::vector;
+
+[[nodiscard]] QImage TelegramQr(const Qr::Data &data, int pixel, int max) {
+ Expects(data.size > 0);
+
+ if (max > 0 && data.size * pixel > max) {
+ pixel = std::max(max / data.size, 1);
+ }
+ auto qr = Qr::Generate(
+ data,
+ pixel * style::DevicePixelRatio(),
+ Qt::transparent,
+ Qt::white);
+ {
+ auto p = QPainter(&qr);
+ auto hq = PainterHighQualityEnabler(p);
+ auto svg = QSvgRenderer(u":/gui/plane_white.svg"_q);
+ const auto size = qr.rect().size();
+ const auto centerWidth = st::profileQrCenterSize
+ * style::DevicePixelRatio();
+ const auto centerRect = Rect(size)
+ - Margins((size.width() - centerWidth) / 2);
+ p.setPen(Qt::NoPen);
+ p.setBrush(Qt::white);
+ p.setCompositionMode(QPainter::CompositionMode_Clear);
+ p.drawEllipse(centerRect);
+ p.setCompositionMode(QPainter::CompositionMode_SourceOver);
+ svg.render(&p, centerRect);
+ }
+ return qr;
+}
+
+[[nodiscard]] not_null PrepareQrWidget(
+ not_null container,
+ not_null topWidget,
+ rpl::producer codes,
+ rpl::producer links,
+ rpl::producer> bgs) {
+ const auto divider = container->add(
+ object_ptr(container));
+ struct State final {
+ explicit State(Fn callback) : updating(callback) {
+ updating.start();
+ }
+
+ Ui::Animations::Basic updating;
+ QImage qr;
+ std::vector bgs;
+ rpl::variable code;
+ rpl::variable link;
+ };
+ auto palettes = rpl::single(rpl::empty) | rpl::then(
+ style::PaletteChanged()
+ );
+ const auto result = Ui::CreateChild(divider);
+ topWidget->setParent(result);
+ topWidget->setAttribute(Qt::WA_TransparentForMouseEvents);
+ const auto state = result->lifetime().make_state(
+ [=] { result->update(); });
+ state->code = rpl::variable(std::move(codes));
+ state->link = rpl::variable(std::move(links));
+ std::move(
+ bgs
+ ) | rpl::start_with_next([=](const std::vector &bgs) {
+ state->bgs = bgs;
+ }, container->lifetime());
+ const auto font = st::mainMenuResetScaleFont;
+ const auto textMaxHeight = font->height * 3;
+ const auto qrMaxSize = st::boxWideWidth
+ - rect::m::sum::h(st::boxRowPadding)
+ - 2 * st::profileQrBackgroundSkip;
+ result->resize(
+ qrMaxSize + 2 * st::profileQrBackgroundSkip,
+ qrMaxSize + 2 * st::profileQrBackgroundSkip + textMaxHeight * 2);
+ rpl::combine(
+ state->link.value() | rpl::map([](const QString &code) {
+ return Qr::Encode(code.toUtf8(), Qr::Redundancy::Default);
+ }),
+ rpl::duplicate(palettes)
+ ) | rpl::map([=](const Qr::Data &code, const auto &) {
+ return TelegramQr(code, st::introQrPixel, qrMaxSize);
+ }) | rpl::start_with_next([=](QImage &&image) {
+ state->qr = std::move(image);
+ }, result->lifetime());
+ result->paintRequest(
+ ) | rpl::start_with_next([=](QRect clip) {
+ auto p = QPainter(result);
+ const auto usualSize = 41;
+ const auto pixel = std::clamp(
+ qrMaxSize / usualSize,
+ 1,
+ st::introQrPixel);
+ const auto size = (state->qr.size() / style::DevicePixelRatio());
+ const auto radius = st::profileQrBackgroundRadius;
+ const auto skip = st::profileQrBackgroundSkip;
+ const auto qr = QRect(
+ (result->width() - size.width()) / 2,
+ skip * 3,
+ size.width(),
+ size.height());
+ auto hq = PainterHighQualityEnabler(p);
+ p.setPen(Qt::NoPen);
+ p.setBrush(Qt::white);
+ p.drawRoundedRect(
+ qr + QMargins(skip, skip + skip / 2, skip, skip + textMaxHeight),
+ radius,
+ radius);
+ if (!state->qr.isNull() && !state->bgs.empty()) {
+ constexpr auto kDuration = crl::time(10000);
+ const auto angle = (crl::now() % kDuration)
+ / float64(kDuration) * 360.0;
+ const auto gradientRotation = int(angle / 45.) * 45;
+ const auto gradientRotationAdd = angle - gradientRotation;
+
+ const auto center = QPointF(rect::center(qr));
+ const auto radius = std::sqrt(std::pow(qr.width() / 2., 2)
+ + std::pow(qr.height() / 2., 2));
+ auto back = Images::GenerateGradient(
+ qr.size(),
+ state->bgs,
+ gradientRotation,
+ 1. - (gradientRotationAdd / 45.));
+ p.drawImage(qr, back);
+ const auto coloredSize = QSize(back.width(), textMaxHeight);
+ auto colored = QImage(
+ coloredSize * style::DevicePixelRatio(),
+ QImage::Format_ARGB32_Premultiplied);
+ colored.setDevicePixelRatio(style::DevicePixelRatio());
+ colored.fill(Qt::transparent);
+ {
+ // '@' + QString(32, 'W');
+ auto p = QPainter(&colored);
+ auto hq = PainterHighQualityEnabler(p);
+ p.setPen(Qt::red);
+ p.setFont(font);
+ auto option = QTextOption(style::al_center);
+ option.setWrapMode(QTextOption::WrapAnywhere);
+ p.drawText(
+ Rect(coloredSize),
+ state->code.current().text.toUpper(),
+ option);
+ p.setCompositionMode(QPainter::CompositionMode_SourceIn);
+ p.drawImage(0, -back.height() + textMaxHeight, back);
+ }
+ p.drawImage(qr, state->qr);
+ p.drawImage(qr.x(), qr.y() + qr.height() + skip / 2, colored);
+ }
+ }, result->lifetime());
+ result->sizeValue(
+ ) | rpl::start_with_next([=](const QSize &size) {
+ divider->resize(container->width(), size.height());
+ result->moveToLeft((container->width() - size.width()) / 2, 0);
+
+ const auto qrHeight = state->qr.height() / style::DevicePixelRatio();
+ topWidget->moveToLeft(
+ (result->width() - topWidget->width()) / 2,
+ (st::profileQrBackgroundSkip
+ + st::profileQrBackgroundSkip / 2
+ - topWidget->height() / 2));
+ topWidget->raise();
+ }, divider->lifetime());
+ return result;
+}
+
+} // namespace
+
+void FillProfileQrBox(
+ not_null box,
+ not_null peer) {
+ const auto window = Core::App().findWindow(box);
+ const auto controller = window ? window->sessionController() : nullptr;
+ if (!controller) {
+ return;
+ }
+ box->setStyle(st::giveawayGiftCodeBox);
+ box->setNoContentMargin(true);
+ box->setWidth(st::aboutWidth);
+ box->setTitle(tr::lng_group_invite_context_qr());
+ box->verticalLayout()->resizeToWidth(box->width());
+ struct State {
+ rpl::variable> bgs;
+ Ui::Animations::Simple animation;
+ rpl::variable chosen = 0;
+ };
+ const auto state = box->lifetime().make_state();
+ const auto qr = PrepareQrWidget(
+ box->verticalLayout(),
+ Ui::CreateChild(
+ box,
+ peer,
+ st::defaultUserpicButton),
+ Info::Profile::UsernameValue(peer->asUser()),
+ Info::Profile::LinkValue(peer) | rpl::map([](const auto &link) {
+ return link.url;
+ }),
+ state->bgs.value());
+ const auto themesContainer = box->addRow(
+ object_ptr(box));
+
+ const auto activewidth = int(
+ (st::defaultInputField.borderActive + st::lineWidth) * 0.9);
+ const auto size = st::chatThemePreviewSize.width();
+
+ const auto fill = [=](const std::vector &cloudThemes) {
+ while (themesContainer->count()) {
+ delete themesContainer->widgetAt(0);
+ }
+ Ui::AddSkip(themesContainer);
+ Ui::AddSkip(themesContainer);
+ Ui::AddSkip(themesContainer);
+ Ui::AddSkip(themesContainer);
+ struct State {
+ Colors colors;
+ QImage image;
+ };
+ constexpr auto kMaxInRow = 4;
+ auto row = (Ui::RpWidget*)(nullptr);
+ auto counter = 0;
+ const auto spacing = (0
+ + (box->width() - rect::m::sum::h(st::boxRowPadding))
+ - (kMaxInRow * size)) / (kMaxInRow + 1);
+
+ for (const auto &cloudTheme : cloudThemes) {
+ const auto it = cloudTheme.settings.find(
+ Data::CloudThemeType::Light);
+ if (it == end(cloudTheme.settings)) {
+ continue;
+ }
+ const auto colors = it->second.paper
+ ? it->second.paper->backgroundColors()
+ : std::vector();
+ if (colors.size() != 4) {
+ continue;
+ }
+ if (state->bgs.current().empty()) {
+ state->bgs = colors;
+ }
+
+ if (counter % kMaxInRow == 0) {
+ row = themesContainer->add(
+ object_ptr(themesContainer));
+ row->resize(size, size);
+ }
+ const auto widget = Ui::CreateChild(row);
+ const auto widgetState = widget->lifetime().make_state();
+ widget->setClickedCallback([=] {
+ state->chosen = counter;
+ widget->update();
+ state->animation.stop();
+ state->animation.start([=](float64 value) {
+ const auto was = state->bgs.current();
+ const auto now = colors;
+ if (was.size() == now.size(); was.size() == 4) {
+ state->bgs = Colors({
+ anim::color(was[0], now[0], value),
+ anim::color(was[1], now[1], value),
+ anim::color(was[2], now[2], value),
+ anim::color(was[3], now[3], value),
+ });
+ }
+ },
+ 0.,
+ 1.,
+ st::shakeDuration);
+ });
+ state->chosen.value() | rpl::combine_previous(
+ ) | rpl::filter([=](int i, int k) {
+ return i == counter || k == counter;
+ }) | rpl::start_with_next([=] {
+ widget->update();
+ }, widget->lifetime());
+ widget->resize(size, size);
+ widget->moveToLeft(
+ spacing + ((counter % kMaxInRow) * (size + spacing)),
+ 0);
+ widget->show();
+ const auto back = [&] {
+ auto result = Images::Round(
+ Images::GenerateGradient(
+ Size(size - activewidth * 5),
+ colors,
+ 0,
+ 0),
+ ImageRoundRadius::Large);
+ auto colored = result;
+ colored.fill(Qt::transparent);
+ {
+ auto p = QPainter(&colored);
+ auto hq = PainterHighQualityEnabler(p);
+ st::profileQrIcon.paintInCenter(p, result.rect());
+ p.setCompositionMode(QPainter::CompositionMode_SourceIn);
+ p.drawImage(0, 0, result);
+ }
+ auto temp = result;
+ temp.fill(Qt::transparent);
+ {
+ auto p = QPainter(&temp);
+ auto hq = PainterHighQualityEnabler(p);
+ p.setPen(st::premiumButtonFg);
+ p.setBrush(st::premiumButtonFg);
+ const auto size = st::profileQrIcon.width() * 1.5;
+ const auto margins = Margins((result.width() - size) / 2);
+ const auto inner = result.rect() - margins;
+ p.drawRoundedRect(
+ inner,
+ st::roundRadiusLarge,
+ st::roundRadiusLarge);
+ p.drawImage(0, 0, colored);
+ }
+ {
+ auto p = QPainter(&result);
+ p.drawImage(0, 0, temp);
+ }
+ return result;
+ }();
+ widget->paintRequest() | rpl::start_with_next([=] {
+ auto p = QPainter(widget);
+ const auto rect = widget->rect() - Margins(activewidth * 2.5);
+ p.drawImage(rect.x(), rect.y(), back);
+ if (state->chosen.current() == counter) {
+ auto hq = PainterHighQualityEnabler(p);
+ auto pen = st::activeLineFg->p;
+ pen.setWidth(st::defaultInputField.borderActive);
+ p.setPen(pen);
+ p.drawRoundedRect(
+ widget->rect() - Margins(pen.width()),
+ st::roundRadiusLarge + activewidth * 4.2,
+ st::roundRadiusLarge + activewidth * 4.2);
+ }
+ }, widget->lifetime());
+ if ((++counter) >= kMaxInRow) {
+ Ui::AddSkip(themesContainer);
+ }
+ }
+ themesContainer->resizeToWidth(box->width());
+ };
+
+ const auto themes = &controller->session().data().cloudThemes();
+ const auto &list = themes->chatThemes();
+ if (!list.empty()) {
+ fill(list);
+ } else {
+ themes->refreshChatThemes();
+ themes->chatThemesUpdated(
+ ) | rpl::take(1) | rpl::start_with_next([=] {
+ fill(themes->chatThemes());
+ }, box->lifetime());
+ }
+
+ const auto show = controller->uiShow();
+ const auto button = box->addButton(tr::lng_chat_link_copy(), [=] {
+ auto mime = std::make_unique();
+ mime->setImageData(Ui::GrabWidget(qr, {}, Qt::transparent));
+ QGuiApplication::clipboard()->setMimeData(mime.release());
+ show->showToast(tr::lng_group_invite_qr_copied(tr::now));
+ });
+ const auto buttonWidth = box->width()
+ - rect::m::sum::h(st::giveawayGiftCodeBox.buttonPadding);
+ button->widthValue() | rpl::filter([=] {
+ return (button->widthNoMargins() != buttonWidth);
+ }) | rpl::start_with_next([=] {
+ button->resizeToWidth(buttonWidth);
+ }, button->lifetime());
+ box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); });
+}
+
+} // namespace Ui
diff --git a/Telegram/SourceFiles/ui/boxes/profile_qr_box.h b/Telegram/SourceFiles/ui/boxes/profile_qr_box.h
new file mode 100644
index 000000000..e942f2850
--- /dev/null
+++ b/Telegram/SourceFiles/ui/boxes/profile_qr_box.h
@@ -0,0 +1,20 @@
+/*
+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
+*/
+#pragma once
+
+class PeerData;
+
+namespace Ui {
+
+class GenericBox;
+
+void FillProfileQrBox(
+ not_null box,
+ not_null peer);
+
+} // namespace Ui
diff --git a/Telegram/lib_qr b/Telegram/lib_qr
index 501f4c350..6fdf60461 160000
--- a/Telegram/lib_qr
+++ b/Telegram/lib_qr
@@ -1 +1 @@
-Subproject commit 501f4c3502fd872ab4d777df8911bdac32de7c48
+Subproject commit 6fdf60461444ba150e13ac36009c0ffce72c4c83