From 8ed9085474a4560b46d94cb0a117937f8c831385 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sat, 11 Apr 2026 17:11:30 +0300 Subject: [PATCH] Added recent search entries to settings. --- Telegram/CMakeLists.txt | 2 + Telegram/SourceFiles/main/main_session.cpp | 2 + Telegram/SourceFiles/main/main_session.h | 5 + .../settings/settings_recent_searches.cpp | 108 +++++++++ .../settings/settings_recent_searches.h | 36 +++ .../SourceFiles/settings/settings_search.cpp | 212 +++++++++++++----- .../SourceFiles/settings/settings_search.h | 8 + .../SourceFiles/storage/storage_account.cpp | 16 +- 8 files changed, 330 insertions(+), 59 deletions(-) create mode 100644 Telegram/SourceFiles/settings/settings_recent_searches.cpp create mode 100644 Telegram/SourceFiles/settings/settings_recent_searches.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index a737da8da0..141ec5e91b 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1656,6 +1656,8 @@ PRIVATE settings/sections/settings_local_passcode.h settings/sections/settings_main.cpp settings/sections/settings_main.h + settings/settings_recent_searches.cpp + settings/settings_recent_searches.h settings/settings_search.cpp settings/settings_search.h settings/settings_faq_suggestions.cpp diff --git a/Telegram/SourceFiles/main/main_session.cpp b/Telegram/SourceFiles/main/main_session.cpp index 3b00a73f31..0dec942268 100644 --- a/Telegram/SourceFiles/main/main_session.cpp +++ b/Telegram/SourceFiles/main/main_session.cpp @@ -41,6 +41,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/components/sponsored_messages.h" #include "data/components/top_peers.h" #include "settings/settings_faq_suggestions.h" +#include "settings/settings_recent_searches.h" #include "data/data_session.h" #include "data/data_changes.h" #include "data/data_user.h" @@ -159,6 +160,7 @@ Session::Session( })) , _passkeys(std::make_unique(this)) , _faqSuggestions(std::make_unique(this)) +, _recentSettingsSearches(std::make_unique(this)) , _cachedReactionIconFactory(std::make_unique()) , _supportHelper(Support::Helper::Create(this)) , _fastButtonsBots(std::make_unique(this)) diff --git a/Telegram/SourceFiles/main/main_session.h b/Telegram/SourceFiles/main/main_session.h index 085c74c1ff..9b899e29ee 100644 --- a/Telegram/SourceFiles/main/main_session.h +++ b/Telegram/SourceFiles/main/main_session.h @@ -47,6 +47,7 @@ class Passkeys; namespace Settings { class FaqSuggestions; +class RecentSearches; } // namespace Settings namespace HistoryView::Reactions { @@ -212,6 +213,9 @@ public: [[nodiscard]] Settings::FaqSuggestions &faqSuggestions() const { return *_faqSuggestions; } + [[nodiscard]] Settings::RecentSearches &recentSettingsSearches() const { + return *_recentSettingsSearches; + } [[nodiscard]] auto cachedReactionIconFactory() const -> HistoryView::Reactions::CachedIconFactory & { return *_cachedReactionIconFactory; @@ -318,6 +322,7 @@ private: const std::unique_ptr _promoSuggestions; const std::unique_ptr _passkeys; const std::unique_ptr _faqSuggestions; + const std::unique_ptr _recentSettingsSearches; using ReactionIconFactory = HistoryView::Reactions::CachedIconFactory; const std::unique_ptr _cachedReactionIconFactory; diff --git a/Telegram/SourceFiles/settings/settings_recent_searches.cpp b/Telegram/SourceFiles/settings/settings_recent_searches.cpp new file mode 100644 index 0000000000..6c9c106dfb --- /dev/null +++ b/Telegram/SourceFiles/settings/settings_recent_searches.cpp @@ -0,0 +1,108 @@ +/* +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 "settings/settings_recent_searches.h" + +#include "main/main_session.h" +#include "storage/serialize_common.h" +#include "storage/storage_account.h" + +namespace Settings { +namespace { + +constexpr auto kLimit = 32; + +} // namespace + +RecentSearches::RecentSearches(not_null session) +: _session(session) { +} + +RecentSearches::~RecentSearches() = default; + +const std::vector &RecentSearches::list() const { + _session->local().readSearchSuggestions(); + + return _list; +} + +void RecentSearches::bump(const QString &entryId) { + if (entryId.isEmpty()) { + return; + } + _session->local().readSearchSuggestions(); + + if (!_list.empty() && _list.front() == entryId) { + return; + } + auto i = ranges::find(_list, entryId); + if (i == end(_list)) { + if (int(_list.size()) >= kLimit) { + _list.pop_back(); + } + _list.push_back(entryId); + i = end(_list) - 1; + } + ranges::rotate(begin(_list), i, i + 1); + + _session->local().writeSearchSuggestionsDelayed(); +} + +void RecentSearches::remove(const QString &entryId) { + const auto i = ranges::find(_list, entryId); + if (i != end(_list)) { + _list.erase(i); + } + _session->local().writeSearchSuggestionsDelayed(); +} + +QByteArray RecentSearches::serialize() const { + _session->local().readSearchSuggestions(); + + if (_list.empty()) { + return {}; + } + const auto count = std::min(int(_list.size()), kLimit); + auto size = 2 * int(sizeof(quint32)); + for (auto i = 0; i < count; ++i) { + size += Serialize::stringSize(_list[i]); + } + auto stream = Serialize::ByteArrayWriter(size); + stream + << quint32(AppVersion) + << quint32(count); + for (auto i = 0; i < count; ++i) { + stream << _list[i]; + } + return std::move(stream).result(); +} + +void RecentSearches::applyLocal(QByteArray serialized) { + _list.clear(); + if (serialized.isEmpty()) { + return; + } + auto stream = Serialize::ByteArrayReader(serialized); + auto version = quint32(); + auto count = quint32(); + stream >> version >> count; + if (!stream.ok()) { + return; + } + _list.reserve(count); + for (auto i = quint32(0); i < count; ++i) { + auto value = QString(); + stream >> value; + if (!stream.ok()) { + _list.clear(); + return; + } + _list.push_back(value); + } +} + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_recent_searches.h b/Telegram/SourceFiles/settings/settings_recent_searches.h new file mode 100644 index 0000000000..6a3ab9a3e9 --- /dev/null +++ b/Telegram/SourceFiles/settings/settings_recent_searches.h @@ -0,0 +1,36 @@ +/* +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 + +namespace Main { +class Session; +} // namespace Main + +namespace Settings { + +class RecentSearches final { +public: + explicit RecentSearches(not_null session); + ~RecentSearches(); + + [[nodiscard]] const std::vector &list() const; + + void bump(const QString &entryId); + void remove(const QString &entryId); + + [[nodiscard]] QByteArray serialize() const; + void applyLocal(QByteArray serialized); + +private: + const not_null _session; + + std::vector _list; + +}; + +} // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_search.cpp b/Telegram/SourceFiles/settings/settings_search.cpp index 9176eff279..97578817e0 100644 --- a/Telegram/SourceFiles/settings/settings_search.cpp +++ b/Telegram/SourceFiles/settings/settings_search.cpp @@ -15,11 +15,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_builder.h" #include "settings/settings_common.h" #include "settings/settings_faq_suggestions.h" +#include "settings/settings_recent_searches.h" #include "ui/painter.h" #include "ui/text/text_entity.h" #include "ui/search_field_controller.h" #include "ui/vertical_list.h" #include "ui/widgets/buttons.h" +#include "ui/widgets/popup_menu.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/elastic_scroll.h" #include "ui/widgets/fields/input_field.h" @@ -260,17 +262,22 @@ void Search::setupCustomizations() { void Search::buildIndex() { _entries.clear(); _firstLetterIndex.clear(); + _entryIdToIndex.clear(); const auto ®istry = Builder::SearchRegistry::Instance(); const auto rawEntries = registry.collectAll(&controller()->session()); _entries.reserve(rawEntries.size()); for (const auto &entry : rawEntries) { + const auto index = int(_entries.size()); auto indexed = IndexedEntry{ .entry = entry, .terms = PrepareEntryWords(entry), .depth = CalculateDepth(entry.section, registry), }; + if (!entry.id.isEmpty()) { + _entryIdToIndex[entry.id] = index; + } _entries.push_back(std::move(indexed)); } @@ -402,6 +409,90 @@ void Search::handleKeyNavigation(int key) { } } +not_null Search::createEntryButton( + int entryIndex, + const QString &subtitle) { + const auto &indexed = _entries[entryIndex]; + const auto &entry = indexed.entry; + const auto hasIcon = entry.icon.icon != nullptr; + const auto hasCheckIcon = !hasIcon + && (entry.checkIcon != Builder::SearchEntryCheckIcon::None); + + const auto it = _customizations.find(entry.id); + const auto custom = (it != _customizations.end()) + ? &it->second + : nullptr; + + const auto &st = custom && custom->st + ? *custom->st + : (hasIcon || hasCheckIcon) + ? st::settingsSearchResult + : st::settingsSearchResultNoIcon; + + const auto button = CreateSearchResultButtonRaw( + this, + entry.title, + subtitle, + st, + IconDescriptor{ entry.icon.icon }, + (hasCheckIcon + ? entry.checkIcon + : Builder::SearchEntryCheckIcon::None)); + + if (custom && custom->hook) { + custom->hook(button); + } + + const auto controlId = entry.id; + if (!controlId.isEmpty()) { + const auto targetSection = entry.section; + const auto deeplink = entry.deeplink; + button->addClickHandler([=] { + bumpRecentEntry(controlId); + if (!deeplink.isEmpty()) { + Core::App().openLocalUrl( + deeplink, + QVariant::fromValue(ClickHandlerContext{ + .sessionWindow = base::make_weak(controller()), + })); + } else { + controller()->setHighlightControlId(controlId); + showOtherMethod()(targetSection); + } + }); + + base::install_event_filter(button, [=](not_null e) { + if (e->type() != QEvent::ContextMenu) { + return base::EventFilterResult::Continue; + } + const auto &recentIds + = controller()->session().recentSettingsSearches().list(); + if (!ranges::contains(recentIds, controlId)) { + return base::EventFilterResult::Continue; + } + _contextMenu = base::make_unique_q( + button, + st::popupMenuWithIcons); + _contextMenu->addAction( + tr::lng_recent_remove(tr::now), + [=] { + controller()->session().recentSettingsSearches().remove( + controlId); + const auto query = _searchController + ? _searchController->query() + : QString(); + rebuildResults(query); + }, + &st::menuIconDelete); + _contextMenu->popup(QCursor::pos()); + return base::EventFilterResult::Cancel; + }, button->lifetime()); + } + + _buttonCache.emplace(entryIndex, button); + return button; +} + void Search::rebuildResults(const QString &query) { for (auto i = 0, count = _list->count(); i != count; ++i) { _list->widgetAt(i)->hide(); @@ -413,7 +504,9 @@ void Search::rebuildResults(const QString &query) { const auto queryWords = TextUtilities::PrepareSearchWords(query); if (queryWords.isEmpty()) { + rebuildRecentResults(); rebuildFaqResults(); + _list->resizeToWidth(_list->width()); return; } @@ -475,7 +568,6 @@ void Search::rebuildResults(const QString &query) { st::settingsSearchNoResults), st::settingsSearchNoResultsPadding); } else { - const auto showOther = showOtherMethod(); const auto ®istry = Builder::SearchRegistry::Instance(); const auto faqSubtitle = tr::lng_settings_faq_subtitle(tr::now); const auto weak = base::make_weak(controller()); @@ -492,43 +584,18 @@ void Search::rebuildResults(const QString &query) { continue; } - auto subtitle = QString(); if (isFaq) { - subtitle = faqSubtitle + u" > "_q + indexed.faqSection; - } else { - const auto parentsOnly = entry.id.isEmpty(); - subtitle = registry.sectionPath(entry.section, parentsOnly); - } - const auto hasIcon = entry.icon.icon != nullptr; - const auto hasCheckIcon = !hasIcon - && (entry.checkIcon != Builder::SearchEntryCheckIcon::None); + const auto subtitle = faqSubtitle + + u" > "_q + + indexed.faqSection; + const auto button = CreateSearchResultButtonRaw( + this, + entry.title, + subtitle, + st::settingsSearchResultNoIcon, + IconDescriptor{}, + Builder::SearchEntryCheckIcon::None); - const auto it = _customizations.find(entry.id); - const auto custom = (it != _customizations.end()) - ? &it->second - : nullptr; - - const auto &st = custom && custom->st - ? *custom->st - : (hasIcon || hasCheckIcon) - ? st::settingsSearchResult - : st::settingsSearchResultNoIcon; - - const auto button = CreateSearchResultButtonRaw( - this, - entry.title, - subtitle, - st, - IconDescriptor{ entry.icon.icon }, - (hasCheckIcon - ? entry.checkIcon - : Builder::SearchEntryCheckIcon::None)); - - if (custom && custom->hook) { - custom->hook(button); - } - - if (isFaq) { const auto url = indexed.faqUrl; button->addClickHandler([=] { UrlClickHandler::Open( @@ -537,26 +604,16 @@ void Search::rebuildResults(const QString &query) { .sessionWindow = weak, })); }); - } else { - const auto targetSection = entry.section; - const auto controlId = entry.id; - const auto deeplink = entry.deeplink; - button->addClickHandler([=] { - if (!deeplink.isEmpty()) { - Core::App().openLocalUrl( - deeplink, - QVariant::fromValue(ClickHandlerContext{ - .sessionWindow = base::make_weak(controller()), - })); - } else { - controller()->setHighlightControlId(controlId); - showOther(targetSection); - } - }); - } - _buttonCache.emplace(entryIndex, button); - addButton(button); + _buttonCache.emplace(entryIndex, button); + addButton(button); + } else { + const auto parentsOnly = entry.id.isEmpty(); + const auto subtitle = registry.sectionPath( + entry.section, + parentsOnly); + addButton(createEntryButton(entryIndex, subtitle)); + } } } @@ -583,11 +640,55 @@ void Search::sectionRestoreState(const std::any &state) { } } +void Search::bumpRecentEntry(const QString &entryId) { + if (!entryId.isEmpty()) { + controller()->session().recentSettingsSearches().bump(entryId); + } +} + +void Search::rebuildRecentResults() { + const auto &recentIds + = controller()->session().recentSettingsSearches().list(); + if (recentIds.empty()) { + return; + } + + const auto ®istry = Builder::SearchRegistry::Instance(); + + auto added = false; + for (const auto &entryId : recentIds) { + const auto it = _entryIdToIndex.find(entryId); + if (it == _entryIdToIndex.end()) { + continue; + } + if (!added) { + Ui::AddSubsectionTitle(_list, tr::lng_recent_title()); + added = true; + } + const auto entryIndex = it->second; + const auto cached = _buttonCache.find(entryIndex); + if (cached != _buttonCache.end()) { + addButton(cached->second); + continue; + } + const auto &entry = _entries[entryIndex].entry; + const auto parentsOnly = entry.id.isEmpty(); + const auto subtitle = registry.sectionPath( + entry.section, + parentsOnly); + addButton(createEntryButton(entryIndex, subtitle)); + } +} + void Search::rebuildFaqResults() { if (_faqStartIndex >= int(_entries.size())) { return; } + if (!_visibleButtons.empty()) { + Ui::AddSubsectionTitle(_list, tr::lng_settings_faq()); + } + const auto faqSubtitle = tr::lng_settings_faq_subtitle(tr::now); const auto weak = base::make_weak(controller()); @@ -621,7 +722,6 @@ void Search::rebuildFaqResults() { _buttonCache.emplace(i, button); addButton(button); } - _list->resizeToWidth(_list->width()); } void Search::addButton(not_null button) { diff --git a/Telegram/SourceFiles/settings/settings_search.h b/Telegram/SourceFiles/settings/settings_search.h index 26e2b9cb12..2f3afcd689 100644 --- a/Telegram/SourceFiles/settings/settings_search.h +++ b/Telegram/SourceFiles/settings/settings_search.h @@ -19,6 +19,7 @@ class SessionController; namespace Ui { class InputField; +class PopupMenu; class RpWidget; class SearchFieldController; class VerticalLayout; @@ -60,7 +61,12 @@ private: void setupCustomizations(); void buildIndex(); void rebuildResults(const QString &query); + void rebuildRecentResults(); void rebuildFaqResults(); + void bumpRecentEntry(const QString &entryId); + [[nodiscard]] not_null createEntryButton( + int entryIndex, + const QString &subtitle); void selectByKeyboard(int newSelected); void clearSelection(); void handleKeyNavigation(int key); @@ -72,6 +78,7 @@ private: Ui::InputField *_searchField = nullptr; Ui::VerticalLayout *_list = nullptr; base::flat_map _customizations; + base::flat_map _entryIdToIndex; QString _pendingQuery; std::vector _entries; base::flat_map> _firstLetterIndex; @@ -79,6 +86,7 @@ private: int _faqStartIndex = 0; std::vector _visibleButtons; base::flat_set> _trackedButtons; + base::unique_qptr _contextMenu; int _selected = -1; }; diff --git a/Telegram/SourceFiles/storage/storage_account.cpp b/Telegram/SourceFiles/storage/storage_account.cpp index 7d94ef4c15..2e42e08dfb 100644 --- a/Telegram/SourceFiles/storage/storage_account.cpp +++ b/Telegram/SourceFiles/storage/storage_account.cpp @@ -29,6 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/core_settings.h" #include "core/file_location.h" #include "data/components/recent_peers.h" +#include "settings/settings_recent_searches.h" #include "data/components/top_peers.h" #include "data/stickers/data_stickers.h" #include "data/data_session.h" @@ -3200,7 +3201,9 @@ void Account::writeSearchSuggestions() { const auto top = _owner->session().topPeers().serialize(); const auto recent = _owner->session().recentPeers().serialize(); - if (top.isEmpty() && recent.isEmpty()) { + const auto settingsSearches + = _owner->session().recentSettingsSearches().serialize(); + if (top.isEmpty() && recent.isEmpty() && settingsSearches.isEmpty()) { if (_searchSuggestionsKey) { ClearKey(_searchSuggestionsKey, _basePath); _searchSuggestionsKey = 0; @@ -3213,9 +3216,10 @@ void Account::writeSearchSuggestions() { writeMapQueued(); } quint32 size = Serialize::bytearraySize(top) - + Serialize::bytearraySize(recent); + + Serialize::bytearraySize(recent) + + Serialize::bytearraySize(settingsSearches); EncryptedDescriptor data(size); - data.stream << top << recent; + data.stream << top << recent << settingsSearches; FileWriteDescriptor file(_searchSuggestionsKey, _basePath); file.writeEncrypted(data, _localKey); @@ -3242,10 +3246,16 @@ void Account::readSearchSuggestions() { auto top = QByteArray(); auto recent = QByteArray(); + auto settingsSearches = QByteArray(); suggestions.stream >> top >> recent; + if (!suggestions.stream.atEnd()) { + suggestions.stream >> settingsSearches; + } if (CheckStreamStatus(suggestions.stream)) { _owner->session().topPeers().applyLocal(top); _owner->session().recentPeers().applyLocal(recent); + _owner->session().recentSettingsSearches().applyLocal( + settingsSearches); } else { DEBUG_LOG(("Suggestions: Could not read content.")); }