mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-13 19:49:51 +01:00
1043f9f803
* refactor: remove unnecessary includes in headers * fix: formatting * chore: changelog * fix: scrollbar * fix: suggestions and old appbase remains * fix: suggestion * fix: missing Qt forward declarations * fix: another qt include * fix: includes for precompiled-headers=off * Add missing `<memory>` includes * Add missing `#pragma once` * Fix tests Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
372 lines
11 KiB
C++
372 lines
11 KiB
C++
#include "SearchPopup.hpp"
|
|
|
|
#include "Application.hpp"
|
|
#include "common/Channel.hpp"
|
|
#include "controllers/filters/FilterSet.hpp"
|
|
#include "controllers/hotkeys/HotkeyController.hpp"
|
|
#include "messages/MessageElement.hpp"
|
|
#include "messages/search/AuthorPredicate.hpp"
|
|
#include "messages/search/BadgePredicate.hpp"
|
|
#include "messages/search/ChannelPredicate.hpp"
|
|
#include "messages/search/LinkPredicate.hpp"
|
|
#include "messages/search/MessageFlagsPredicate.hpp"
|
|
#include "messages/search/RegexPredicate.hpp"
|
|
#include "messages/search/SubstringPredicate.hpp"
|
|
#include "messages/search/SubtierPredicate.hpp"
|
|
#include "singletons/WindowManager.hpp"
|
|
#include "widgets/helper/ChannelView.hpp"
|
|
#include "widgets/splits/Split.hpp"
|
|
|
|
#include <QHBoxLayout>
|
|
#include <QLineEdit>
|
|
#include <QPushButton>
|
|
|
|
namespace chatterino {
|
|
|
|
ChannelPtr SearchPopup::filter(const QString &text, const QString &channelName,
|
|
const LimitedQueueSnapshot<MessagePtr> &snapshot)
|
|
{
|
|
ChannelPtr channel(new Channel(channelName, Channel::Type::None));
|
|
|
|
// Parse predicates from tags in "text"
|
|
auto predicates = parsePredicates(text);
|
|
|
|
// Check for every message whether it fulfills all predicates that have
|
|
// been registered
|
|
for (size_t i = 0; i < snapshot.size(); ++i)
|
|
{
|
|
MessagePtr message = snapshot[i];
|
|
|
|
bool accept = true;
|
|
for (const auto &pred : predicates)
|
|
{
|
|
// Discard the message as soon as one predicate fails
|
|
if (!pred->appliesTo(*message))
|
|
{
|
|
accept = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If all predicates match, add the message to the channel
|
|
if (accept)
|
|
{
|
|
auto overrideFlags = boost::optional<MessageFlags>(message->flags);
|
|
overrideFlags->set(MessageFlag::DoNotLog);
|
|
|
|
channel->addMessage(message, overrideFlags);
|
|
}
|
|
}
|
|
|
|
return channel;
|
|
}
|
|
|
|
SearchPopup::SearchPopup(QWidget *parent, Split *split)
|
|
: BasePopup({BaseWindow::DisableLayoutSave}, parent)
|
|
, split_(split)
|
|
{
|
|
this->initLayout();
|
|
if (this->split_ && this->split_->getChannelView().hasSelection())
|
|
{
|
|
this->searchInput_->setText(
|
|
this->split_->getChannelView().getSelectedText().trimmed());
|
|
this->searchInput_->selectAll();
|
|
}
|
|
this->resize(400, 600);
|
|
this->addShortcuts();
|
|
}
|
|
|
|
void SearchPopup::addShortcuts()
|
|
{
|
|
HotkeyController::HotkeyMap actions{
|
|
{"search",
|
|
[this](const std::vector<QString> &) -> QString {
|
|
this->searchInput_->setFocus();
|
|
this->searchInput_->selectAll();
|
|
return "";
|
|
}},
|
|
{"delete",
|
|
[this](const std::vector<QString> &) -> QString {
|
|
this->close();
|
|
return "";
|
|
}},
|
|
|
|
{"reject", nullptr},
|
|
{"accept", nullptr},
|
|
{"openTab", nullptr},
|
|
{"scrollPage", nullptr},
|
|
};
|
|
|
|
this->shortcuts_ = getApp()->hotkeys->shortcutsForCategory(
|
|
HotkeyCategory::PopupWindow, actions, this);
|
|
}
|
|
|
|
void SearchPopup::addChannel(ChannelView &channel)
|
|
{
|
|
if (this->searchChannels_.empty())
|
|
{
|
|
this->channelView_->setSourceChannel(channel.channel());
|
|
this->channelName_ = channel.channel()->getName();
|
|
}
|
|
else if (this->searchChannels_.size() == 1)
|
|
{
|
|
this->channelView_->setSourceChannel(
|
|
std::make_shared<Channel>("multichannel", Channel::Type::None));
|
|
|
|
auto flags = this->channelView_->getFlags();
|
|
flags.set(MessageElementFlag::ChannelName);
|
|
flags.unset(MessageElementFlag::ModeratorTools);
|
|
this->channelView_->setOverrideFlags(flags);
|
|
}
|
|
|
|
this->searchChannels_.append(std::ref(channel));
|
|
|
|
this->updateWindowTitle();
|
|
}
|
|
|
|
void SearchPopup::goToMessage(const MessagePtr &message)
|
|
{
|
|
for (const auto &view : this->searchChannels_)
|
|
{
|
|
if (view.get().channel()->getType() == Channel::Type::TwitchMentions)
|
|
{
|
|
getApp()->windows->scrollToMessage(message);
|
|
return;
|
|
}
|
|
|
|
if (view.get().scrollToMessage(message))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void SearchPopup::goToMessageId(const QString &messageId)
|
|
{
|
|
for (const auto &view : this->searchChannels_)
|
|
{
|
|
if (view.get().scrollToMessageId(messageId))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void SearchPopup::updateWindowTitle()
|
|
{
|
|
QString historyName;
|
|
|
|
if (this->searchChannels_.size() > 1)
|
|
{
|
|
historyName = "multiple channels'";
|
|
}
|
|
else if (this->channelName_ == "/mentions")
|
|
{
|
|
historyName = "mentions";
|
|
}
|
|
else if (this->channelName_ == "/whispers")
|
|
{
|
|
historyName = "whispers";
|
|
}
|
|
else if (this->channelName_.isEmpty())
|
|
{
|
|
historyName = "<empty>'s";
|
|
}
|
|
else
|
|
{
|
|
historyName = QString("%1's").arg(this->channelName_);
|
|
}
|
|
this->setWindowTitle("Searching in " + historyName + " history");
|
|
}
|
|
|
|
void SearchPopup::showEvent(QShowEvent *)
|
|
{
|
|
this->search();
|
|
}
|
|
|
|
void SearchPopup::search()
|
|
{
|
|
if (this->snapshot_.size() == 0)
|
|
{
|
|
this->snapshot_ = this->buildSnapshot();
|
|
}
|
|
|
|
this->channelView_->setChannel(filter(this->searchInput_->text(),
|
|
this->channelName_, this->snapshot_));
|
|
}
|
|
|
|
LimitedQueueSnapshot<MessagePtr> SearchPopup::buildSnapshot()
|
|
{
|
|
// no point in filtering/sorting if it's a single channel search
|
|
if (this->searchChannels_.length() == 1)
|
|
{
|
|
const auto channelPtr = this->searchChannels_.at(0);
|
|
return channelPtr.get().channel()->getMessageSnapshot();
|
|
}
|
|
|
|
auto combinedSnapshot = std::vector<std::shared_ptr<const Message>>{};
|
|
for (auto &channel : this->searchChannels_)
|
|
{
|
|
ChannelView &sharedView = channel.get();
|
|
|
|
const FilterSetPtr filterSet = sharedView.getFilterSet();
|
|
const LimitedQueueSnapshot<MessagePtr> &snapshot =
|
|
sharedView.channel()->getMessageSnapshot();
|
|
|
|
// TODO: implement iterator on LimitedQueueSnapshot?
|
|
for (auto i = 0; i < snapshot.size(); ++i)
|
|
{
|
|
const MessagePtr &message = snapshot[i];
|
|
if (filterSet && !filterSet->filter(message, sharedView.channel()))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
combinedSnapshot.push_back(message);
|
|
}
|
|
}
|
|
|
|
// remove any duplicate messages from splits containing the same channel
|
|
std::sort(combinedSnapshot.begin(), combinedSnapshot.end(),
|
|
[](MessagePtr &a, MessagePtr &b) {
|
|
return a->id > b->id;
|
|
});
|
|
|
|
auto uniqueIterator =
|
|
std::unique(combinedSnapshot.begin(), combinedSnapshot.end(),
|
|
[](MessagePtr &a, MessagePtr &b) {
|
|
// nullptr check prevents system messages from being dropped
|
|
return (a->id != nullptr) && a->id == b->id;
|
|
});
|
|
|
|
combinedSnapshot.erase(uniqueIterator, combinedSnapshot.end());
|
|
|
|
// resort by time for presentation
|
|
std::sort(combinedSnapshot.begin(), combinedSnapshot.end(),
|
|
[](MessagePtr &a, MessagePtr &b) {
|
|
return a->serverReceivedTime < b->serverReceivedTime;
|
|
});
|
|
|
|
auto queue = LimitedQueue<MessagePtr>(combinedSnapshot.size());
|
|
queue.pushFront(combinedSnapshot);
|
|
|
|
return queue.getSnapshot();
|
|
}
|
|
|
|
void SearchPopup::initLayout()
|
|
{
|
|
// VBOX
|
|
{
|
|
auto *layout1 = new QVBoxLayout(this);
|
|
layout1->setContentsMargins(0, 0, 0, 0);
|
|
layout1->setSpacing(0);
|
|
|
|
// HBOX
|
|
{
|
|
auto *layout2 = new QHBoxLayout(this);
|
|
layout2->setContentsMargins(8, 8, 8, 8);
|
|
layout2->setSpacing(8);
|
|
|
|
// SEARCH INPUT
|
|
{
|
|
this->searchInput_ = new QLineEdit(this);
|
|
layout2->addWidget(this->searchInput_);
|
|
|
|
this->searchInput_->setPlaceholderText("Type to search");
|
|
this->searchInput_->setClearButtonEnabled(true);
|
|
this->searchInput_->findChild<QAbstractButton *>()->setIcon(
|
|
QPixmap(":/buttons/clearSearch.png"));
|
|
QObject::connect(this->searchInput_, &QLineEdit::textChanged,
|
|
this, &SearchPopup::search);
|
|
}
|
|
|
|
layout1->addLayout(layout2);
|
|
}
|
|
|
|
// CHANNELVIEW
|
|
{
|
|
this->channelView_ = new ChannelView(this, this->split_,
|
|
ChannelView::Context::Search);
|
|
|
|
layout1->addWidget(this->channelView_);
|
|
}
|
|
|
|
this->setLayout(layout1);
|
|
}
|
|
|
|
this->searchInput_->setFocus();
|
|
}
|
|
|
|
std::vector<std::unique_ptr<MessagePredicate>> SearchPopup::parsePredicates(
|
|
const QString &input)
|
|
{
|
|
// This regex captures all name:value predicate pairs into named capturing
|
|
// groups and matches all other inputs seperated by spaces as normal
|
|
// strings.
|
|
// It also ignores whitespaces in values when being surrounded by quotation
|
|
// marks, to enable inputs like this => regex:"kappa 123"
|
|
static QRegularExpression predicateRegex(
|
|
R"lit((?<negation>[!\-])?(?:(?<name>\w+):(?<value>".+?"|[^\s]+))|[^\s]+?(?=$|\s))lit");
|
|
static QRegularExpression trimQuotationMarksRegex(R"(^"|"$)");
|
|
|
|
QRegularExpressionMatchIterator it = predicateRegex.globalMatch(input);
|
|
|
|
std::vector<std::unique_ptr<MessagePredicate>> predicates;
|
|
|
|
while (it.hasNext())
|
|
{
|
|
QRegularExpressionMatch match = it.next();
|
|
|
|
QString name = match.captured("name");
|
|
bool isNegated = !match.captured("negation").isEmpty();
|
|
QString value = match.captured("value");
|
|
value.remove(trimQuotationMarksRegex);
|
|
|
|
// match predicates
|
|
|
|
if (name == "from")
|
|
{
|
|
predicates.push_back(
|
|
std::make_unique<AuthorPredicate>(value, isNegated));
|
|
}
|
|
else if (name == "badge")
|
|
{
|
|
predicates.push_back(
|
|
std::make_unique<BadgePredicate>(value, isNegated));
|
|
}
|
|
else if (name == "subtier")
|
|
{
|
|
predicates.push_back(
|
|
std::make_unique<SubtierPredicate>(value, isNegated));
|
|
}
|
|
else if (name == "has" && value == "link")
|
|
{
|
|
predicates.push_back(std::make_unique<LinkPredicate>(isNegated));
|
|
}
|
|
else if (name == "in")
|
|
{
|
|
predicates.push_back(
|
|
std::make_unique<ChannelPredicate>(value, isNegated));
|
|
}
|
|
else if (name == "is")
|
|
{
|
|
predicates.push_back(
|
|
std::make_unique<MessageFlagsPredicate>(value, isNegated));
|
|
}
|
|
else if (name == "regex")
|
|
{
|
|
predicates.push_back(
|
|
std::make_unique<RegexPredicate>(value, isNegated));
|
|
}
|
|
else
|
|
{
|
|
predicates.push_back(
|
|
std::make_unique<SubstringPredicate>(match.captured()));
|
|
}
|
|
}
|
|
|
|
return predicates;
|
|
}
|
|
|
|
} // namespace chatterino
|