From b7eb9eea4ef018e07921e36bf729dec72afc284f Mon Sep 17 00:00:00 2001
From: 23rd <23rd@vivaldi.net>
Date: Sun, 19 Jul 2026 20:09:01 +0300
Subject: [PATCH] Added favorite link button to folders strip.
---
Telegram/CMakeLists.txt | 2 +
.../icons/folders/svg/section_chat.svg | 1 +
.../icons/folders/svg/section_folders.svg | 1 +
.../icons/folders/svg/section_info.svg | 1 +
.../icons/folders/svg/section_language.svg | 1 +
.../folders/svg/section_notifications.svg | 1 +
.../icons/folders/svg/section_privacy.svg | 1 +
.../icons/folders/svg/section_settings.svg | 1 +
.../icons/folders/svg/section_stickers.svg | 1 +
.../settings/settings_experimental.cpp | 64 +++
Telegram/SourceFiles/ui/filter_icons.style | 9 +
.../window/window_filters_favorite.cpp | 498 ++++++++++++++++++
.../window/window_filters_favorite.h | 86 +++
.../window/window_filters_menu.cpp | 74 ++-
.../SourceFiles/window/window_filters_menu.h | 7 +
15 files changed, 747 insertions(+), 1 deletion(-)
create mode 100644 Telegram/Resources/icons/folders/svg/section_chat.svg
create mode 100644 Telegram/Resources/icons/folders/svg/section_folders.svg
create mode 100644 Telegram/Resources/icons/folders/svg/section_info.svg
create mode 100644 Telegram/Resources/icons/folders/svg/section_language.svg
create mode 100644 Telegram/Resources/icons/folders/svg/section_notifications.svg
create mode 100644 Telegram/Resources/icons/folders/svg/section_privacy.svg
create mode 100644 Telegram/Resources/icons/folders/svg/section_settings.svg
create mode 100644 Telegram/Resources/icons/folders/svg/section_stickers.svg
create mode 100644 Telegram/SourceFiles/window/window_filters_favorite.cpp
create mode 100644 Telegram/SourceFiles/window/window_filters_favorite.h
diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt
index 5476f7ed33..b695d6b5d4 100644
--- a/Telegram/CMakeLists.txt
+++ b/Telegram/CMakeLists.txt
@@ -1985,6 +1985,8 @@ PRIVATE
window/window_connecting_widget.h
window/window_controller.cpp
window/window_controller.h
+ window/window_filters_favorite.cpp
+ window/window_filters_favorite.h
window/window_filters_menu.cpp
window/window_filters_menu.h
window/window_history_hider.cpp
diff --git a/Telegram/Resources/icons/folders/svg/section_chat.svg b/Telegram/Resources/icons/folders/svg/section_chat.svg
new file mode 100644
index 0000000000..694f98342d
--- /dev/null
+++ b/Telegram/Resources/icons/folders/svg/section_chat.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Telegram/Resources/icons/folders/svg/section_folders.svg b/Telegram/Resources/icons/folders/svg/section_folders.svg
new file mode 100644
index 0000000000..2c04aebc3b
--- /dev/null
+++ b/Telegram/Resources/icons/folders/svg/section_folders.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Telegram/Resources/icons/folders/svg/section_info.svg b/Telegram/Resources/icons/folders/svg/section_info.svg
new file mode 100644
index 0000000000..773e0294b1
--- /dev/null
+++ b/Telegram/Resources/icons/folders/svg/section_info.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Telegram/Resources/icons/folders/svg/section_language.svg b/Telegram/Resources/icons/folders/svg/section_language.svg
new file mode 100644
index 0000000000..ab30e58250
--- /dev/null
+++ b/Telegram/Resources/icons/folders/svg/section_language.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Telegram/Resources/icons/folders/svg/section_notifications.svg b/Telegram/Resources/icons/folders/svg/section_notifications.svg
new file mode 100644
index 0000000000..77425b82e0
--- /dev/null
+++ b/Telegram/Resources/icons/folders/svg/section_notifications.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Telegram/Resources/icons/folders/svg/section_privacy.svg b/Telegram/Resources/icons/folders/svg/section_privacy.svg
new file mode 100644
index 0000000000..368701b8b8
--- /dev/null
+++ b/Telegram/Resources/icons/folders/svg/section_privacy.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Telegram/Resources/icons/folders/svg/section_settings.svg b/Telegram/Resources/icons/folders/svg/section_settings.svg
new file mode 100644
index 0000000000..f9663ad912
--- /dev/null
+++ b/Telegram/Resources/icons/folders/svg/section_settings.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Telegram/Resources/icons/folders/svg/section_stickers.svg b/Telegram/Resources/icons/folders/svg/section_stickers.svg
new file mode 100644
index 0000000000..72d63b1750
--- /dev/null
+++ b/Telegram/Resources/icons/folders/svg/section_stickers.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Telegram/SourceFiles/settings/settings_experimental.cpp b/Telegram/SourceFiles/settings/settings_experimental.cpp
index 809793c7fa..c668c1f667 100644
--- a/Telegram/SourceFiles/settings/settings_experimental.cpp
+++ b/Telegram/SourceFiles/settings/settings_experimental.cpp
@@ -7,7 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "settings/settings_experimental.h"
+#include "settings/settings_common.h"
#include "data/components/passkeys.h"
+#include "ui/layers/generic_box.h"
#include "main/main_session.h"
#include "ui/boxes/confirm_box.h"
#include "ui/search_field_controller.h"
@@ -48,6 +50,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "mtproto/session_private.h"
#include "webview/webview_embed.h"
#include "window/main_window.h"
+#include "window/window_filters_favorite.h"
#include "window/window_peer_menu.h"
#include "window/window_session_controller.h"
#include "window/window_controller.h"
@@ -215,6 +218,61 @@ void AddOption(
}, wrap->lifetime());
}
+void AddFavoriteLinkButton(
+ not_null window,
+ not_null container,
+ rpl::producer query,
+ Fn)> registerHighlight) {
+ const auto option = &base::options::lookup(
+ Window::kOptionFolderFavoriteLink);
+ const auto name = option->name().isEmpty()
+ ? option->id()
+ : option->name();
+ const auto &description = option->description();
+
+ const auto wrap = container->add(
+ object_ptr>(
+ container,
+ object_ptr(container)));
+ const auto inner = wrap->entity();
+
+ auto label = rpl::single(
+ rpl::empty
+ ) | rpl::then(
+ option->changes()
+ ) | rpl::map([option] {
+ return option->value();
+ });
+ const auto button = AddButtonWithLabel(
+ inner,
+ rpl::single(name),
+ std::move(label),
+ st::settingsButtonNoIcon);
+ button->setClickedCallback([=] {
+ window->show(Box(Window::EditFolderFavoriteLinkBox));
+ });
+
+ if (registerHighlight) {
+ registerHighlight(u"experimental/"_q + option->id(), button);
+ }
+
+ if (!description.isEmpty()) {
+ Ui::AddSkip(inner, st::settingsCheckboxesSkip);
+ Ui::AddDividerText(inner, rpl::single(description));
+ Ui::AddSkip(inner, st::settingsCheckboxesSkip);
+ }
+
+ std::move(
+ query
+ ) | rpl::on_next([=](const QString &text) {
+ const auto trimmed = text.trimmed();
+ const auto matches = trimmed.isEmpty()
+ || name.contains(trimmed, Qt::CaseInsensitive)
+ || description.contains(trimmed, Qt::CaseInsensitive);
+ wrap->toggle(matches, anim::type::instant);
+ }, wrap->lifetime());
+}
+
void SetupExperimental(
not_null window,
not_null container,
@@ -317,6 +375,12 @@ void SetupExperimental(
addToggle(HistoryView::Controls::kOptionMacCmdReplyImmediately);
addToggle(Ui::kOptionQScroller);
addToggle(FFmpeg::kOptionFFmpegMultiThread);
+
+ AddFavoriteLinkButton(
+ window,
+ container,
+ rpl::duplicate(query),
+ registerHighlight);
}
} // namespace
diff --git a/Telegram/SourceFiles/ui/filter_icons.style b/Telegram/SourceFiles/ui/filter_icons.style
index ca45d10fd4..a613deeebe 100644
--- a/Telegram/SourceFiles/ui/filter_icons.style
+++ b/Telegram/SourceFiles/ui/filter_icons.style
@@ -85,3 +85,12 @@ foldersPoo: icon {{ "folders/folders_poo", sideBarIconFg }};
foldersPooActive: icon {{ "folders/folders_poo", sideBarIconFgActive }};
filtersEdit: icon {{ "folders/folders_edit", sideBarIconFg }};
+
+foldersSectionSettings: icon {{ "folders/svg/section_settings-36x36", sideBarIconFg }};
+foldersSectionFolders: icon {{ "folders/svg/section_folders-36x36", sideBarIconFg }};
+foldersSectionNotifications: icon {{ "folders/svg/section_notifications-36x36", sideBarIconFg }};
+foldersSectionPrivacy: icon {{ "folders/svg/section_privacy-36x36", sideBarIconFg }};
+foldersSectionStickers: icon {{ "folders/svg/section_stickers-36x36", sideBarIconFg }};
+foldersSectionChat: icon {{ "folders/svg/section_chat-36x36", sideBarIconFg }};
+foldersSectionLanguage: icon {{ "folders/svg/section_language-36x36", sideBarIconFg }};
+foldersSectionInfo: icon {{ "folders/svg/section_info-36x36", sideBarIconFg }};
diff --git a/Telegram/SourceFiles/window/window_filters_favorite.cpp b/Telegram/SourceFiles/window/window_filters_favorite.cpp
new file mode 100644
index 0000000000..5cceed629a
--- /dev/null
+++ b/Telegram/SourceFiles/window/window_filters_favorite.cpp
@@ -0,0 +1,498 @@
+/*
+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 "window/window_filters_favorite.h"
+
+#include "apiwrap.h"
+#include "base/options.h"
+#include "base/qthelp_url.h"
+#include "base/weak_ptr.h"
+#include "core/click_handler_types.h"
+#include "core/local_url_handlers.h"
+#include "data/data_changes.h"
+#include "data/data_file_origin.h"
+#include "data/data_msg_id.h"
+#include "data/data_peer.h"
+#include "data/data_peer_id.h"
+#include "data/data_photo.h"
+#include "data/data_session.h"
+#include "data/data_types.h"
+#include "data/data_user.h"
+#include "data/stickers/data_stickers.h"
+#include "data/stickers/data_stickers_set.h"
+#include "lang/lang_keys.h"
+#include "main/main_session.h"
+#include "ui/basic_click_handlers.h"
+#include "ui/dynamic_image.h"
+#include "ui/dynamic_thumbnails.h"
+#include "ui/layers/generic_box.h"
+#include "ui/painter.h"
+#include "ui/widgets/fields/input_field.h"
+#include "ui/widgets/menu/menu_add_action_callback.h"
+#include "ui/widgets/menu/menu_add_action_callback_factory.h"
+#include "ui/widgets/popup_menu.h"
+#include "window/window_session_controller.h"
+#include "styles/style_filter_icons.h"
+#include "styles/style_menu_icons.h"
+#include "styles/style_widgets.h"
+#include "styles/style_window.h"
+
+#include
+
+namespace Window {
+namespace {
+
+constexpr auto kMaxLabelLines = 3;
+
+base::options::option FavoriteLink({
+ .id = kOptionFolderFavoriteLink,
+ .name = "Favorite folder button",
+ .description = "Favorite button at the bottom of the folders strip "
+ "that opens a tg:// or t.me/ link.",
+});
+
+enum class Category {
+ Generic,
+ Peer,
+ Invite,
+ StickerSet,
+ EmojiSet,
+ ChatFolder,
+ Settings,
+};
+
+struct ParsedLink {
+ Category category = Category::Generic;
+ QString value;
+ bool phone = false;
+};
+
+[[nodiscard]] ParsedLink ParseFavoriteLink(const QString &link) {
+ const auto local = Core::TryConvertUrlToLocal(link.trimmed());
+ const auto prefix = u"tg://"_q;
+ if (!local.startsWith(prefix, Qt::CaseInsensitive)) {
+ return {};
+ }
+ const auto command = local.mid(prefix.size());
+ const auto queryStart = command.indexOf('?');
+ const auto name = (queryStart < 0)
+ ? command
+ : command.mid(0, queryStart);
+ const auto params = (queryStart < 0)
+ ? QMap()
+ : qthelp::url_parse_params(
+ command.mid(queryStart + 1),
+ qthelp::UrlParamNameTransform::ToLower);
+ const auto starts = [&](const QString &n) {
+ return name.startsWith(n, Qt::CaseInsensitive);
+ };
+ const auto with = [](Category category, const QString &value) {
+ return value.isEmpty() ? ParsedLink() : ParsedLink{ category, value };
+ };
+ if (starts(u"resolve"_q)) {
+ const auto phone = params.value(u"phone"_q);
+ if (!phone.isEmpty()) {
+ return { Category::Peer, phone, true };
+ }
+ const auto domain = params.value(u"domain"_q);
+ return (domain.isEmpty() || domain == u"giftcode"_q)
+ ? ParsedLink()
+ : ParsedLink{ Category::Peer, domain };
+ } else if (starts(u"join"_q)) {
+ return with(Category::Invite, params.value(u"invite"_q));
+ } else if (starts(u"addstickers"_q)) {
+ return with(Category::StickerSet, params.value(u"set"_q));
+ } else if (starts(u"addemoji"_q)) {
+ return with(Category::EmojiSet, params.value(u"set"_q));
+ } else if (starts(u"addlist"_q)) {
+ return with(Category::ChatFolder, params.value(u"slug"_q));
+ } else if (starts(u"settings"_q)) {
+ auto path = name.mid(u"settings"_q.size());
+ while (path.startsWith('/')) {
+ path = path.mid(1);
+ }
+ return { Category::Settings, path };
+ }
+ return {};
+}
+
+struct KnownContent {
+ const style::icon *icon = nullptr;
+ QString label;
+};
+
+[[nodiscard]] KnownContent SettingsContent(const QString &path) {
+ const auto starts = [&](const char *prefix) {
+ return path.startsWith(QString::fromLatin1(prefix), Qt::CaseInsensitive);
+ };
+ if (path.isEmpty()) {
+ return { &st::foldersSectionSettings, tr::lng_menu_settings(tr::now) };
+ } else if (starts("folders")) {
+ return { &st::foldersSectionFolders, tr::lng_filters_title(tr::now) };
+ } else if (starts("notifications")) {
+ return {
+ &st::foldersSectionNotifications,
+ tr::lng_settings_section_notify(tr::now),
+ };
+ } else if (starts("privacy") || starts("phone_privacy")) {
+ return {
+ &st::foldersSectionPrivacy,
+ tr::lng_settings_section_privacy(tr::now),
+ };
+ } else if (starts("appearance/stickers") || starts("stickers")) {
+ return {
+ &st::foldersSectionStickers,
+ tr::lng_settings_stickers_emoji(tr::now),
+ };
+ } else if (starts("appearance") || starts("chat") || starts("themes")) {
+ return {
+ &st::foldersSectionChat,
+ tr::lng_settings_section_chat_settings(tr::now),
+ };
+ } else if (starts("devices")) {
+ return {
+ &st::foldersSectionSettings,
+ tr::lng_settings_sessions_title(tr::now),
+ };
+ } else if (starts("language")) {
+ return {
+ &st::foldersSectionLanguage,
+ tr::lng_settings_language(tr::now),
+ };
+ } else if (starts("advanced")) {
+ return {
+ &st::foldersSectionSettings,
+ tr::lng_settings_advanced(tr::now),
+ };
+ } else if (starts("experiment")) {
+ return {
+ &st::foldersSectionSettings,
+ tr::lng_settings_experimental(tr::now),
+ };
+ } else if (starts("edit")
+ || starts("information")
+ || starts("my-profile")
+ || starts("edit_profile")) {
+ return {
+ &st::foldersSectionInfo,
+ tr::lng_settings_section_info(tr::now),
+ };
+ }
+ return { &st::foldersSectionSettings, tr::lng_menu_settings(tr::now) };
+}
+
+} // namespace
+
+const char kOptionFolderFavoriteLink[] = "folder-favorite-link";
+
+bool ValidFolderFavoriteLink(const QString &link) {
+ return Core::TryConvertUrlToLocal(link.trimmed())
+ .startsWith(u"tg://"_q, Qt::CaseInsensitive);
+}
+
+void EditFolderFavoriteLinkBox(not_null box) {
+ box->setTitle(tr::lng_menu_formatting_link_edit());
+ const auto field = box->addRow(object_ptr(
+ box,
+ st::defaultInputField,
+ tr::lng_info_link_label(),
+ FavoriteLink.value()));
+ box->setFocusCallback([=] { field->setFocusFast(); });
+ const auto submit = [=] {
+ const auto link = field->getLastText().trimmed();
+ if (link.isEmpty()) {
+ FavoriteLink.set(QString());
+ box->closeBox();
+ } else if (!ValidFolderFavoriteLink(link)) {
+ field->showError();
+ } else {
+ FavoriteLink.set(link);
+ box->closeBox();
+ }
+ };
+ field->submits(
+ ) | rpl::on_next([=](auto) { submit(); }, field->lifetime());
+ box->addButton(tr::lng_settings_save(), submit);
+ box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
+}
+
+FolderFavoriteButton::FolderFavoriteButton(
+ QWidget *parent,
+ not_null controller,
+ const style::SideBarButton &st)
+: RippleButton(parent, st.ripple)
+, _controller(controller)
+, _st(st)
+, _api(&controller->session().mtp())
+, _label(st.minTextWidth) {
+ setAttribute(Qt::WA_OpaquePaintEvent);
+
+ setClickedCallback([=] { openLink(); });
+
+ events(
+ ) | rpl::filter([](not_null e) {
+ return (e->type() == QEvent::ContextMenu);
+ }) | rpl::on_next([=](not_null e) {
+ showMenu();
+ e->accept();
+ }, lifetime());
+}
+
+FolderFavoriteButton::~FolderFavoriteButton() = default;
+
+rpl::producer FolderFavoriteButton::shownValue() const {
+ return _shown.value();
+}
+
+bool FolderFavoriteButton::shown() const {
+ return _shown.current();
+}
+
+int FolderFavoriteButton::resizeGetHeight(int newWidth) {
+ const auto result = _st.minHeight;
+ const auto text = std::min(
+ _label.countHeight(newWidth - _st.textSkip * 2),
+ _st.style.font->height * kMaxLabelLines);
+ const auto add = text - _st.style.font->height;
+ return result + std::max(add, 0);
+}
+
+void FolderFavoriteButton::paintEvent(QPaintEvent *e) {
+ auto p = Painter(this);
+ p.fillRect(e->rect(), _st.textBg);
+
+ RippleButton::paintRipple(p, 0, 0);
+
+ if (_thumbnail) {
+ const auto size = st::foldersFavorite.width();
+ const auto x = (width() - size) / 2;
+ p.drawImage(x, _st.iconPosition.y(), _thumbnail->image(size));
+ }
+
+ p.setPen(_st.textFg);
+ _label.draw(p, {
+ .position = { _st.textSkip, _st.textTop },
+ .availableWidth = width() - 2 * _st.textSkip,
+ .align = style::al_top,
+ .elisionLines = kMaxLabelLines,
+ });
+}
+
+void FolderFavoriteButton::setLink(const QString &link) {
+ _resolveLifetime.destroy();
+ _api.request(base::take(_requestId)).cancel();
+
+ _link = link.trimmed();
+ if (_link.isEmpty()) {
+ return;
+ }
+
+ const auto parsed = ParseFavoriteLink(_link);
+ switch (parsed.category) {
+ case Category::Peer:
+ resolvePeer(parsed.value, parsed.phone);
+ break;
+ case Category::Invite:
+ resolveInvite(parsed.value);
+ break;
+ case Category::StickerSet:
+ case Category::EmojiSet:
+ resolveStickerSet(parsed.value);
+ break;
+ case Category::ChatFolder:
+ apply({
+ .thumbnail = Ui::MakeIconThumbnail(st::foldersCustom),
+ .label = tr::lng_filters_title(tr::now),
+ });
+ break;
+ case Category::Settings: {
+ const auto content = SettingsContent(parsed.value);
+ apply({
+ .thumbnail = Ui::MakeIconThumbnail(*content.icon),
+ .label = content.label,
+ });
+ break;
+ }
+ default:
+ showGeneric();
+ break;
+ }
+}
+
+void FolderFavoriteButton::resolvePeer(const QString &value, bool phone) {
+ const auto session = &_controller->session();
+ if (const auto cached = phone
+ ? session->data().userByPhone(value)
+ : session->data().peerByUsername(value)) {
+ applyPeer(cached);
+ return;
+ }
+ const auto done = [=](const MTPcontacts_ResolvedPeer &result) {
+ _requestId = 0;
+ result.match([&](const MTPDcontacts_resolvedPeer &data) {
+ session->data().processUsers(data.vusers());
+ session->data().processChats(data.vchats());
+ if (const auto peerId = peerFromMTP(data.vpeer())) {
+ applyPeer(session->data().peer(peerId));
+ } else {
+ showGeneric();
+ }
+ });
+ };
+ const auto fail = [=](const MTP::Error &) {
+ _requestId = 0;
+ showGeneric();
+ };
+ if (phone) {
+ _requestId = _api.request(MTPcontacts_ResolvePhone(
+ MTP_string(value)
+ )).done(done).fail(fail).send();
+ } else {
+ _requestId = _api.request(MTPcontacts_ResolveUsername(
+ MTP_flags(0),
+ MTP_string(value),
+ MTP_string(QString())
+ )).done(done).fail(fail).send();
+ }
+}
+
+void FolderFavoriteButton::resolveInvite(const QString &hash) {
+ const auto session = &_controller->session();
+ const auto done = [=](const MTPChatInvite &result) {
+ _requestId = 0;
+ result.match([&](const MTPDchatInvite &data) {
+ const auto photo = session->data().processPhoto(data.vphoto());
+ auto thumbnail = std::shared_ptr();
+ if (!photo->isNull()) {
+ thumbnail = Ui::MakePhotoThumbnail(photo, FullMsgId());
+ } else {
+ const auto icon = data.is_broadcast()
+ ? &st::foldersChannels
+ : &st::foldersGroups;
+ thumbnail = Ui::MakeIconThumbnail(*icon);
+ }
+ apply({
+ .thumbnail = std::move(thumbnail),
+ .label = qs(data.vtitle()),
+ });
+ }, [&](const MTPDchatInviteAlready &data) {
+ applyPeer(session->data().processChat(data.vchat()));
+ }, [&](const MTPDchatInvitePeek &data) {
+ applyPeer(session->data().processChat(data.vchat()));
+ });
+ };
+ const auto fail = [=](const MTP::Error &) {
+ _requestId = 0;
+ showGeneric();
+ };
+ _requestId = _api.request(MTPmessages_CheckChatInvite(
+ MTP_string(hash)
+ )).done(done).fail(fail).send();
+}
+
+void FolderFavoriteButton::resolveStickerSet(const QString &shortName) {
+ const auto session = &_controller->session();
+ _requestId = _api.request(MTPmessages_GetStickerSet(
+ Data::InputStickerSet(StickerSetIdentifier{ .shortName = shortName }),
+ MTP_int(0)
+ )).done([=](const MTPmessages_StickerSet &result) {
+ _requestId = 0;
+ result.match([&](const MTPDmessages_stickerSet &data) {
+ const auto set = session->data().stickers().feedSetFull(data);
+ const auto thumb = set->lookupThumbnailDocument();
+ const auto document = thumb
+ ? thumb
+ : set->stickers.isEmpty()
+ ? nullptr
+ : set->stickers.front();
+ if (!document) {
+ showGeneric();
+ return;
+ }
+ apply({
+ .thumbnail = Ui::MakeDocumentThumbnail(
+ document,
+ Data::FileOrigin(Data::FileOriginStickerSet(
+ set->id,
+ set->accessHash))),
+ .label = set->title,
+ });
+ }, [&](const MTPDmessages_stickerSetNotModified &) {
+ showGeneric();
+ });
+ }).fail([=](const MTP::Error &) {
+ _requestId = 0;
+ showGeneric();
+ }).send();
+}
+
+void FolderFavoriteButton::applyPeer(not_null peer) {
+ peer->loadUserpic();
+ apply({
+ .thumbnail = Ui::MakeUserpicThumbnail(peer),
+ .label = peer->name(),
+ });
+ peer->session().changes().peerUpdates(
+ peer,
+ Data::PeerUpdate::Flag::Name
+ ) | rpl::on_next([=] {
+ _label.setText(_st.style, peer->name());
+ resizeToWidth(width());
+ update();
+ }, _resolveLifetime);
+}
+
+void FolderFavoriteButton::apply(Presentation presentation) {
+ _thumbnail = std::move(presentation.thumbnail);
+ _label.setText(_st.style, presentation.label);
+ if (_thumbnail) {
+ _thumbnail->subscribeToUpdates([=] { update(); });
+ }
+ _shown = true;
+ resizeToWidth(width());
+ update();
+}
+
+void FolderFavoriteButton::showGeneric() {
+ apply({
+ .thumbnail = Ui::MakeIconThumbnail(st::foldersFavorite),
+ .label = tr::lng_info_link_label(tr::now),
+ });
+}
+
+void FolderFavoriteButton::openLink() {
+ if (_link.isEmpty()) {
+ return;
+ }
+ UrlClickHandler::Open(
+ Core::TryConvertUrlToLocal(_link),
+ QVariant::fromValue(ClickHandlerContext{
+ .sessionWindow = base::make_weak(_controller.get()),
+ }));
+}
+
+void FolderFavoriteButton::showMenu() {
+ _menu = base::make_unique_q(this, st::popupMenuWithIcons);
+ const auto addAction = Ui::Menu::CreateAddActionCallback(_menu);
+ addAction(
+ tr::lng_menu_formatting_link_edit(tr::now),
+ [=] { editLink(); },
+ &st::menuIconEdit);
+ addAction({
+ .text = tr::lng_filters_context_remove(tr::now),
+ .handler = [=] { FavoriteLink.set(QString()); },
+ .icon = &st::menuIconDeleteAttention,
+ .isAttention = true,
+ });
+ _menu->popup(QCursor::pos());
+}
+
+void FolderFavoriteButton::editLink() {
+ _controller->show(Box(EditFolderFavoriteLinkBox));
+}
+
+} // namespace Window
diff --git a/Telegram/SourceFiles/window/window_filters_favorite.h b/Telegram/SourceFiles/window/window_filters_favorite.h
new file mode 100644
index 0000000000..14f3e391ae
--- /dev/null
+++ b/Telegram/SourceFiles/window/window_filters_favorite.h
@@ -0,0 +1,86 @@
+/*
+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
+
+#include "base/unique_qptr.h"
+#include "mtproto/sender.h"
+#include "ui/text/text.h"
+#include "ui/widgets/buttons.h"
+
+class PeerData;
+
+namespace style {
+struct SideBarButton;
+} // namespace style
+
+namespace Ui {
+class DynamicImage;
+class GenericBox;
+class PopupMenu;
+} // namespace Ui
+
+namespace Window {
+
+class SessionController;
+
+extern const char kOptionFolderFavoriteLink[];
+
+[[nodiscard]] bool ValidFolderFavoriteLink(const QString &link);
+
+void EditFolderFavoriteLinkBox(not_null box);
+
+class FolderFavoriteButton final : public Ui::RippleButton {
+public:
+ FolderFavoriteButton(
+ QWidget *parent,
+ not_null controller,
+ const style::SideBarButton &st);
+ ~FolderFavoriteButton();
+
+ [[nodiscard]] rpl::producer shownValue() const;
+ [[nodiscard]] bool shown() const;
+
+ void setLink(const QString &link);
+
+ int resizeGetHeight(int newWidth) override;
+
+private:
+ struct Presentation {
+ std::shared_ptr thumbnail;
+ QString label;
+ };
+
+ void paintEvent(QPaintEvent *e) override;
+
+ void resolvePeer(const QString &usernameOrPhone, bool phone);
+ void resolveInvite(const QString &hash);
+ void resolveStickerSet(const QString &shortName);
+ void applyPeer(not_null peer);
+ void apply(Presentation presentation);
+ void showGeneric();
+
+ void openLink();
+ void showMenu();
+ void editLink();
+
+ const not_null _controller;
+ const style::SideBarButton &_st;
+ MTP::Sender _api;
+
+ QString _link;
+ std::shared_ptr _thumbnail;
+ Ui::Text::String _label;
+ rpl::variable _shown = false;
+
+ mtpRequestId _requestId = 0;
+ base::unique_qptr _menu;
+ rpl::lifetime _resolveLifetime;
+
+};
+
+} // namespace Window
diff --git a/Telegram/SourceFiles/window/window_filters_menu.cpp b/Telegram/SourceFiles/window/window_filters_menu.cpp
index d22390eaff..842206c2ea 100644
--- a/Telegram/SourceFiles/window/window_filters_menu.cpp
+++ b/Telegram/SourceFiles/window/window_filters_menu.cpp
@@ -12,8 +12,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "window/window_controller.h"
#include "window/window_main_menu.h"
#include "window/window_peer_menu.h"
+#include "window/window_filters_favorite.h"
#include "main/main_session.h"
#include "base/event_filter.h"
+#include "base/options.h"
#include "core/ui_integration.h"
#include "data/data_session.h"
#include "data/data_chat_filters.h"
@@ -25,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/filter_icons.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/wrap/vertical_layout_reorder.h"
+#include "ui/wrap/slide_wrap.h"
#include "ui/widgets/menu/menu_add_action_callback_factory.h"
#include "ui/widgets/popup_menu.h"
#include "ui/boxes/confirm_box.h"
@@ -298,7 +301,13 @@ void FiltersMenu::setListTabStop(not_null stop) {
stop->setFocusPolicy(Qt::TabFocus);
_tabStop = stop.get();
QWidget::setTabOrder(&_menu, stop.get());
- QWidget::setTabOrder(stop.get(), _setup.get());
+ const auto favorite = _favorite ? _favorite->entity() : nullptr;
+ if (favorite) {
+ QWidget::setTabOrder(stop.get(), favorite);
+ QWidget::setTabOrder(favorite, _setup.get());
+ } else {
+ QWidget::setTabOrder(stop.get(), _setup.get());
+ }
}
bool FiltersMenu::listFocused() const {
@@ -421,6 +430,69 @@ void FiltersMenu::setupList() {
}
}
}, _outer.lifetime());
+
+ base::options::lookup(kOptionFolderFavoriteLink).changes(
+ ) | rpl::on_next([=] {
+ updateFavorite();
+ }, _outer.lifetime());
+ updateFavorite();
+}
+
+void FiltersMenu::updateFavorite() {
+ const auto link = base::options::lookup(
+ kOptionFolderFavoriteLink).value().trimmed();
+ if (link.isEmpty()) {
+ if (_favorite && _favorite->toggled()) {
+ // Animate out; the finished callback drops it afterwards.
+ _favorite->toggle(false, anim::type::normal);
+ } else if (_favorite) {
+ // Hidden already (e.g. still resolving), so nothing to animate.
+ destroyFavorite();
+ }
+ return;
+ }
+ if (!_favorite) {
+ createFavorite();
+ }
+ const auto button = _favorite->entity();
+ button->setLink(link);
+ if (button->shown() && !_favorite->toggled()) {
+ _favorite->toggle(true, anim::type::normal);
+ }
+}
+
+void FiltersMenu::createFavorite() {
+ _favorite = base::unique_qptr>(
+ _container->insert(
+ _container->count() - 1,
+ object_ptr>(
+ _container,
+ object_ptr(
+ _container,
+ _session,
+ st::windowFiltersButton))));
+ _favorite->toggle(false, anim::type::instant);
+ _favorite->setFinishedCallback([=] {
+ if (_favorite && !_favorite->toggled()) {
+ destroyFavorite();
+ }
+ });
+ _favorite->entity()->shownValue(
+ ) | rpl::on_next([=](bool shown) {
+ if (_favorite && shown) {
+ _favorite->toggle(true, anim::type::normal);
+ }
+ }, _favorite->lifetime());
+}
+
+void FiltersMenu::destroyFavorite() {
+ Ui::PostponeCall(&_outer, [=] {
+ const auto empty = base::options::lookup(
+ kOptionFolderFavoriteLink).value().trimmed().isEmpty();
+ if (_favorite && !_favorite->toggled() && empty) {
+ _favorite = nullptr;
+ }
+ });
}
bool FiltersMenu::premium() const {
diff --git a/Telegram/SourceFiles/window/window_filters_menu.h b/Telegram/SourceFiles/window/window_filters_menu.h
index 0739c770e3..4b77fbf2e6 100644
--- a/Telegram/SourceFiles/window/window_filters_menu.h
+++ b/Telegram/SourceFiles/window/window_filters_menu.h
@@ -23,11 +23,14 @@ class VerticalLayout;
class VerticalLayoutReorder;
enum class FilterIcon : uchar;
class PopupMenu;
+template
+class SlideWrap;
} // namespace Ui
namespace Window {
class SessionController;
+class FolderFavoriteButton;
class FiltersMenu final {
public:
@@ -40,6 +43,9 @@ private:
void setup();
void refresh();
void setupList();
+ void updateFavorite();
+ void createFavorite();
+ void destroyFavorite();
void applyReorder(
not_null widget,
int oldPosition,
@@ -73,6 +79,7 @@ private:
Ui::VerticalLayout *_list = nullptr;
std::unique_ptr _reorder;
base::unique_qptr _setup;
+ base::unique_qptr> _favorite;
base::flat_map> _filters;
base::weak_qptr _tabStop;
rpl::variable _includeMuted;