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;