Added recent search entries to settings.

This commit is contained in:
23rd
2026-04-11 17:11:30 +03:00
parent a1389ac778
commit 8ed9085474
8 changed files with 330 additions and 59 deletions
+2
View File
@@ -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
@@ -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<Data::Passkeys>(this))
, _faqSuggestions(std::make_unique<Settings::FaqSuggestions>(this))
, _recentSettingsSearches(std::make_unique<Settings::RecentSearches>(this))
, _cachedReactionIconFactory(std::make_unique<ReactionIconFactory>())
, _supportHelper(Support::Helper::Create(this))
, _fastButtonsBots(std::make_unique<Support::FastButtonsBots>(this))
+5
View File
@@ -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<Data::PromoSuggestions> _promoSuggestions;
const std::unique_ptr<Data::Passkeys> _passkeys;
const std::unique_ptr<Settings::FaqSuggestions> _faqSuggestions;
const std::unique_ptr<Settings::RecentSearches> _recentSettingsSearches;
using ReactionIconFactory = HistoryView::Reactions::CachedIconFactory;
const std::unique_ptr<ReactionIconFactory> _cachedReactionIconFactory;
@@ -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<Main::Session*> session)
: _session(session) {
}
RecentSearches::~RecentSearches() = default;
const std::vector<QString> &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
@@ -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<Main::Session*> session);
~RecentSearches();
[[nodiscard]] const std::vector<QString> &list() const;
void bump(const QString &entryId);
void remove(const QString &entryId);
[[nodiscard]] QByteArray serialize() const;
void applyLocal(QByteArray serialized);
private:
const not_null<Main::Session*> _session;
std::vector<QString> _list;
};
} // namespace Settings
+156 -56
View File
@@ -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 &registry = 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<Ui::SettingsButton*> 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<QEvent*> 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<Ui::PopupMenu>(
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 &registry = 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 &registry = 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<Ui::SettingsButton*> button) {
@@ -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<Ui::SettingsButton*> 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<QString, ResultCustomization> _customizations;
base::flat_map<QString, int> _entryIdToIndex;
QString _pendingQuery;
std::vector<IndexedEntry> _entries;
base::flat_map<QChar, base::flat_set<int>> _firstLetterIndex;
@@ -79,6 +86,7 @@ private:
int _faqStartIndex = 0;
std::vector<Ui::SettingsButton*> _visibleButtons;
base::flat_set<not_null<Ui::SettingsButton*>> _trackedButtons;
base::unique_qptr<Ui::PopupMenu> _contextMenu;
int _selected = -1;
};
@@ -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."));
}