From 48aecae2d91d3bf860d6dabecd1df5b7cb540b55 Mon Sep 17 00:00:00 2001 From: AlexeyZavar Date: Mon, 26 Jan 2026 22:51:07 +0300 Subject: [PATCH] feat: search in deleted messages --- .../SourceFiles/ayu/data/ayu_database.cpp | 28 ++- Telegram/SourceFiles/ayu/data/ayu_database.h | 2 +- .../SourceFiles/ayu/data/messages_storage.cpp | 4 +- .../SourceFiles/ayu/data/messages_storage.h | 2 +- .../ayu/ui/message_history/history_inner.cpp | 26 ++- .../ayu/ui/message_history/history_inner.h | 3 + .../ui/message_history/history_section.cpp | 197 ++++++++++++++++-- .../ayu/ui/message_history/history_section.h | 9 + 8 files changed, 245 insertions(+), 26 deletions(-) diff --git a/Telegram/SourceFiles/ayu/data/ayu_database.cpp b/Telegram/SourceFiles/ayu/data/ayu_database.cpp index 4b5a13dbba..a2688f97ec 100644 --- a/Telegram/SourceFiles/ayu/data/ayu_database.cpp +++ b/Telegram/SourceFiles/ayu/data/ayu_database.cpp @@ -296,14 +296,38 @@ void addDeletedMessage(const DeletedMessage &message) { } } -std::vector getDeletedMessages(ID userId, ID dialogId, ID topicId, ID minId, ID maxId, int totalLimit) { +std::vector getDeletedMessages(ID userId, ID dialogId, ID topicId, ID minId, ID maxId, int totalLimit, const std::string &searchQuery) { + if (searchQuery.empty()) { + return storage.get_all( + where( + column(&DeletedMessage::userId) == userId and + column(&DeletedMessage::dialogId) == dialogId and + (column(&DeletedMessage::topicId) == topicId or topicId == 0) and + (column(&DeletedMessage::messageId) > minId or minId == 0) and + (column(&DeletedMessage::messageId) < maxId or maxId == 0) + ), + order_by(column(&DeletedMessage::messageId)).desc(), + limit(totalLimit) + ); + } + + std::string escaped; + escaped.reserve(searchQuery.size()); + for (const auto c : searchQuery) { + if (c == '%' || c == '_' || c == '\\') { + escaped += '\\'; + } + escaped += c; + } + const auto pattern = "%" + escaped + "%"; return storage.get_all( where( column(&DeletedMessage::userId) == userId and column(&DeletedMessage::dialogId) == dialogId and (column(&DeletedMessage::topicId) == topicId or topicId == 0) and (column(&DeletedMessage::messageId) > minId or minId == 0) and - (column(&DeletedMessage::messageId) < maxId or maxId == 0) + (column(&DeletedMessage::messageId) < maxId or maxId == 0) and + like(column(&DeletedMessage::text), pattern, "\\") ), order_by(column(&DeletedMessage::messageId)).desc(), limit(totalLimit) diff --git a/Telegram/SourceFiles/ayu/data/ayu_database.h b/Telegram/SourceFiles/ayu/data/ayu_database.h index f4de000606..70154c24d1 100644 --- a/Telegram/SourceFiles/ayu/data/ayu_database.h +++ b/Telegram/SourceFiles/ayu/data/ayu_database.h @@ -26,7 +26,7 @@ std::vector getEditedMessages(ID userId, ID dialogId, ID messageI bool hasRevisions(ID userId, ID dialogId, ID messageId); void addDeletedMessage(const DeletedMessage &message); -std::vector getDeletedMessages(ID userId, ID dialogId, ID topicId, ID minId, ID maxId, int totalLimit); +std::vector getDeletedMessages(ID userId, ID dialogId, ID topicId, ID minId, ID maxId, int totalLimit, const std::string &searchQuery = ""); bool hasDeletedMessages(ID userId, ID dialogId, ID topicId); std::vector getAllRegexFilters(); diff --git a/Telegram/SourceFiles/ayu/data/messages_storage.cpp b/Telegram/SourceFiles/ayu/data/messages_storage.cpp index 036d65781e..7008aeafda 100644 --- a/Telegram/SourceFiles/ayu/data/messages_storage.cpp +++ b/Telegram/SourceFiles/ayu/data/messages_storage.cpp @@ -127,10 +127,10 @@ void addDeletedMessage(not_null item) { } std::vector -getDeletedMessages(not_null peer, ID topicId, ID minId, ID maxId, int totalLimit) { +getDeletedMessages(not_null peer, ID topicId, ID minId, ID maxId, int totalLimit, const QString &searchQuery) { const ID userId = peer->session().userId().bare & PeerId::kChatTypeMask; return convertToBase( - AyuDatabase::getDeletedMessages(userId, getDialogIdFromPeer(peer), topicId, minId, maxId, totalLimit)); + AyuDatabase::getDeletedMessages(userId, getDialogIdFromPeer(peer), topicId, minId, maxId, totalLimit, searchQuery.toStdString())); } bool hasDeletedMessages(not_null peer, ID topicId) { diff --git a/Telegram/SourceFiles/ayu/data/messages_storage.h b/Telegram/SourceFiles/ayu/data/messages_storage.h index 8780ae1d12..334f271c88 100644 --- a/Telegram/SourceFiles/ayu/data/messages_storage.h +++ b/Telegram/SourceFiles/ayu/data/messages_storage.h @@ -17,7 +17,7 @@ std::vector getEditedMessages(not_null item, ID mi bool hasRevisions(not_null item); void addDeletedMessage(not_null item); -std::vector getDeletedMessages(not_null peer, ID topicId, ID minId, ID maxId, int totalLimit); +std::vector getDeletedMessages(not_null peer, ID topicId, ID minId, ID maxId, int totalLimit, const QString &searchQuery = QString()); bool hasDeletedMessages(not_null peer, ID topicId); } diff --git a/Telegram/SourceFiles/ayu/ui/message_history/history_inner.cpp b/Telegram/SourceFiles/ayu/ui/message_history/history_inner.cpp index 3f03ac131a..b07647de8e 100644 --- a/Telegram/SourceFiles/ayu/ui/message_history/history_inner.cpp +++ b/Telegram/SourceFiles/ayu/ui/message_history/history_inner.cpp @@ -472,6 +472,9 @@ void InnerWidget::checkPreloadMore() { } void InnerWidget::updateEmptyText() { + // auto text = !_searchQuery.isEmpty() + // ? tr::lng_admin_log_no_results_title(tr::now) + // : tr::lng_search_messages_none(tr::now); _emptyText.setMarkedText(st::defaultTextStyle, Ui::Text::Semibold(tr::lng_search_messages_none(tr::now))); } @@ -639,6 +642,7 @@ void InnerWidget::saveState(not_null memento) { base::take(_messageIds), _upLoaded, _downLoaded); + memento->setSearchQuery(base::take(_searchQuery)); base::take(_itemsByData); _upLoaded = _downLoaded = true; // Don't load or handle anything anymore. } @@ -652,10 +656,30 @@ void InnerWidget::restoreState(not_null memento) { _messageIds = memento->takeMessageIds(); _upLoaded = memento->upLoaded(); _downLoaded = memento->downLoaded(); + _searchQuery = memento->takeSearchQuery(); updateMinMaxIds(); updateSize(); } +void InnerWidget::applySearch(const QString &query) { + if (_item) { + return; + } + if (_searchQuery != query) { + _searchQuery = query; + _upLoaded = false; + _downLoaded = true; + _items.clear(); + _messageIds.clear(); + _itemsByData.clear(); + _itemDates.clear(); + updateMinMaxIds(); + updateEmptyText(); + updateSize(); + preloadMore(Direction::Up); + } +} + void InnerWidget::preloadMore(Direction direction) { auto &loadedFlag = (direction == Direction::Up) ? _upLoaded : _downLoaded; if (loadedFlag) { @@ -672,7 +696,7 @@ void InnerWidget::preloadMore(Direction direction) { messages = AyuMessages::getEditedMessages(_item, minId, maxId, perPage); } else { // viewing deleted messages - messages = AyuMessages::getDeletedMessages(_peer, _topicId, minId, maxId, perPage); + messages = AyuMessages::getDeletedMessages(_peer, _topicId, minId, maxId, perPage, _searchQuery); } crl::on_main([=] diff --git a/Telegram/SourceFiles/ayu/ui/message_history/history_inner.h b/Telegram/SourceFiles/ayu/ui/message_history/history_inner.h index fdc26e3d3e..913284bdb1 100644 --- a/Telegram/SourceFiles/ayu/ui/message_history/history_inner.h +++ b/Telegram/SourceFiles/ayu/ui/message_history/history_inner.h @@ -78,6 +78,8 @@ public: void saveState(not_null memento); void restoreState(not_null memento); + void applySearch(const QString &query); + // Ui::AbstractTooltipShower interface. QString tooltipText() const override; QPoint tooltipPos() const override; @@ -292,6 +294,7 @@ private: bool _upLoaded = true; bool _downLoaded = true; Ui::Text::String _emptyText; + QString _searchQuery; MouseAction _mouseAction = MouseAction::None; TextSelectType _mouseSelectType = TextSelectType::Letters; diff --git a/Telegram/SourceFiles/ayu/ui/message_history/history_section.cpp b/Telegram/SourceFiles/ayu/ui/message_history/history_section.cpp index 732472509b..357e6477b5 100644 --- a/Telegram/SourceFiles/ayu/ui/message_history/history_section.cpp +++ b/Telegram/SourceFiles/ayu/ui/message_history/history_section.cpp @@ -9,6 +9,7 @@ #include "apiwrap.h" #include "ayu/ui/message_history/history_inner.h" #include "base/timer.h" +#include "core/shortcuts.h" #include "data/data_channel.h" #include "data/data_session.h" #include "info/profile/info_profile_values.h" @@ -17,10 +18,12 @@ #include "styles/style_chat.h" #include "styles/style_chat_helpers.h" #include "styles/style_info.h" +#include "ui/effects/animations.h" #include "ui/ui_utility.h" #include "ui/boxes/confirm_box.h" #include "ui/controls/userpic_button.h" #include "ui/widgets/buttons.h" +#include "ui/widgets/fields/input_field.h" #include "ui/widgets/scroll_area.h" #include "ui/widgets/shadow.h" #include "window/window_adaptive.h" @@ -35,13 +38,25 @@ public: FixedBar( QWidget *parent, not_null controller, - not_null peer); + not_null peer, + bool searchEnabled); + + [[nodiscard]] rpl::producer<> searchCancelRequests() const; + [[nodiscard]] rpl::producer searchRequests() const; // When animating mode is enabled the content is hidden and the // whole fixed bar acts like a back button. void setAnimatingMode(bool enabled); void goBack(); + void showSearch(); + bool setSearchFocus() { + if (_searchShown) { + _field->setFocus(); + return true; + } + return false; + } protected: void mousePressEvent(QMouseEvent *e) override; @@ -49,12 +64,27 @@ protected: int resizeGetHeight(int newWidth) override; private: + void toggleSearch(); + void cancelSearch(); + void searchUpdated(); + void applySearch(); + void searchAnimationCallback(); + not_null _controller; not_null _peer; + object_ptr _field; object_ptr _backButton; + object_ptr _search; object_ptr _cancel; + Ui::Animations::Simple _searchShownAnimation; + bool _searchShown = false; + bool _searchEnabled = true; bool _animatingMode = false; + base::Timer _searchTimer; + + rpl::event_stream<> _searchCancelRequests; + rpl::event_stream _searchRequests; }; object_ptr SectionMemento::createWidget( @@ -73,18 +103,34 @@ object_ptr SectionMemento::createWidget( FixedBar::FixedBar( QWidget *parent, not_null controller, - not_null peer) : Ui::RpWidget(parent) + not_null peer, + bool searchEnabled) : Ui::RpWidget(parent) , _controller(controller) , _peer(peer) +, _field(this, st::defaultMultiSelectSearchField, tr::lng_dlg_filter()) , _backButton(this) -, _cancel(this, st::historyAdminLogCancelSearch) { +, _search(this, st::topBarSearch) +, _cancel(this, st::historyAdminLogCancelSearch) +, _searchEnabled(searchEnabled) { _backButton->moveToLeft(0, 0); - _backButton->setClickedCallback([=] - { - goBack(); - }); + _backButton->setClickedCallback([=] { goBack(); }); + _search->setClickedCallback([=] { showSearch(); }); + _cancel->setClickedCallback([=] { cancelSearch(); }); + _field->hide(); + _field->cancelled() | rpl::on_next([=] { + cancelSearch(); + }, _field->lifetime()); + _field->changes() | rpl::on_next([=] { + searchUpdated(); + }, _field->lifetime()); + _field->submits( + ) | rpl::on_next([=] { applySearch(); }, _field->lifetime()); + _searchTimer.setCallback([=] { applySearch(); }); _cancel->hide(anim::type::instant); + if (!_searchEnabled) { + _search->hide(); + } Info::Profile::NameValue(peer) | rpl::on_next([=](QString name) { _backButton->setText(name); @@ -99,19 +145,106 @@ void FixedBar::goBack() { _controller->showBackFromStack(); } +void FixedBar::showSearch() { + if (_searchEnabled && !_searchShown) { + toggleSearch(); + } +} + +void FixedBar::toggleSearch() { + _searchShown = !_searchShown; + _cancel->toggle(_searchShown, anim::type::normal); + _searchShownAnimation.start( + [=] { searchAnimationCallback(); }, + _searchShown ? 0. : 1., + _searchShown ? 1. : 0., + st::historyAdminLogSearchSlideDuration); + _search->setDisabled(_searchShown); + if (_searchShown) { + _field->show(); + _field->setFocus(); + } else { + _searchCancelRequests.fire({}); + } +} + +void FixedBar::searchAnimationCallback() { + if (!_searchShownAnimation.animating()) { + _field->setVisible(_searchShown); + _search->setIconOverride( + _searchShown ? &st::topBarSearch.icon : nullptr, + _searchShown ? &st::topBarSearch.icon : nullptr); + _search->setRippleColorOverride( + _searchShown ? &st::topBarBg : nullptr); + _search->setCursor( + _searchShown ? style::cur_default : style::cur_pointer); + _backButton->setOpacity(1.); + } + resizeToWidth(width()); +} + +void FixedBar::cancelSearch() { + if (_searchShown) { + if (!_field->getLastText().isEmpty()) { + _field->clear(); + _field->setFocus(); + applySearch(); + } else { + toggleSearch(); + } + } +} + +void FixedBar::searchUpdated() { + if (_field->getLastText().isEmpty()) { + applySearch(); + } else { + _searchTimer.callOnce(AutoSearchTimeout); + } +} + +void FixedBar::applySearch() { + _searchRequests.fire_copy(_field->getLastText()); +} + +rpl::producer<> FixedBar::searchCancelRequests() const { + return _searchCancelRequests.events(); +} + +rpl::producer FixedBar::searchRequests() const { + return _searchRequests.events(); +} + int FixedBar::resizeGetHeight(int newWidth) { - auto filterLeft = newWidth; - - auto cancelLeft = filterLeft - _cancel->width(); - _cancel->moveToLeft(cancelLeft, 0); - - auto searchShownLeft = st::topBarArrowPadding.left(); - auto searchHiddenLeft = filterLeft - 0; - auto searchCurrentLeft = anim::interpolate(searchHiddenLeft, searchShownLeft, 0.0); + const auto offset = st::historySendRight + st::lineWidth; + const auto searchShownLeft = st::topBarArrowPadding.left(); + const auto searchHiddenLeft = _searchEnabled + ? newWidth - _search->width() - offset + : newWidth; + const auto searchShown = _searchShownAnimation.value(_searchShown + ? 1. + : 0.); + const auto searchCurrentLeft = anim::interpolate( + searchHiddenLeft, + searchShownLeft, + searchShown); + if (_searchEnabled) { + _search->moveToLeft(searchCurrentLeft, 0); + } + _backButton->setOpacity(1. - searchShown); _backButton->resizeToWidth(searchCurrentLeft); _backButton->moveToLeft(0, 0); - auto newHeight = _backButton->height(); + const auto cancelLeft = newWidth - _cancel->width() - offset; + _cancel->moveToLeft(cancelLeft, 0); + + const auto newHeight = _backButton->height(); + const auto fieldLeft = searchShownLeft + _search->width(); + _field->setGeometryToLeft( + fieldLeft, + st::historyAdminLogSearchTop, + cancelLeft - fieldLeft, + _field->height()); return newHeight; } @@ -126,7 +259,11 @@ void FixedBar::setAnimatingMode(bool enabled) { } else { setAttribute(Qt::WA_OpaquePaintEvent); showChildren(); + _field->hide(); _cancel->setVisible(false); + if (!_searchEnabled) { + _search->hide(); + } } show(); } @@ -155,12 +292,20 @@ Widget::Widget( ID topicId) : Window::SectionWidget(parent, controller, rpl::single(peer)), _scroll(this, st::historyScroll, false), - _fixedBar(this, controller, peer), + _fixedBar(this, controller, peer, !item), _fixedBarShadow(this), _item(item), _topicId(topicId) { _fixedBar->move(0, 0); _fixedBar->resizeToWidth(width()); + _fixedBar->searchCancelRequests( + ) | rpl::on_next([=] { + setInnerFocus(); + }, lifetime()); + _fixedBar->searchRequests( + ) | rpl::on_next([=](const QString &query) { + _inner->applySearch(query); + }, lifetime()); _fixedBar->show(); _fixedBarShadow->raise(); @@ -219,7 +364,9 @@ QPixmap Widget::grabForShowAnimation(const Window::SectionSlideParams ¶ms) { } void Widget::doSetInnerFocus() { - _inner->setFocus(); + if (!_fixedBar->setSearchFocus()) { + _inner->setFocus(); + } } bool Widget::showInternal( @@ -241,7 +388,19 @@ void Widget::setInternalState(const QRect &geometry, not_null m } void Widget::setupShortcuts() { - // todo: smth + Shortcuts::Requests( + ) | rpl::filter([=] { + return Ui::AppInFocus() + && Ui::InFocusChain(this) + && !controller()->isLayerShown() + && isActiveWindow(); + }) | rpl::on_next([=](not_null request) { + using Command = Shortcuts::Command; + request->check(Command::Search, 2) && request->handle([=] { + _fixedBar->showSearch(); + return true; + }); + }, lifetime()); } std::shared_ptr Widget::createMemento() { diff --git a/Telegram/SourceFiles/ayu/ui/message_history/history_section.h b/Telegram/SourceFiles/ayu/ui/message_history/history_section.h index 71315edb76..fc1ff5943a 100644 --- a/Telegram/SourceFiles/ayu/ui/message_history/history_section.h +++ b/Telegram/SourceFiles/ayu/ui/message_history/history_section.h @@ -139,6 +139,14 @@ public: return _downLoaded; } + void setSearchQuery(QString &&query) { + _searchQuery = std::move(query); + } + + QString takeSearchQuery() { + return std::move(_searchQuery); + } + private: not_null _peer; HistoryItem *_item; @@ -148,6 +156,7 @@ private: std::set _messageIds; bool _upLoaded = false; bool _downLoaded = true; + QString _searchQuery; }; } // namespace MessageHistory