Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dab107cf90 | ||
|
|
d1d1aa3d21 | ||
|
|
d0536cc31f | ||
|
|
c47f5e9995 | ||
|
|
0916836ff9 | ||
|
|
5dd9ff1062 | ||
|
|
d1e3b9f15d | ||
|
|
ca3c179b75 | ||
|
|
c2d5924508 | ||
|
|
016d0395c3 | ||
|
|
925c9239bd | ||
|
|
fdcbe3cf3a | ||
|
|
f6b9cc5ce1 | ||
|
|
8c915e6dc3 | ||
|
|
1db426da2e | ||
|
|
d9be363962 | ||
|
|
88daa37e34 | ||
|
|
931fa01337 | ||
|
|
8e1595eb29 | ||
|
|
29e97232d8 |
@@ -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/peer_qr_box.cpp
|
||||
ui/boxes/peer_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
|
||||
|
||||
1
Telegram/Resources/icons/plane_white.svg
Normal file
1
Telegram/Resources/icons/plane_white.svg
Normal 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 |
BIN
Telegram/Resources/icons/qr_mini.png
Normal file
BIN
Telegram/Resources/icons/qr_mini.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 641 B |
BIN
Telegram/Resources/icons/qr_mini@2x.png
Normal file
BIN
Telegram/Resources/icons/qr_mini@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
Telegram/Resources/icons/qr_mini@3x.png
Normal file
BIN
Telegram/Resources/icons/qr_mini@3x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@@ -5546,6 +5546,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"lng_contact_details_birthday" = "Birthday";
|
||||
"lng_contact_details_organization" = "Organization";
|
||||
|
||||
"lng_qr_box_quality" = "Quality";
|
||||
"lng_qr_box_quality1" = "Normal";
|
||||
"lng_qr_box_quality2" = "High";
|
||||
"lng_qr_box_quality3" = "Very High";
|
||||
|
||||
// Wnd specific
|
||||
|
||||
"lng_wnd_choose_program_menu" = "Choose Default Program...";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
|
||||
ProcessorArchitecture="ARCHITECTURE"
|
||||
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
|
||||
Version="5.5.2.0" />
|
||||
Version="5.5.3.0" />
|
||||
<Properties>
|
||||
<DisplayName>Telegram Desktop</DisplayName>
|
||||
<PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>
|
||||
|
||||
@@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 5,5,2,0
|
||||
PRODUCTVERSION 5,5,2,0
|
||||
FILEVERSION 5,5,3,0
|
||||
PRODUCTVERSION 5,5,3,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
@@ -62,10 +62,10 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Telegram FZ-LLC"
|
||||
VALUE "FileDescription", "Telegram Desktop"
|
||||
VALUE "FileVersion", "5.5.2.0"
|
||||
VALUE "FileVersion", "5.5.3.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2024"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "5.5.2.0"
|
||||
VALUE "ProductVersion", "5.5.3.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 5,5,2,0
|
||||
PRODUCTVERSION 5,5,2,0
|
||||
FILEVERSION 5,5,3,0
|
||||
PRODUCTVERSION 5,5,3,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
@@ -53,10 +53,10 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Telegram FZ-LLC"
|
||||
VALUE "FileDescription", "Telegram Desktop Updater"
|
||||
VALUE "FileVersion", "5.5.2.0"
|
||||
VALUE "FileVersion", "5.5.3.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2024"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "5.5.2.0"
|
||||
VALUE "ProductVersion", "5.5.3.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -1120,3 +1120,10 @@ moderateBoxDividerLabel: FlatLabel(boxDividerLabel) {
|
||||
selectLinkFg: windowActiveTextFg;
|
||||
}
|
||||
}
|
||||
|
||||
profileQrFont: font(fsize bold);
|
||||
profileQrCenterSize: 34px;
|
||||
profileQrBackgroundRadius: 12px;
|
||||
profileQrIcon: icon{{ "qr_mini", windowActiveTextFg }};
|
||||
profileQrBackgroundMargins: margins(36px, 12px, 36px, 12px);
|
||||
profileQrBackgroundPadding: margins(0px, 24px, 0px, 24px);
|
||||
|
||||
@@ -582,6 +582,7 @@ void LinkController::addLinkBlock(not_null<Ui::VerticalLayout*> container) {
|
||||
});
|
||||
const auto getLinkQr = crl::guard(weak, [=] {
|
||||
delegate()->peerListUiShow()->showBox(InviteLinkQrBox(
|
||||
nullptr,
|
||||
link,
|
||||
tr::lng_group_invite_qr_title(),
|
||||
tr::lng_filters_link_qr_about()));
|
||||
@@ -890,6 +891,7 @@ base::unique_qptr<Ui::PopupMenu> LinksController::createRowContextMenu(
|
||||
};
|
||||
const auto getLinkQr = [=] {
|
||||
delegate()->peerListUiShow()->showBox(InviteLinkQrBox(
|
||||
nullptr,
|
||||
link,
|
||||
tr::lng_group_invite_qr_title(),
|
||||
tr::lng_filters_link_qr_about()));
|
||||
|
||||
@@ -33,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "ui/boxes/edit_invite_link.h"
|
||||
#include "ui/boxes/edit_invite_link_session.h"
|
||||
#include "ui/boxes/peer_qr_box.h"
|
||||
#include "ui/controls/invite_link_buttons.h"
|
||||
#include "ui/controls/invite_link_label.h"
|
||||
#include "ui/controls/userpic_button.h"
|
||||
@@ -64,8 +65,8 @@ namespace {
|
||||
|
||||
constexpr auto kFirstPage = 20;
|
||||
constexpr auto kPerPage = 100;
|
||||
constexpr auto kShareQrSize = 768;
|
||||
constexpr auto kShareQrPadding = 16;
|
||||
// constexpr auto kShareQrSize = 768;
|
||||
// constexpr auto kShareQrPadding = 16;
|
||||
|
||||
using LinkData = Api::InviteLink;
|
||||
|
||||
@@ -282,6 +283,8 @@ private:
|
||||
return updated.link.isEmpty() || (!revoked && updated.revoked);
|
||||
}
|
||||
|
||||
#if 0
|
||||
|
||||
QImage QrExact(const Qr::Data &data, int pixel, QColor color) {
|
||||
const auto image = [](int size) {
|
||||
auto result = QImage(
|
||||
@@ -383,6 +386,8 @@ void QrBox(
|
||||
box->addLeftButton(tr::lng_group_invite_context_copy(), copyCallback);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
Controller::Controller(
|
||||
not_null<PeerData*> peer,
|
||||
not_null<UserData*> admin,
|
||||
@@ -421,6 +426,7 @@ void Controller::addHeaderBlock(not_null<Ui::VerticalLayout*> container) {
|
||||
});
|
||||
const auto getLinkQr = crl::guard(weak, [=] {
|
||||
delegate()->peerListUiShow()->showBox(InviteLinkQrBox(
|
||||
_peer,
|
||||
link,
|
||||
tr::lng_group_invite_qr_title(),
|
||||
tr::lng_group_invite_qr_about()));
|
||||
@@ -1253,6 +1259,7 @@ void AddPermanentLinkBlock(
|
||||
const auto getLinkQr = crl::guard(weak, [=] {
|
||||
if (const auto current = value->current(); !current.link.isEmpty()) {
|
||||
show->showBox(InviteLinkQrBox(
|
||||
peer,
|
||||
current.link,
|
||||
tr::lng_group_invite_qr_title(),
|
||||
tr::lng_group_invite_qr_about()));
|
||||
@@ -1510,16 +1517,14 @@ object_ptr<Ui::BoxContent> ShareInviteLinkBox(
|
||||
}
|
||||
|
||||
object_ptr<Ui::BoxContent> InviteLinkQrBox(
|
||||
PeerData *peer,
|
||||
const QString &link,
|
||||
rpl::producer<QString> title,
|
||||
rpl::producer<QString> about) {
|
||||
return Box(QrBox, link, std::move(title), std::move(about), [=](
|
||||
const QImage &image,
|
||||
std::shared_ptr<Ui::Show> show) {
|
||||
auto mime = std::make_unique<QMimeData>();
|
||||
mime->setImageData(image);
|
||||
QGuiApplication::clipboard()->setMimeData(mime.release());
|
||||
show->showToast(tr::lng_group_invite_qr_copied(tr::now));
|
||||
return Box([=, t = std::move(title), a = std::move(about)](
|
||||
not_null<Ui::GenericBox*> box) {
|
||||
Ui::FillPeerQrBox(box, peer, link, std::move(a));
|
||||
box->setTitle(std::move(t));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ void CopyInviteLink(std::shared_ptr<Ui::Show> show, const QString &link);
|
||||
const QString &link,
|
||||
const QString &copied = {});
|
||||
[[nodiscard]] object_ptr<Ui::BoxContent> InviteLinkQrBox(
|
||||
PeerData *peer,
|
||||
const QString &link,
|
||||
rpl::producer<QString> title,
|
||||
rpl::producer<QString> about);
|
||||
|
||||
@@ -587,6 +587,7 @@ base::unique_qptr<Ui::PopupMenu> LinksController::createRowContextMenu(
|
||||
}, &st::menuIconShare);
|
||||
result->addAction(tr::lng_group_invite_context_qr(tr::now), [=] {
|
||||
delegate()->peerListUiShow()->showBox(InviteLinkQrBox(
|
||||
nullptr,
|
||||
link,
|
||||
tr::lng_group_invite_qr_title(),
|
||||
tr::lng_group_invite_qr_about()));
|
||||
|
||||
@@ -32,7 +32,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "boxes/premium_limits_box.h"
|
||||
#include "boxes/premium_preview_box.h"
|
||||
#include "boxes/send_credits_box.h"
|
||||
#include "platform/platform_file_utilities.h"
|
||||
#include "ui/effects/scroll_content_shadow.h"
|
||||
#include "ui/widgets/fields/number_input.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
@@ -72,7 +71,7 @@ constexpr auto kMaxMessageLength = 4096;
|
||||
using Ui::SendFilesWay;
|
||||
|
||||
[[nodiscard]] inline bool CanAddUrls(const QList<QUrl> &urls) {
|
||||
return !urls.isEmpty() && ranges::all_of(urls, Core::UrlIsLocal);
|
||||
return !urls.isEmpty() && ranges::all_of(urls, &QUrl::isLocalFile);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool CanAddFiles(not_null<const QMimeData*> data) {
|
||||
|
||||
@@ -7,10 +7,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace Core {
|
||||
bool UrlIsLocal(const QUrl &url);
|
||||
} // namespace Core
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
@@ -49,7 +45,7 @@ void ShowInFolder(const QString &filepath);
|
||||
namespace internal {
|
||||
|
||||
inline QString UrlToLocalDefault(const QUrl &url) {
|
||||
return Core::UrlIsLocal(url) ? url.toLocalFile() : QString();
|
||||
return url.toLocalFile();
|
||||
}
|
||||
|
||||
void UnsafeOpenUrlDefault(const QString &url);
|
||||
|
||||
@@ -226,24 +226,13 @@ bool CanSendFiles(not_null<const QMimeData*> data) {
|
||||
if (data->hasImage()) {
|
||||
return true;
|
||||
} else if (const auto urls = ReadMimeUrls(data); !urls.empty()) {
|
||||
if (ranges::all_of(urls, UrlIsLocal)) {
|
||||
if (ranges::all_of(urls, &QUrl::isLocalFile)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UrlIsLocal(const QUrl &url) {
|
||||
if (!url.isLocalFile()) {
|
||||
return false;
|
||||
}
|
||||
const auto result = url.toLocalFile();
|
||||
if (result.startsWith("//")) {
|
||||
return false;
|
||||
}
|
||||
return !result.isEmpty();
|
||||
}
|
||||
|
||||
QString FileExtension(const QString &filepath) {
|
||||
const auto reversed = ranges::views::reverse(filepath);
|
||||
const auto last = ranges::find_first_of(reversed, ".\\/");
|
||||
|
||||
@@ -68,7 +68,6 @@ struct MimeImageData {
|
||||
[[nodiscard]] QString ReadMimeText(not_null<const QMimeData*> data);
|
||||
[[nodiscard]] QList<QUrl> ReadMimeUrls(not_null<const QMimeData*> data);
|
||||
[[nodiscard]] bool CanSendFiles(not_null<const QMimeData*> data);
|
||||
[[nodiscard]] bool UrlIsLocal(const QUrl &url);
|
||||
|
||||
enum class NameType : uchar {
|
||||
Unknown,
|
||||
|
||||
@@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D1ED}"_cs;
|
||||
constexpr auto AppNameOld = "Telegram Win (Unofficial)"_cs;
|
||||
constexpr auto AppName = "Telegram Desktop"_cs;
|
||||
constexpr auto AppFile = "Telegram"_cs;
|
||||
constexpr auto AppVersion = 5005002;
|
||||
constexpr auto AppVersionStr = "5.5.2";
|
||||
constexpr auto AppVersion = 5005003;
|
||||
constexpr auto AppVersionStr = "5.5.3";
|
||||
constexpr auto AppBetaVersion = false;
|
||||
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;
|
||||
|
||||
@@ -5786,7 +5786,7 @@ bool HistoryWidget::canSendFiles(not_null<const QMimeData*> data) const {
|
||||
} else if (data->hasImage()) {
|
||||
return true;
|
||||
} else if (const auto urls = Core::ReadMimeUrls(data); !urls.empty()) {
|
||||
if (ranges::all_of(urls, Core::UrlIsLocal)) {
|
||||
if (ranges::all_of(urls, &QUrl::isLocalFile)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -931,6 +931,7 @@ void Document::draw(
|
||||
.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),
|
||||
.selection = selection,
|
||||
.highlight = highlightRequest ? &*highlightRequest : nullptr,
|
||||
.useFullWidth = true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,6 +280,7 @@ void Game::draw(Painter &p, const PaintContext &context) const {
|
||||
.selection = toDescriptionSelection(context.selection),
|
||||
.elisionHeight = _descriptionLines * lineHeight,
|
||||
.elisionRemoveFromEnd = endskip,
|
||||
.useFullWidth = true,
|
||||
});
|
||||
tshift += _descriptionLines * lineHeight;
|
||||
}
|
||||
|
||||
@@ -245,6 +245,7 @@ void Invoice::draw(Painter &p, const PaintContext &context) const {
|
||||
.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),
|
||||
.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),
|
||||
.selection = toDescriptionSelection(context.selection),
|
||||
.useFullWidth = true,
|
||||
});
|
||||
tshift += _descriptionHeight;
|
||||
}
|
||||
|
||||
@@ -1085,6 +1085,7 @@ void WebPage::draw(Painter &p, const PaintContext &context) const {
|
||||
? (_descriptionLines * lineHeight)
|
||||
: 0),
|
||||
.elisionRemoveFromEnd = (_descriptionLines > 0) ? endskip : 0,
|
||||
.useFullWidth = true,
|
||||
});
|
||||
tshift += (_descriptionLines > 0)
|
||||
? (_descriptionLines * lineHeight)
|
||||
|
||||
@@ -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 }};
|
||||
|
||||
@@ -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/peer_qr_box.h"
|
||||
#include "ui/boxes/report_box.h"
|
||||
#include "ui/controls/userpic_button.h"
|
||||
#include "ui/painter.h"
|
||||
@@ -1034,6 +1035,22 @@ object_ptr<Ui::RpWidget> DetailsFiller::setupInfo() {
|
||||
result.text->setContextCopyText(contextCopyText);
|
||||
return result;
|
||||
};
|
||||
const auto fitLabelToButton = [&](
|
||||
not_null<Ui::RpWidget*> button,
|
||||
not_null<Ui::FlatLabel*> label) {
|
||||
const auto parent = label->parentWidget();
|
||||
result->sizeValue() | rpl::start_with_next([=] {
|
||||
const auto s = parent->size();
|
||||
button->moveToRight(
|
||||
0,
|
||||
(s.height() - button->height()) / 2);
|
||||
label->resizeToWidth(
|
||||
s.width()
|
||||
- label->geometry().left()
|
||||
- st::lineWidth * 2
|
||||
- button->width());
|
||||
}, button->lifetime());
|
||||
};
|
||||
if (const auto user = _peer->asUser()) {
|
||||
const auto controller = _controller->parentController();
|
||||
if (user->session().supportMode()) {
|
||||
@@ -1101,19 +1118,19 @@ object_ptr<Ui::RpWidget> DetailsFiller::setupInfo() {
|
||||
usernameLine.subtext->overrideLinkClickHandler(callback);
|
||||
usernameLine.text->setContextMenuHook(hook);
|
||||
usernameLine.subtext->setContextMenuHook(hook);
|
||||
const auto usernameLabel = usernameLine.text;
|
||||
if (user->isBot()) {
|
||||
if (user) {
|
||||
const auto copyUsername = Ui::CreateChild<Ui::IconButton>(
|
||||
usernameLabel->parentWidget(),
|
||||
st::infoProfileLabeledButtonCopy);
|
||||
result->sizeValue(
|
||||
) | rpl::start_with_next([=] {
|
||||
const auto s = usernameLabel->parentWidget()->size();
|
||||
copyUsername->moveToRight(
|
||||
0,
|
||||
(s.height() - copyUsername->height()) / 2);
|
||||
}, copyUsername->lifetime());
|
||||
usernameLine.text->parentWidget(),
|
||||
user->isBot()
|
||||
? st::infoProfileLabeledButtonCopy
|
||||
: st::infoProfileLabeledButtonQr);
|
||||
fitLabelToButton(copyUsername, usernameLine.text);
|
||||
copyUsername->setClickedCallback([=] {
|
||||
if (!user->isBot()) {
|
||||
controller->show(
|
||||
Box(Ui::FillPeerQrBox, user, std::nullopt, nullptr));
|
||||
return false;
|
||||
}
|
||||
const auto link = user->session().createInternalLinkFull(
|
||||
user->username());
|
||||
if (!link.isEmpty()) {
|
||||
@@ -1172,7 +1189,7 @@ object_ptr<Ui::RpWidget> DetailsFiller::setupInfo() {
|
||||
: text) + addToLink,
|
||||
(addToLink.isEmpty() ? link.url : (text + addToLink)));
|
||||
});
|
||||
auto linkLine = addInfoOneLine(
|
||||
const auto linkLine = addInfoOneLine(
|
||||
(topicRootId
|
||||
? tr::lng_info_link_label(Ui::Text::WithEntities)
|
||||
: UsernamesSubtext(_peer, tr::lng_info_link_label())),
|
||||
@@ -1185,6 +1202,17 @@ object_ptr<Ui::RpWidget> DetailsFiller::setupInfo() {
|
||||
addToLink);
|
||||
linkLine.text->overrideLinkClickHandler(linkCallback);
|
||||
linkLine.subtext->overrideLinkClickHandler(linkCallback);
|
||||
{
|
||||
const auto qr = Ui::CreateChild<Ui::IconButton>(
|
||||
linkLine.text->parentWidget(),
|
||||
st::infoProfileLabeledButtonQr);
|
||||
fitLabelToButton(qr, linkLine.text);
|
||||
qr->setClickedCallback([=, peer = _peer] {
|
||||
controller->show(
|
||||
Box(Ui::FillPeerQrBox, peer, std::nullopt, nullptr));
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
if (const auto channel = _topic ? nullptr : _peer->asChannel()) {
|
||||
auto locationText = LocationValue(
|
||||
|
||||
@@ -154,11 +154,11 @@ rpl::producer<TextWithEntities> PhoneOrHiddenValue(not_null<UserData*> user) {
|
||||
}
|
||||
|
||||
rpl::producer<TextWithEntities> UsernameValue(
|
||||
not_null<UserData*> user,
|
||||
not_null<PeerData*> peer,
|
||||
bool primary) {
|
||||
return (primary
|
||||
? PlainPrimaryUsernameValue(user)
|
||||
: (PlainUsernameValue(user) | rpl::type_erased())
|
||||
? PlainPrimaryUsernameValue(peer)
|
||||
: (PlainUsernameValue(peer) | rpl::type_erased())
|
||||
) | rpl::map([](QString &&username) {
|
||||
return username.isEmpty()
|
||||
? QString()
|
||||
|
||||
@@ -57,7 +57,7 @@ rpl::producer<not_null<PeerData*>> MigratedOrMeValue(
|
||||
[[nodiscard]] rpl::producer<TextWithEntities> PhoneOrHiddenValue(
|
||||
not_null<UserData*> user);
|
||||
[[nodiscard]] rpl::producer<TextWithEntities> UsernameValue(
|
||||
not_null<UserData*> user,
|
||||
not_null<PeerData*> peer,
|
||||
bool primary = false);
|
||||
[[nodiscard]] rpl::producer<std::vector<TextWithEntities>> UsernamesValue(
|
||||
not_null<PeerData*> peer);
|
||||
|
||||
@@ -8,7 +8,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "platform/mac/file_utilities_mac.h"
|
||||
|
||||
#include "base/platform/mac/base_utilities_mac.h"
|
||||
#include "core/mime_type.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "styles/style_window.h"
|
||||
|
||||
@@ -380,9 +379,6 @@ namespace Platform {
|
||||
namespace File {
|
||||
|
||||
QString UrlToLocal(const QUrl &url) {
|
||||
if (!Core::UrlIsLocal(url)) {
|
||||
return QString();
|
||||
}
|
||||
auto result = url.toLocalFile();
|
||||
if (result.startsWith(u"/.file/id="_q)) {
|
||||
NSString *nsurl = [[[NSURL URLWithString: [NSString stringWithUTF8String: (u"file://"_q + result).toUtf8().constData()]] filePathURL] path];
|
||||
|
||||
@@ -606,6 +606,7 @@ base::unique_qptr<Ui::PopupMenu> LinksController::createRowContextMenu(
|
||||
}, &st::menuIconShare);
|
||||
result->addAction(tr::lng_group_invite_context_qr(tr::now), [=] {
|
||||
delegate()->peerListUiShow()->showBox(InviteLinkQrBox(
|
||||
nullptr,
|
||||
link,
|
||||
tr::lng_chat_link_qr_title(),
|
||||
tr::lng_chat_link_qr_about()));
|
||||
|
||||
@@ -83,9 +83,10 @@ bool ValidatePhotoEditorMediaDragData(not_null<const QMimeData*> data) {
|
||||
}
|
||||
|
||||
if (!urls.isEmpty()) {
|
||||
using namespace Core;
|
||||
const auto file = Platform::File::UrlToLocal(urls.front());
|
||||
if (!file.isEmpty()) {
|
||||
const auto url = urls.front();
|
||||
if (url.isLocalFile()) {
|
||||
using namespace Core;
|
||||
const auto file = Platform::File::UrlToLocal(url);
|
||||
const auto info = QFileInfo(file);
|
||||
return FileIsImage(file, MimeTypeForFile(info).name())
|
||||
&& QImageReader(file).canRead();
|
||||
@@ -106,10 +107,10 @@ bool ValidateEditMediaDragData(
|
||||
}
|
||||
|
||||
if (albumType == Ui::AlbumType::PhotoVideo && !urls.isEmpty()) {
|
||||
using namespace Core;
|
||||
const auto file = Platform::File::UrlToLocal(urls.front());
|
||||
if (!file.isEmpty()) {
|
||||
const auto info = QFileInfo(file);
|
||||
const auto url = urls.front();
|
||||
if (url.isLocalFile()) {
|
||||
using namespace Core;
|
||||
const auto info = QFileInfo(Platform::File::UrlToLocal(url));
|
||||
return IsMimeAcceptedForPhotoVideoAlbum(MimeTypeForFile(info).name());
|
||||
}
|
||||
}
|
||||
@@ -133,10 +134,10 @@ MimeDataState ComputeMimeDataState(const QMimeData *data) {
|
||||
|
||||
auto allAreSmallImages = true;
|
||||
for (const auto &url : urls) {
|
||||
const auto file = Platform::File::UrlToLocal(url);
|
||||
if (file.isEmpty()) {
|
||||
if (!url.isLocalFile()) {
|
||||
return MimeDataState::None;
|
||||
}
|
||||
const auto file = Platform::File::UrlToLocal(url);
|
||||
|
||||
const auto info = QFileInfo(file);
|
||||
if (info.isDir()) {
|
||||
@@ -174,13 +175,13 @@ PreparedList PrepareMediaList(
|
||||
auto locals = QStringList();
|
||||
locals.reserve(files.size());
|
||||
for (const auto &url : files) {
|
||||
locals.push_back(Platform::File::UrlToLocal(url));
|
||||
if (locals.back().isEmpty()) {
|
||||
if (!url.isLocalFile()) {
|
||||
return {
|
||||
PreparedList::Error::NonLocalUrl,
|
||||
url.toDisplayString()
|
||||
};
|
||||
}
|
||||
locals.push_back(Platform::File::UrlToLocal(url));
|
||||
}
|
||||
return PrepareMediaList(locals, previewWidth, premium);
|
||||
}
|
||||
|
||||
875
Telegram/SourceFiles/ui/boxes/peer_qr_box.cpp
Normal file
875
Telegram/SourceFiles/ui/boxes/peer_qr_box.cpp
Normal file
@@ -0,0 +1,875 @@
|
||||
/*
|
||||
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/peer_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/channel_statistics/boosts/giveaway/boost_badge.h" // InfiniteRadialAnimationWidget.
|
||||
#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/widgets/continuous_sliders.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_credits.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]] QMargins NoPhotoBackgroundMargins() {
|
||||
return QMargins(
|
||||
st::profileQrBackgroundMargins.left(),
|
||||
st::profileQrBackgroundMargins.left(),
|
||||
st::profileQrBackgroundMargins.right(),
|
||||
st::profileQrBackgroundMargins.bottom());
|
||||
}
|
||||
|
||||
[[nodiscard]] QImage TelegramQr(const Qr::Data &data, int pixel, int max) {
|
||||
Expects(data.size > 0);
|
||||
|
||||
constexpr auto kCenterRatio = 0.175;
|
||||
|
||||
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 centerRect = Rect(size)
|
||||
- Margins((size.width() - (size.width() * kCenterRatio)) / 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]] QMargins RoundedMargins(
|
||||
const QMargins &backgroundMargins,
|
||||
int photoSize,
|
||||
int textMaxHeight) {
|
||||
return (textMaxHeight
|
||||
? (backgroundMargins + QMargins(0, photoSize / 2, 0, textMaxHeight))
|
||||
: photoSize
|
||||
? backgroundMargins + QMargins(0, photoSize / 2, 0, photoSize / 2)
|
||||
: Margins(backgroundMargins.left()));
|
||||
}
|
||||
|
||||
void Paint(
|
||||
QPainter &p,
|
||||
const style::font &font,
|
||||
const QString &text,
|
||||
const Colors &backgroundColors,
|
||||
const QMargins &backgroundMargins,
|
||||
const QImage &qrImage,
|
||||
const QRect &qrRect,
|
||||
int qrMaxSize,
|
||||
int qrPixel,
|
||||
int radius,
|
||||
int textMaxHeight,
|
||||
int photoSize) {
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(Qt::white);
|
||||
const auto roundedRect = qrRect
|
||||
+ RoundedMargins(backgroundMargins, photoSize, textMaxHeight);
|
||||
p.drawRoundedRect(roundedRect, radius, radius);
|
||||
if (!qrImage.isNull() && !backgroundColors.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;
|
||||
|
||||
auto back = Images::GenerateGradient(
|
||||
qrRect.size(),
|
||||
backgroundColors,
|
||||
gradientRotation,
|
||||
1. - (gradientRotationAdd / 45.));
|
||||
p.drawImage(qrRect, 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);
|
||||
if (textMaxHeight) {
|
||||
// '@' + QString(32, 'W');
|
||||
auto p = QPainter(&colored);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setPen(Qt::black);
|
||||
p.setFont(font);
|
||||
auto option = QTextOption(style::al_center);
|
||||
option.setWrapMode(QTextOption::WrapAnywhere);
|
||||
p.drawText(Rect(coloredSize), text, option);
|
||||
p.setCompositionMode(QPainter::CompositionMode_SourceIn);
|
||||
p.drawImage(0, -back.height() + textMaxHeight, back);
|
||||
}
|
||||
p.drawImage(qrRect, qrImage);
|
||||
if (textMaxHeight) {
|
||||
p.drawImage(
|
||||
qrRect.x(),
|
||||
rect::bottom(qrRect)
|
||||
+ ((rect::bottom(roundedRect) - rect::bottom(qrRect))
|
||||
- textMaxHeight) / 2,
|
||||
colored);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
not_null<Ui::RpWidget*> PrepareQrWidget(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
not_null<Ui::RpWidget*> topWidget,
|
||||
const style::font &font,
|
||||
rpl::producer<bool> userpicToggled,
|
||||
rpl::producer<QString> username,
|
||||
rpl::producer<QString> links,
|
||||
rpl::producer<Colors> bgs,
|
||||
rpl::producer<QString> about) {
|
||||
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 qrImage;
|
||||
Colors backgroundColors;
|
||||
QString text;
|
||||
QMargins backgroundMargins;
|
||||
int textWidth = 0;
|
||||
int textMaxHeight = 0;
|
||||
int photoSize = 0;
|
||||
};
|
||||
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(); });
|
||||
const auto qrMaxSize = st::boxWideWidth
|
||||
- rect::m::sum::h(st::boxRowPadding)
|
||||
- rect::m::sum::h(st::profileQrBackgroundMargins);
|
||||
const auto aboutLabel = Ui::CreateChild<Ui::FlatLabel>(
|
||||
divider,
|
||||
st::creditsBoxAboutDivider);
|
||||
rpl::combine(
|
||||
std::move(userpicToggled),
|
||||
std::move(username),
|
||||
std::move(bgs),
|
||||
std::move(links),
|
||||
std::move(about),
|
||||
rpl::single(rpl::empty) | rpl::then(style::PaletteChanged())
|
||||
) | rpl::start_with_next([=](
|
||||
bool userpicToggled,
|
||||
const QString &username,
|
||||
const Colors &backgroundColors,
|
||||
const QString &link,
|
||||
const QString &about,
|
||||
const auto &) {
|
||||
state->backgroundMargins = userpicToggled
|
||||
? st::profileQrBackgroundMargins
|
||||
: NoPhotoBackgroundMargins();
|
||||
state->photoSize = userpicToggled
|
||||
? st::defaultUserpicButton.photoSize
|
||||
: 0;
|
||||
state->backgroundColors = backgroundColors;
|
||||
state->text = username.toUpper();
|
||||
state->textWidth = font->width(state->text);
|
||||
{
|
||||
const auto remainder = qrMaxSize % st::introQrPixel;
|
||||
const auto downTo = remainder
|
||||
? qrMaxSize - remainder
|
||||
: qrMaxSize;
|
||||
state->qrImage = TelegramQr(
|
||||
Qr::Encode(link.toUtf8(), Qr::Redundancy::Default),
|
||||
st::introQrPixel,
|
||||
downTo).scaled(
|
||||
Size(qrMaxSize * style::DevicePixelRatio()),
|
||||
Qt::IgnoreAspectRatio,
|
||||
Qt::SmoothTransformation);
|
||||
}
|
||||
const auto resultWidth = qrMaxSize
|
||||
+ rect::m::sum::h(state->backgroundMargins);
|
||||
{
|
||||
aboutLabel->setText(about);
|
||||
aboutLabel->resizeToWidth(resultWidth);
|
||||
}
|
||||
const auto qrWidth = state->qrImage.width()
|
||||
/ style::DevicePixelRatio();
|
||||
const auto lines = int(state->textWidth / qrWidth) + 1;
|
||||
state->textMaxHeight = state->textWidth ? (font->height * lines) : 0;
|
||||
const auto whiteMargins = RoundedMargins(
|
||||
state->backgroundMargins,
|
||||
state->photoSize,
|
||||
state->textMaxHeight);
|
||||
result->resize(
|
||||
qrMaxSize + rect::m::sum::h(whiteMargins),
|
||||
qrMaxSize
|
||||
+ rect::m::sum::v(whiteMargins) // White.
|
||||
+ rect::m::sum::v(st::profileQrBackgroundPadding) // Gray.
|
||||
+ state->photoSize / 2
|
||||
+ aboutLabel->height());
|
||||
|
||||
divider->resize(container->width(), result->height());
|
||||
result->moveToLeft((container->width() - result->width()) / 2, 0);
|
||||
topWidget->setVisible(userpicToggled);
|
||||
topWidget->moveToLeft(0, -std::numeric_limits<int>::min());
|
||||
topWidget->raise();
|
||||
|
||||
aboutLabel->raise();
|
||||
aboutLabel->moveToLeft(
|
||||
result->x(),
|
||||
divider->height()
|
||||
- aboutLabel->height()
|
||||
- st::defaultBoxDividerLabelPadding.top());
|
||||
}, container->lifetime());
|
||||
result->paintRequest(
|
||||
) | rpl::start_with_next([=](QRect clip) {
|
||||
auto p = QPainter(result);
|
||||
const auto size = (state->qrImage.size() / style::DevicePixelRatio());
|
||||
const auto qrRect = Rect(
|
||||
(result->width() - size.width()) / 2,
|
||||
state->backgroundMargins.top() + state->photoSize / 2,
|
||||
size);
|
||||
p.translate(
|
||||
0,
|
||||
st::profileQrBackgroundPadding.top() + state->photoSize / 2);
|
||||
Paint(
|
||||
p,
|
||||
font,
|
||||
state->text,
|
||||
state->backgroundColors,
|
||||
state->backgroundMargins,
|
||||
state->qrImage,
|
||||
qrRect,
|
||||
qrMaxSize,
|
||||
st::introQrPixel,
|
||||
st::profileQrBackgroundRadius,
|
||||
state->textMaxHeight,
|
||||
state->photoSize);
|
||||
if (!state->photoSize) {
|
||||
return;
|
||||
}
|
||||
const auto photoSize = state->photoSize;
|
||||
const auto top = Ui::GrabWidget(
|
||||
topWidget,
|
||||
QRect(),
|
||||
Qt::transparent).scaled(
|
||||
Size(photoSize * style::DevicePixelRatio()),
|
||||
Qt::IgnoreAspectRatio,
|
||||
Qt::SmoothTransformation);
|
||||
p.drawPixmap((result->width() - photoSize) / 2, -photoSize / 2, top);
|
||||
}, result->lifetime());
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] Fn<void(int)> AddDotsToSlider(
|
||||
not_null<Ui::ContinuousSlider*> slider,
|
||||
const style::MediaSlider &st,
|
||||
int count) {
|
||||
const auto lineWidth = st::lineWidth;
|
||||
const auto smallSize = Size(st.seekSize.height() - st.width);
|
||||
auto smallDots = std::vector<not_null<Ui::RpWidget*>>();
|
||||
smallDots.reserve(count - 1);
|
||||
const auto paintSmall = [=](QPainter &p, const QBrush &brush) {
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
auto pen = st::boxBg->p;
|
||||
pen.setWidth(st.width);
|
||||
p.setPen(pen);
|
||||
p.setBrush(brush);
|
||||
p.drawEllipse(Rect(smallSize) - Margins(lineWidth));
|
||||
};
|
||||
for (auto i = 0; i < count - 1; i++) {
|
||||
smallDots.push_back(
|
||||
Ui::CreateChild<Ui::RpWidget>(slider->parentWidget()));
|
||||
const auto dot = smallDots.back();
|
||||
dot->resize(smallSize);
|
||||
dot->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
dot->paintRequest() | rpl::start_with_next([=] {
|
||||
auto p = QPainter(dot);
|
||||
const auto fg = (slider->value() > (i / float64(count - 1)))
|
||||
? st.activeFg
|
||||
: st.inactiveFg;
|
||||
paintSmall(p, fg);
|
||||
}, dot->lifetime());
|
||||
}
|
||||
const auto bigDot = Ui::CreateChild<Ui::RpWidget>(slider->parentWidget());
|
||||
bigDot->resize(st.seekSize);
|
||||
bigDot->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
bigDot->paintRequest() | rpl::start_with_next([=] {
|
||||
auto p = QPainter(bigDot);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
auto pen = st::boxBg->p;
|
||||
pen.setWidth(st.width);
|
||||
p.setPen(pen);
|
||||
p.setBrush(st.activeFg);
|
||||
p.drawEllipse(Rect(st.seekSize) - Margins(lineWidth));
|
||||
}, bigDot->lifetime());
|
||||
|
||||
return [=](int index) {
|
||||
const auto g = slider->geometry();
|
||||
const auto bigTop = g.y() + (g.height() - bigDot->height()) / 2;
|
||||
const auto smallTop = g.y()
|
||||
+ (g.height() - smallSize.height()) / 2;
|
||||
for (auto i = 0; i < count; ++i) {
|
||||
if (index == i) {
|
||||
const auto x = ((g.width() - bigDot->width()) * i)
|
||||
/ float64(count - 1);
|
||||
bigDot->move(g.x() + std::ceil(x), bigTop);
|
||||
} else {
|
||||
const auto k = (i < index) ? i : i - 1;
|
||||
const auto w = smallDots[k]->width();
|
||||
smallDots[k]->move(
|
||||
g.x() + ((g.width() - w) * i) / (count - 1),
|
||||
smallTop);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void FillPeerQrBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
PeerData *peer,
|
||||
std::optional<QString> customLink,
|
||||
rpl::producer<QString> about) {
|
||||
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 {
|
||||
Ui::RpWidget* saveButton = nullptr;
|
||||
rpl::variable<bool> saveButtonBusy = false;
|
||||
rpl::variable<bool> userpicToggled = true;
|
||||
rpl::variable<Colors> bgs;
|
||||
Ui::Animations::Simple animation;
|
||||
rpl::variable<int> chosen = 0;
|
||||
rpl::variable<int> scaleValue = 0;
|
||||
|
||||
style::font font;
|
||||
};
|
||||
const auto state = box->lifetime().make_state<State>();
|
||||
state->userpicToggled = !(customLink || !peer);
|
||||
const auto createFont = [=](int scale) {
|
||||
return style::font(
|
||||
style::ConvertScale(30, scale),
|
||||
st::profileQrFont->flags(),
|
||||
st::profileQrFont->family());
|
||||
};
|
||||
state->font = createFont(style::Scale());
|
||||
|
||||
const auto usernameValue = [=] {
|
||||
return (customLink || !peer)
|
||||
? (rpl::single(QString()) | rpl::type_erased())
|
||||
: Info::Profile::UsernameValue(peer, true) | rpl::map(
|
||||
[](const auto &username) { return username.text; });
|
||||
};
|
||||
const auto linkValue = [=] {
|
||||
return customLink
|
||||
? rpl::single(*customLink)
|
||||
: peer
|
||||
? Info::Profile::LinkValue(peer, true) | rpl::map(
|
||||
[](const auto &link) { return link.text; })
|
||||
: (rpl::single(QString()) | rpl::type_erased());
|
||||
};
|
||||
|
||||
const auto userpic = Ui::CreateChild<Ui::UserpicButton>(
|
||||
box,
|
||||
peer ? peer : controller->session().user().get(),
|
||||
st::defaultUserpicButton);
|
||||
userpic->setVisible(peer != nullptr);
|
||||
PrepareQrWidget(
|
||||
box->verticalLayout(),
|
||||
userpic,
|
||||
state->font,
|
||||
state->userpicToggled.value(),
|
||||
usernameValue(),
|
||||
linkValue(),
|
||||
state->bgs.value(),
|
||||
about ? std::move(about) : rpl::single(QString()));
|
||||
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
Ui::AddSubsectionTitle(
|
||||
box->verticalLayout(),
|
||||
tr::lng_userpic_builder_color_subtitle());
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
struct State {
|
||||
Colors colors;
|
||||
QImage image;
|
||||
};
|
||||
constexpr auto kMaxInRow = 4;
|
||||
constexpr auto kMaxColors = 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);
|
||||
|
||||
auto colorsCollection = ranges::views::all(
|
||||
cloudThemes
|
||||
) | ranges::views::transform([](const auto &cloudTheme) -> Colors {
|
||||
const auto it = cloudTheme.settings.find(
|
||||
Data::CloudThemeType::Light);
|
||||
if (it == end(cloudTheme.settings)) {
|
||||
return Colors();
|
||||
}
|
||||
const auto colors = it->second.paper
|
||||
? it->second.paper->backgroundColors()
|
||||
: Colors();
|
||||
if (colors.size() != kMaxColors) {
|
||||
return Colors();
|
||||
}
|
||||
return colors;
|
||||
}) | ranges::views::filter([](const Colors &colors) {
|
||||
return !colors.empty();
|
||||
}) | ranges::to_vector;
|
||||
colorsCollection.insert(
|
||||
colorsCollection.begin(),
|
||||
Colors{
|
||||
st::premiumButtonBg1->c,
|
||||
st::premiumButtonBg1->c,
|
||||
st::premiumButtonBg2->c,
|
||||
st::premiumButtonBg3->c,
|
||||
});
|
||||
// colorsCollection.push_back(Colors{
|
||||
// st::creditsBg1->c,
|
||||
// st::creditsBg2->c,
|
||||
// st::creditsBg1->c,
|
||||
// st::creditsBg2->c,
|
||||
// });
|
||||
|
||||
for (const auto &colors : colorsCollection) {
|
||||
if (state->bgs.current().empty()) {
|
||||
state->bgs = colors;
|
||||
}
|
||||
|
||||
if (counter % kMaxInRow == 0) {
|
||||
Ui::AddSkip(themesContainer);
|
||||
row = themesContainer->add(
|
||||
object_ptr<Ui::RpWidget>(themesContainer));
|
||||
row->resize(size, size);
|
||||
}
|
||||
const auto widget = Ui::CreateChild<Ui::AbstractButton>(row);
|
||||
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() == kMaxColors) {
|
||||
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());
|
||||
counter++;
|
||||
}
|
||||
Ui::AddSkip(themesContainer);
|
||||
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());
|
||||
}
|
||||
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
Ui::AddDivider(box->verticalLayout());
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
Ui::AddSubsectionTitle(
|
||||
box->verticalLayout(),
|
||||
tr::lng_qr_box_quality());
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
constexpr auto kMaxQualities = 3;
|
||||
{
|
||||
const auto seekSize = st::settingsScale.seekSize.height();
|
||||
const auto &labelSt = st::defaultFlatLabel;
|
||||
const auto labels = box->verticalLayout()->add(
|
||||
Ui::CreateSkipWidget(
|
||||
box,
|
||||
labelSt.style.font->height + labelSt.style.font->descent),
|
||||
st::boxRowPadding);
|
||||
const auto left = Ui::CreateChild<Ui::FlatLabel>(
|
||||
labels,
|
||||
tr::lng_qr_box_quality1(),
|
||||
labelSt);
|
||||
const auto middle = Ui::CreateChild<Ui::FlatLabel>(
|
||||
labels,
|
||||
tr::lng_qr_box_quality2(),
|
||||
labelSt);
|
||||
const auto right = Ui::CreateChild<Ui::FlatLabel>(
|
||||
labels,
|
||||
tr::lng_qr_box_quality3(),
|
||||
labelSt);
|
||||
labels->sizeValue(
|
||||
) | rpl::start_with_next([=](const QSize &size) {
|
||||
left->moveToLeft(0, 0);
|
||||
middle->moveToLeft((size.width() - middle->width()) / 2, 0);
|
||||
right->moveToRight(0, 0);
|
||||
}, labels->lifetime());
|
||||
|
||||
const auto slider = box->verticalLayout()->add(
|
||||
object_ptr<Ui::MediaSliderWheelless>(
|
||||
box->verticalLayout(),
|
||||
st::settingsScale),
|
||||
st::boxRowPadding);
|
||||
slider->resize(slider->width(), seekSize);
|
||||
const auto active = st::windowActiveTextFg->c;
|
||||
const auto inactive = st::windowSubTextFg->c;
|
||||
const auto colorize = [=](int index) {
|
||||
if (index == 0) {
|
||||
left->setTextColorOverride(active);
|
||||
middle->setTextColorOverride(inactive);
|
||||
right->setTextColorOverride(inactive);
|
||||
} else if (index == 1) {
|
||||
left->setTextColorOverride(inactive);
|
||||
middle->setTextColorOverride(active);
|
||||
right->setTextColorOverride(inactive);
|
||||
} else if (index == 2) {
|
||||
left->setTextColorOverride(inactive);
|
||||
middle->setTextColorOverride(inactive);
|
||||
right->setTextColorOverride(active);
|
||||
}
|
||||
};
|
||||
const auto updateGeometry = AddDotsToSlider(
|
||||
slider,
|
||||
st::settingsScale,
|
||||
kMaxQualities);
|
||||
slider->geometryValue(
|
||||
) | rpl::start_with_next([=](const QRect &rect) {
|
||||
updateGeometry(int(slider->value() * (kMaxQualities - 1)));
|
||||
}, box->lifetime());
|
||||
|
||||
box->setShowFinishedCallback([=] {
|
||||
colorize(0);
|
||||
updateGeometry(0);
|
||||
});
|
||||
slider->setPseudoDiscrete(
|
||||
kMaxQualities,
|
||||
[=](int index) { return index; },
|
||||
0,
|
||||
[=](int scale) {
|
||||
state->scaleValue = scale;
|
||||
colorize(scale);
|
||||
updateGeometry(scale);
|
||||
},
|
||||
[](int) {});
|
||||
}
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
if (peer) {
|
||||
const auto userpicToggle = box->verticalLayout()->add(
|
||||
object_ptr<Ui::SettingsButton>(
|
||||
box->verticalLayout(),
|
||||
(peer->isUser()
|
||||
? tr::lng_mediaview_profile_photo
|
||||
: (peer->isChannel() && !peer->isMegagroup())
|
||||
? tr::lng_mediaview_channel_photo
|
||||
: tr::lng_mediaview_group_photo)(),
|
||||
st::settingsButtonNoIcon));
|
||||
userpicToggle->toggleOn(state->userpicToggled.value(), true);
|
||||
userpicToggle->setClickedCallback([=] {
|
||||
state->userpicToggled = !state->userpicToggled.current();
|
||||
});
|
||||
}
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
|
||||
auto buttonText = rpl::conditional(
|
||||
state->saveButtonBusy.value() | rpl::map(rpl::mappers::_1),
|
||||
rpl::single(QString()),
|
||||
tr::lng_chat_link_copy());
|
||||
const auto show = controller->uiShow();
|
||||
state->saveButton = box->addButton(std::move(buttonText), [=] {
|
||||
if (state->saveButtonBusy.current()) {
|
||||
return;
|
||||
}
|
||||
const auto buttonWidth = state->saveButton
|
||||
? state->saveButton->width()
|
||||
: 0;
|
||||
state->saveButtonBusy = true;
|
||||
if (state->saveButton) {
|
||||
state->saveButton->resizeToWidth(buttonWidth);
|
||||
}
|
||||
|
||||
const auto userpicToggled = state->userpicToggled.current();
|
||||
const auto scale = style::kScaleDefault
|
||||
* (kMaxQualities + int(state->scaleValue.current() * 2));
|
||||
const auto divider = std::max(1, style::Scale())
|
||||
/ style::kScaleDefault;
|
||||
const auto profileQrBackgroundRadius = style::ConvertScale(
|
||||
st::profileQrBackgroundRadius / divider,
|
||||
scale);
|
||||
const auto introQrPixel = style::ConvertScale(
|
||||
st::introQrPixel / divider,
|
||||
scale);
|
||||
const auto lineWidth = style::ConvertScale(
|
||||
st::lineWidth / divider,
|
||||
scale);
|
||||
const auto boxWideWidth = style::ConvertScale(
|
||||
st::boxWideWidth / divider,
|
||||
scale);
|
||||
const auto createMargins = [&](const style::margins &margins) {
|
||||
return QMargins(
|
||||
style::ConvertScale(margins.left() / divider, scale),
|
||||
style::ConvertScale(margins.top() / divider, scale),
|
||||
style::ConvertScale(margins.right() / divider, scale),
|
||||
style::ConvertScale(margins.bottom() / divider, scale));
|
||||
};
|
||||
const auto boxRowPadding = createMargins(st::boxRowPadding);
|
||||
const auto backgroundMargins = userpicToggled
|
||||
? createMargins(st::profileQrBackgroundMargins)
|
||||
: createMargins(NoPhotoBackgroundMargins());
|
||||
const auto qrMaxSize = boxWideWidth
|
||||
- rect::m::sum::h(boxRowPadding)
|
||||
- rect::m::sum::h(backgroundMargins);
|
||||
const auto photoSize = userpicToggled
|
||||
? style::ConvertScale(
|
||||
st::defaultUserpicButton.photoSize / divider,
|
||||
scale)
|
||||
: 0;
|
||||
|
||||
const auto font = createFont(scale);
|
||||
const auto username = rpl::variable<QString>(
|
||||
usernameValue()).current().toUpper();
|
||||
const auto link = rpl::variable<QString>(linkValue());
|
||||
const auto textWidth = font->width(username);
|
||||
const auto top = Ui::GrabWidget(
|
||||
userpic,
|
||||
{},
|
||||
Qt::transparent);
|
||||
const auto weak = Ui::MakeWeak(box);
|
||||
|
||||
crl::async([=] {
|
||||
const auto qrImage = TelegramQr(
|
||||
Qr::Encode(
|
||||
link.current().toUtf8(),
|
||||
Qr::Redundancy::Default),
|
||||
introQrPixel,
|
||||
qrMaxSize);
|
||||
const auto qrWidth = qrImage.width() / style::DevicePixelRatio();
|
||||
const auto lines = int(textWidth / qrWidth) + 1;
|
||||
const auto textMaxHeight = textWidth ? font->height * lines : 0;
|
||||
|
||||
const auto whiteMargins = RoundedMargins(
|
||||
backgroundMargins,
|
||||
photoSize,
|
||||
textMaxHeight);
|
||||
const auto resultSize = QSize(
|
||||
qrMaxSize + rect::m::sum::h(whiteMargins),
|
||||
qrMaxSize + rect::m::sum::v(whiteMargins) + photoSize / 2);
|
||||
|
||||
const auto qrImageSize = qrImage.size()
|
||||
/ style::DevicePixelRatio();
|
||||
const auto qrRect = Rect(
|
||||
(resultSize.width() - qrImageSize.width()) / 2,
|
||||
whiteMargins.top() + photoSize / 2,
|
||||
qrImageSize);
|
||||
|
||||
auto image = QImage(
|
||||
resultSize * style::DevicePixelRatio(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
image.fill(Qt::transparent);
|
||||
image.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
{
|
||||
auto p = QPainter(&image);
|
||||
p.translate(0, lineWidth); // Bad.
|
||||
Paint(
|
||||
p,
|
||||
font,
|
||||
username,
|
||||
state->bgs.current(),
|
||||
backgroundMargins,
|
||||
qrImage,
|
||||
qrRect,
|
||||
qrMaxSize,
|
||||
introQrPixel,
|
||||
profileQrBackgroundRadius,
|
||||
textMaxHeight,
|
||||
photoSize);
|
||||
|
||||
if (userpicToggled) {
|
||||
p.drawPixmap(
|
||||
(resultSize.width() - photoSize) / 2,
|
||||
0,
|
||||
top.scaled(
|
||||
Size(photoSize * style::DevicePixelRatio()),
|
||||
Qt::IgnoreAspectRatio,
|
||||
Qt::SmoothTransformation));
|
||||
}
|
||||
}
|
||||
crl::on_main(weak, [=] {
|
||||
state->saveButtonBusy = false;
|
||||
auto mime = std::make_unique<QMimeData>();
|
||||
mime->setImageData(std::move(image));
|
||||
QGuiApplication::clipboard()->setMimeData(mime.release());
|
||||
show->showToast(tr::lng_group_invite_qr_copied(tr::now));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (const auto saveButton = state->saveButton) {
|
||||
using namespace Info::Statistics;
|
||||
const auto loadingAnimation = InfiniteRadialAnimationWidget(
|
||||
saveButton,
|
||||
saveButton->height() / 2);
|
||||
AddChildToWidgetCenter(saveButton, loadingAnimation);
|
||||
loadingAnimation->showOn(state->saveButtonBusy.value());
|
||||
}
|
||||
|
||||
const auto buttonWidth = box->width()
|
||||
- rect::m::sum::h(st::giveawayGiftCodeBox.buttonPadding);
|
||||
state->saveButton->widthValue() | rpl::filter([=] {
|
||||
return (state->saveButton->widthNoMargins() != buttonWidth);
|
||||
}) | rpl::start_with_next([=] {
|
||||
state->saveButton->resizeToWidth(buttonWidth);
|
||||
}, state->saveButton->lifetime());
|
||||
box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); });
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
22
Telegram/SourceFiles/ui/boxes/peer_qr_box.h
Normal file
22
Telegram/SourceFiles/ui/boxes/peer_qr_box.h
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
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 FillPeerQrBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
PeerData *peer,
|
||||
std::optional<QString> customLink,
|
||||
rpl::producer<QString> about);
|
||||
|
||||
} // namespace Ui
|
||||
@@ -457,7 +457,7 @@ if customRunCommand:
|
||||
stage('patches', """
|
||||
git clone https://github.com/desktop-app/patches.git
|
||||
cd patches
|
||||
git checkout 8cd00d57c7
|
||||
git checkout e66e768a5f
|
||||
""")
|
||||
|
||||
stage('msys64', """
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
AppVersion 5005002
|
||||
AppVersion 5005003
|
||||
AppVersionStrMajor 5.5
|
||||
AppVersionStrSmall 5.5.2
|
||||
AppVersionStr 5.5.2
|
||||
AppVersionStrSmall 5.5.3
|
||||
AppVersionStr 5.5.3
|
||||
BetaChannel 0
|
||||
AlphaVersion 0
|
||||
AppVersionOriginal 5.5.2
|
||||
AppVersionOriginal 5.5.3
|
||||
|
||||
Submodule Telegram/lib_qr updated: 501f4c3502...6fdf604614
Submodule Telegram/lib_ui updated: eaf4437e49...c4e3a08e6f
@@ -1,3 +1,9 @@
|
||||
5.5.3 (10.09.24)
|
||||
|
||||
- Fix custom emoji sending.
|
||||
- Add mention link QR code sharing.
|
||||
- Fix sending files from network drives. (Windows)
|
||||
|
||||
5.5.2 (09.09.24)
|
||||
|
||||
- Support two bottom buttons in web apps.
|
||||
|
||||
Reference in New Issue
Block a user