Added favorite link button to folders strip.

This commit is contained in:
23rd
2026-07-19 20:09:01 +03:00
parent 4a1410bc15
commit b7eb9eea4e
15 changed files with 747 additions and 1 deletions
+2
View File
@@ -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
@@ -0,0 +1 @@
<svg width="36px" height="36px" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M27 18.5c0 3.6-4 6.5-9 6.5a11 11 0 0 1-3-.4L9.5 27l1-3.4A6.3 6.3 0 0 1 9 18.5C9 14.9 13 12 18 12s9 2.9 9 6.5z"/></svg>

After

Width:  |  Height:  |  Size: 307 B

@@ -0,0 +1 @@
<svg width="36px" height="36px" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 13a2 2 0 0 1 2-2h5l2.5 3H26a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H10a2 2 0 0 1-2-2z"/></svg>

After

Width:  |  Height:  |  Size: 276 B

@@ -0,0 +1 @@
<svg width="36px" height="36px" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="14.5" r="4.5"/><path d="M9.5 27.5a8.5 8.5 0 0 1 17 0"/></svg>

After

Width:  |  Height:  |  Size: 261 B

@@ -0,0 +1 @@
<svg width="36px" height="36px" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="18" r="10"/><path d="M8 18h20"/><path d="M18 8c3 2.5 4.5 6 4.5 10s-1.5 7.5-4.5 10c-3-2.5-4.5-6-4.5-10s1.5-7.5 4.5-10z"/></svg>

After

Width:  |  Height:  |  Size: 326 B

@@ -0,0 +1 @@
<svg width="36px" height="36px" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8a6.5 6.5 0 0 1 6.5 6.5v4l2 3h-17l2-3v-4A6.5 6.5 0 0 1 18 8z"/><path d="M15.5 25a2.5 2.5 0 0 0 5 0"/></svg>

After

Width:  |  Height:  |  Size: 300 B

@@ -0,0 +1 @@
<svg width="36px" height="36px" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="10" y="16.5" width="16" height="11.5" rx="2.5"/><path d="M13.5 16.5v-3.5a4.5 4.5 0 0 1 9 0v3.5"/></svg>

After

Width:  |  Height:  |  Size: 292 B

@@ -0,0 +1 @@
<svg width="36px" height="36px" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M26.07 16.06 28.92 16.71 28.92 19.29 26.07 19.94 25.08 22.34 26.64 24.81 24.81 26.64 22.34 25.08 19.94 26.07 19.29 28.92 16.71 28.92 16.06 26.07 13.66 25.08 11.19 26.64 9.36 24.81 10.92 22.34 9.93 19.94 7.08 19.29 7.08 16.71 9.93 16.06 10.92 13.66 9.36 11.19 11.19 9.36 13.66 10.92 16.06 9.93 16.71 7.08 19.29 7.08 19.94 9.93 22.34 10.92 24.81 9.36 26.64 11.19 25.08 13.66Z"/><circle cx="18" cy="18" r="3.6"/></svg>

After

Width:  |  Height:  |  Size: 604 B

@@ -0,0 +1 @@
<svg width="36px" height="36px" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="18" r="10"/><circle cx="14.5" cy="15.5" r="1.1" fill="#FFFFFF" stroke="none"/><circle cx="21.5" cy="15.5" r="1.1" fill="#FFFFFF" stroke="none"/><path d="M13.5 21a5 5 0 0 0 9 0"/></svg>

After

Width:  |  Height:  |  Size: 384 B

@@ -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::Controller*> window,
not_null<Ui::VerticalLayout*> container,
rpl::producer<QString> query,
Fn<void(const QString&, not_null<QWidget*>)> registerHighlight) {
const auto option = &base::options::lookup<QString>(
Window::kOptionFolderFavoriteLink);
const auto name = option->name().isEmpty()
? option->id()
: option->name();
const auto &description = option->description();
const auto wrap = container->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
container,
object_ptr<Ui::VerticalLayout>(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::Controller*> window,
not_null<Ui::VerticalLayout*> 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
@@ -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 }};
@@ -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 <QtGui/QtEvents>
namespace Window {
namespace {
constexpr auto kMaxLabelLines = 3;
base::options::option<QString> 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<QString, QString>()
: 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<Ui::GenericBox*> box) {
box->setTitle(tr::lng_menu_formatting_link_edit());
const auto field = box->addRow(object_ptr<Ui::InputField>(
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<SessionController*> 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<QEvent*> e) {
return (e->type() == QEvent::ContextMenu);
}) | rpl::on_next([=](not_null<QEvent*> e) {
showMenu();
e->accept();
}, lifetime());
}
FolderFavoriteButton::~FolderFavoriteButton() = default;
rpl::producer<bool> 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<Ui::DynamicImage>();
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<PeerData*> 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<Ui::PopupMenu>(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
@@ -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<Ui::GenericBox*> box);
class FolderFavoriteButton final : public Ui::RippleButton {
public:
FolderFavoriteButton(
QWidget *parent,
not_null<SessionController*> controller,
const style::SideBarButton &st);
~FolderFavoriteButton();
[[nodiscard]] rpl::producer<bool> shownValue() const;
[[nodiscard]] bool shown() const;
void setLink(const QString &link);
int resizeGetHeight(int newWidth) override;
private:
struct Presentation {
std::shared_ptr<Ui::DynamicImage> 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<PeerData*> peer);
void apply(Presentation presentation);
void showGeneric();
void openLink();
void showMenu();
void editLink();
const not_null<SessionController*> _controller;
const style::SideBarButton &_st;
MTP::Sender _api;
QString _link;
std::shared_ptr<Ui::DynamicImage> _thumbnail;
Ui::Text::String _label;
rpl::variable<bool> _shown = false;
mtpRequestId _requestId = 0;
base::unique_qptr<Ui::PopupMenu> _menu;
rpl::lifetime _resolveLifetime;
};
} // namespace Window
@@ -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<Ui::SideBarButton*> 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<QString>(kOptionFolderFavoriteLink).changes(
) | rpl::on_next([=] {
updateFavorite();
}, _outer.lifetime());
updateFavorite();
}
void FiltersMenu::updateFavorite() {
const auto link = base::options::lookup<QString>(
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<Ui::SlideWrap<FolderFavoriteButton>>(
_container->insert(
_container->count() - 1,
object_ptr<Ui::SlideWrap<FolderFavoriteButton>>(
_container,
object_ptr<FolderFavoriteButton>(
_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<QString>(
kOptionFolderFavoriteLink).value().trimmed().isEmpty();
if (_favorite && !_favorite->toggled() && empty) {
_favorite = nullptr;
}
});
}
bool FiltersMenu::premium() const {
@@ -23,11 +23,14 @@ class VerticalLayout;
class VerticalLayoutReorder;
enum class FilterIcon : uchar;
class PopupMenu;
template <typename Widget>
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<Ui::RpWidget*> widget,
int oldPosition,
@@ -73,6 +79,7 @@ private:
Ui::VerticalLayout *_list = nullptr;
std::unique_ptr<Ui::VerticalLayoutReorder> _reorder;
base::unique_qptr<Ui::SideBarButton> _setup;
base::unique_qptr<Ui::SlideWrap<FolderFavoriteButton>> _favorite;
base::flat_map<FilterId, base::unique_qptr<Ui::SideBarButton>> _filters;
base::weak_qptr<Ui::SideBarButton> _tabStop;
rpl::variable<bool> _includeMuted;