Added initial ability to share QR code from user profile.

This commit is contained in:
23rd 2024-09-07 00:33:11 +03:00
parent f05191e668
commit 29e97232d8
12 changed files with 464 additions and 7 deletions

View file

@ -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

View file

@ -0,0 +1 @@
<svg width="1000px" height="1000px" viewBox="0 0 1000 1000" version="1.1" xmlns="http://www.w3.org/2000/svg"><g id="Artboard" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><path d="M226.328419,494.722069 C372.088573,431.216685 469.284839,389.350049 517.917216,369.122161 C656.772535,311.36743 685.625481,301.334815 704.431427,301.003532 C708.567621,300.93067 717.815839,301.955743 723.806446,306.816707 C728.864797,310.92121 730.256552,316.46581 730.922551,320.357329 C731.588551,324.248848 732.417879,333.113828 731.758626,340.040666 C724.234007,419.102486 691.675104,610.964674 675.110982,699.515267 C668.10208,736.984342 654.301336,749.547532 640.940618,750.777006 C611.904684,753.448938 589.856115,731.588035 561.733393,713.153237 C517.726886,684.306416 492.866009,666.349181 450.150074,638.200013 C400.78442,605.66878 432.786119,587.789048 460.919462,558.568563 C468.282091,550.921423 596.21508,434.556479 598.691227,424.000355 C599.00091,422.680135 599.288312,417.758981 596.36474,415.160431 C593.441168,412.561881 589.126229,413.450484 586.012448,414.157198 C581.598758,415.158943 511.297793,461.625274 375.109553,553.556189 C355.154858,567.258623 337.080515,573.934908 320.886524,573.585046 C303.033948,573.199351 268.692754,563.490928 243.163606,555.192408 C211.851067,545.013936 186.964484,539.632504 189.131547,522.346309 C190.260287,513.342589 202.659244,504.134509 226.328419,494.722069 Z" id="Path-3" fill="#FFFFFF"></path></g></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 641 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -31,6 +31,7 @@
<file alias="topic_icons/gray.svg">../../art/topic_icons/gray.svg</file>
<file alias="topic_icons/general.svg">../../art/topic_icons/general.svg</file>
<file alias="links_subscription.svg">../../icons/info/edit/links_subscription.svg</file>
<file alias="plane_white.svg">../../icons/plane_white.svg</file>
</qresource>
<qresource prefix="/icons">
<file alias="calls/hands.lottie">../../icons/calls/hands.lottie</file>

View file

@ -1120,3 +1120,8 @@ moderateBoxDividerLabel: FlatLabel(boxDividerLabel) {
selectLinkFg: windowActiveTextFg;
}
}
profileQrCenterSize: 34px;
profileQrBackgroundRadius: 12px;
profileQrIcon: icon{{ "qr_mini", windowActiveTextFg }};
profileQrBackgroundSkip: 36px;

View file

@ -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 }};

View file

@ -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<Ui::RpWidget> DetailsFiller::setupInfo() {
usernameLine.text->setContextMenuHook(hook);
usernameLine.subtext->setContextMenuHook(hook);
const auto usernameLabel = usernameLine.text;
if (user->isBot()) {
const auto copyUsername = Ui::CreateChild<Ui::IconButton>(
if (user) {
const auto copyUsername = user->isBot()
? Ui::CreateChild<Ui::IconButton>(
usernameLabel->parentWidget(),
st::infoProfileLabeledButtonCopy);
st::infoProfileLabeledButtonCopy)
: Ui::CreateChild<Ui::IconButton>(
usernameLabel->parentWidget(),
st::infoProfileLabeledButtonQr);
result->sizeValue(
) | rpl::start_with_next([=] {
const auto s = usernameLabel->parentWidget()->size();
@ -1114,6 +1119,12 @@ object_ptr<Ui::RpWidget> DetailsFiller::setupInfo() {
(s.height() - copyUsername->height()) / 2);
}, copyUsername->lifetime());
copyUsername->setClickedCallback([=] {
if (!user->isBot()) {
controller->show(Box([=](not_null<Ui::GenericBox*> box) {
Ui::FillProfileQrBox(box, user);
}));
return false;
}
const auto link = user->session().createInternalLinkFull(
user->username());
if (!link.isEmpty()) {

View file

@ -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 <QtCore/QMimeData>
#include <QtGui/QGuiApplication>
#include <QtSvg/QSvgRenderer>
namespace Ui {
namespace {
using Colors = std::vector<QColor>;
[[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<Ui::RpWidget*> PrepareQrWidget(
not_null<Ui::VerticalLayout*> container,
not_null<Ui::RpWidget*> topWidget,
rpl::producer<TextWithEntities> codes,
rpl::producer<QString> links,
rpl::producer<std::vector<QColor>> bgs) {
const auto divider = container->add(
object_ptr<Ui::BoxContentDivider>(container));
struct State final {
explicit State(Fn<void()> callback) : updating(callback) {
updating.start();
}
Ui::Animations::Basic updating;
QImage qr;
std::vector<QColor> bgs;
rpl::variable<TextWithEntities> code;
rpl::variable<QString> link;
};
auto palettes = rpl::single(rpl::empty) | rpl::then(
style::PaletteChanged()
);
const auto result = Ui::CreateChild<Ui::RpWidget>(divider);
topWidget->setParent(result);
topWidget->setAttribute(Qt::WA_TransparentForMouseEvents);
const auto state = result->lifetime().make_state<State>(
[=] { result->update(); });
state->code = rpl::variable<TextWithEntities>(std::move(codes));
state->link = rpl::variable<QString>(std::move(links));
std::move(
bgs
) | rpl::start_with_next([=](const std::vector<QColor> &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<Ui::GenericBox*> box,
not_null<PeerData*> 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<std::vector<QColor>> bgs;
Ui::Animations::Simple animation;
rpl::variable<int> chosen = 0;
};
const auto state = box->lifetime().make_state<State>();
const auto qr = PrepareQrWidget(
box->verticalLayout(),
Ui::CreateChild<Ui::UserpicButton>(
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<Ui::VerticalLayout>(box));
const auto activewidth = int(
(st::defaultInputField.borderActive + st::lineWidth) * 0.9);
const auto size = st::chatThemePreviewSize.width();
const auto fill = [=](const std::vector<Data::CloudTheme> &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<QColor>();
if (colors.size() != 4) {
continue;
}
if (state->bgs.current().empty()) {
state->bgs = colors;
}
if (counter % kMaxInRow == 0) {
row = themesContainer->add(
object_ptr<Ui::RpWidget>(themesContainer));
row->resize(size, size);
}
const auto widget = Ui::CreateChild<Ui::AbstractButton>(row);
const auto widgetState = widget->lifetime().make_state<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<QMimeData>();
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

View file

@ -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<Ui::GenericBox*> box,
not_null<PeerData*> peer);
} // namespace Ui

@ -1 +1 @@
Subproject commit 501f4c3502fd872ab4d777df8911bdac32de7c48
Subproject commit 6fdf60461444ba150e13ac36009c0ffce72c4c83